mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Complete attribute values in manifest files
This commit is contained in:
+28
@@ -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<String> {
|
||||
return ResourceTableRegistry.getInstance()
|
||||
.getActivityActions(getPlatformDir() ?: return emptyList())
|
||||
}
|
||||
|
||||
/** @see ResourceTableRegistry.getBroadcastActions */
|
||||
fun getBroadcastActions(): List<String> {
|
||||
return ResourceTableRegistry.getInstance()
|
||||
.getBroadcastActions(getPlatformDir() ?: return emptyList())
|
||||
}
|
||||
|
||||
/** @see ResourceTableRegistry.getServiceActions */
|
||||
fun getServiceActions(): List<String> {
|
||||
return ResourceTableRegistry.getInstance()
|
||||
.getServiceActions(getPlatformDir() ?: return emptyList())
|
||||
}
|
||||
|
||||
/** @see ResourceTableRegistry.getCategories */
|
||||
fun getCategories(): List<String> {
|
||||
return ResourceTableRegistry.getInstance().getCategories(getPlatformDir() ?: return emptyList())
|
||||
}
|
||||
|
||||
/** @see ResourceTableRegistry.getFeatures */
|
||||
fun getFeatures(): List<String> {
|
||||
return ResourceTableRegistry.getInstance().getFeatures(getPlatformDir() ?: return emptyList())
|
||||
}
|
||||
|
||||
private fun getPlatformDir() = bootClassPaths.firstOrNull { it.name == "android.jar" }?.parentFile
|
||||
}
|
||||
|
||||
+15
@@ -62,6 +62,21 @@ interface ResourceTableRegistry : XmlRegistry<ResourceTable> {
|
||||
*/
|
||||
fun getManifestAttrTable(platform: File): ResourceTable?
|
||||
|
||||
/** Get the list of all activity actions for the given [platform] directory. */
|
||||
fun getActivityActions(platform: File): List<String>
|
||||
|
||||
/** Get the list of all broadcast actions for the given [platform] directory. */
|
||||
fun getBroadcastActions(platform: File): List<String>
|
||||
|
||||
/** Get the list of all service actions for the given [platform] directory. */
|
||||
fun getServiceActions(platform: File): List<String>
|
||||
|
||||
/** Get the list of all categories for the given [platform] directory. */
|
||||
fun getCategories(platform: File): List<String>
|
||||
|
||||
/** Get the list of all features for the given [platform] directory. */
|
||||
fun getFeatures(platform: File): List<String>
|
||||
|
||||
override fun forPlatformDir(platform: File): ResourceTable? {
|
||||
return forPackage(PCK_ANDROID, File(platform, "data/res"))
|
||||
}
|
||||
|
||||
+94
-8
@@ -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<String, ResourceTable>()
|
||||
private val platformTables = ConcurrentHashMap<String, ResourceTable>()
|
||||
private val manifestAttrs = ConcurrentHashMap<String, ResourceTable>()
|
||||
private val singleLineValueEntries =
|
||||
ConcurrentHashMap<String, ConcurrentHashMap<SingleLineValueEntryType, List<String>>>()
|
||||
|
||||
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<String> {
|
||||
return getSingleLineEntry(platform, ACTIVITY_ACTIONS)
|
||||
}
|
||||
|
||||
override fun getBroadcastActions(platform: File): List<String> {
|
||||
return getSingleLineEntry(platform, BROADCAST_ACTIONS)
|
||||
}
|
||||
|
||||
override fun getServiceActions(platform: File): List<String> {
|
||||
return getSingleLineEntry(platform, SERVICE_ACTIONS)
|
||||
}
|
||||
|
||||
override fun getCategories(platform: File): List<String> {
|
||||
return getSingleLineEntry(platform, CATEGORIES)
|
||||
}
|
||||
|
||||
override fun getFeatures(platform: File): List<String> {
|
||||
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<String> {
|
||||
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<SingleLineValueEntryType, List<String>> {
|
||||
val map = ConcurrentHashMap<SingleLineValueEntryType, List<String>>()
|
||||
readSingleLineEntriesTo(platform, type, map)
|
||||
return map
|
||||
}
|
||||
|
||||
private fun readSingleLineEntriesTo(
|
||||
platform: File,
|
||||
type: SingleLineValueEntryType,
|
||||
map: ConcurrentHashMap<SingleLineValueEntryType, List<String>>
|
||||
) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
+86
-2
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+24
-8
@@ -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<Triple<String, ResourceTable, ResourceGroup>>,
|
||||
result: MutableList<CompletionItem>
|
||||
) {
|
||||
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(
|
||||
|
||||
-10
@@ -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)]
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+1
-8
@@ -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<ResourceTable> {
|
||||
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<Styleable> {
|
||||
val name = node.nodeName
|
||||
|
||||
+140
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Triple<String, ResourceTable, ResourceGroup>>,
|
||||
result: MutableList<CompletionItem>
|
||||
) {
|
||||
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<CompletionItem>) {
|
||||
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<CompletionItem>) {
|
||||
val module = getModule()
|
||||
if (module is AndroidModule) {
|
||||
addMatches(prefix, module.getServiceActions(), result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeActivityActions(prefix: String, result: MutableList<CompletionItem>) {
|
||||
val module = getModule()
|
||||
if (module is AndroidModule) {
|
||||
addMatches(prefix, module.getActivityActions(), result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeReceiverActions(prefix: String, result: MutableList<CompletionItem>) {
|
||||
val module = getModule()
|
||||
if (module is AndroidModule) {
|
||||
addMatches(prefix, module.getBroadcastActions(), result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeCategory(prefix: String, result: MutableList<CompletionItem>) {
|
||||
val mod = getModule()
|
||||
if (mod is AndroidModule) {
|
||||
addMatches(prefix, mod.getCategories(), result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeFeature(prefix: String, result: MutableList<CompletionItem>) {
|
||||
val mod = getModule()
|
||||
if (mod is AndroidModule) {
|
||||
addMatches(prefix, mod.getFeatures(), result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMatches(
|
||||
prefix: String,
|
||||
entries: List<String>,
|
||||
result: MutableList<CompletionItem>
|
||||
) {
|
||||
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<ResourceTable> {
|
||||
return manifestResourceTable()
|
||||
}
|
||||
}
|
||||
+9
@@ -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<ResourceTable> {
|
||||
return setOf(
|
||||
Lookup.DEFAULT.lookup(ResourceTableRegistry.COMPLETION_MANIFEST_ATTR_RES) ?: return emptySet()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms entry name to tag name.
|
||||
*
|
||||
|
||||
+33
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)]
|
||||
}
|
||||
+158
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String>) {
|
||||
openFile("completion/$file")
|
||||
val (incomplete, items) = complete()
|
||||
assertThat(incomplete).isFalse()
|
||||
assertThat(items).containsAtLeastElementsIn(expect)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
+21
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.itsaky.test.app">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="M@@cursor@@" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.itsaky.test.app">
|
||||
|
||||
<application
|
||||
android:appCategory="@@cursor@@"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.itsaky.test.app">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="A@@cursor@@" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.itsaky.test.app">
|
||||
|
||||
<uses-feature android:name="f@@cursor@@"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.itsaky.test.app">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<receiver
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="M@@cursor@@" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.itsaky.test.app">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<service
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="M@@cursor@@" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
Reference in New Issue
Block a user