Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions projects/sdk/core/loader/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ dependencies {
compileOnly project(':common')
api project(':load-parameters')
compileOnly files(AndroidJar.ANDROID_JAR_PATH)
testImplementation "org.mockito:mockito-inline:5.2.0"
testImplementation files(AndroidJar.ANDROID_JAR_PATH)
}

def generateCode = tasks.register('generateCode') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,42 @@ class PluginContentProviderManager() : UriConverter.UriParseDelegate {

fun convert2PluginUri(uri: Uri): Uri {
val containerAuthority: String? = uri.authority
if (!providerAuthorityMap.values.contains(containerAuthority)) {
val matchAuthorityMap = providerAuthorityMap.filter { it.value == containerAuthority }
if (matchAuthorityMap.isEmpty()) {
throw IllegalArgumentException("不能识别的uri Authority:$containerAuthority")
}
val uriString = uri.toString()
for (entry in matchAuthorityMap) {
val pluginAuthority = entry.key
// 通过正则表达式去除 containerAuthority ,支持以下场景:
// 1. content://containerAuthority/pluginAuthority(插件内部调用 insert 、query 等方法)
// 2. content://containerAuthority/containerAuthority/pluginAuthority(插件内部调用 call 方法)
// 3. content://containerAuthority (外部应用调用 content provider 方法且 containerAuthority == pluginAuthority)
// 正则表达式结构分解:
// - ^content://:
// - 作用:强制从字符串的最开始进行匹配。
// - 目的:确保只处理标准的 content 协议 URI。
// - ((?:$escapedContainer/)*)(捕获组 1):
// - $escapedContainer/:这是经过 Regex.escape() 处理后的容器 Authority 字符串,后面紧跟一个斜杠。转义确保了如 a.b 中的点号不会匹配任意字符。
// - (?:...):非捕获组,仅用于将“容器名+斜杠”作为一个整体进行多次匹配。
// - *(贪婪匹配):匹配零个或多个连续的容器前缀。使用贪婪模式是为了在 containerAuthority 和 pluginAuthority 相同的情况下(如content://A/A/path),尽可能多地剥离外层容器,只留下最后一个作为插件标识。
// - $escapedPlugin:
// - 作用:匹配插件真实的 Authority 。它是整个正则的锚点,用于确定这个 URI 属于哪个插件。
// - (?=/|$)(正向肯定预查):
// - 作用:这是一个非占位匹配,要求匹配到的 pluginAuthority 后面必须紧跟一个斜杠 /(表示路径开始)或者字符串结束符 $ 。
// - 目的:防止部分匹配。例如,如果 pluginAuthority 是 A,而 URI 是 content://Ab/path,如果没有这个预查,正则会错误地匹配到 Ab 。
val escapedContainer = Regex.escape(containerAuthority!!)
val escapedPlugin = Regex.escape(pluginAuthority)
val regex = Regex("^$CONTENT_PREFIX((?:$escapedContainer/)*)$escapedPlugin(?=/|$)")

// 可能存在一个 containerAuthority 匹配多个 pluginAuthority 的场景,所以存在无法匹配的场景
val matchResult = regex.find(uriString) ?: continue
// 如果找到了匹配的内容,则剔除匹配的 containerAuthority 内容
val range = matchResult.groups[1]!!.range
return Uri.parse(
uriString.substring(0, range.first) + uriString.substring(range.last + 1)
)
}
return Uri.parse(uriString.replace("$containerAuthority/", ""))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Tencent is pleased to support the open source community by making Tencent Shadow available.
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package com.tencent.shadow.core.loader.managers

import android.net.Uri
import com.tencent.shadow.core.loader.infos.ContainerProviderInfo
import com.tencent.shadow.core.runtime.PluginManifest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mockito.mock
import org.mockito.Mockito.mockStatic
import org.mockito.Mockito.`when`

@RunWith(Enclosed::class)
class PluginContentProviderManagerTest {

@RunWith(Parameterized::class)
class Convert2PluginUriTest(
private val containerAuthority: String,
private val pluginAuthority: String,
private val input: String,
private val expected: String
) {
private lateinit var manager: PluginContentProviderManager

companion object {
@JvmStatic
@Parameters
fun data(): Collection<Array<String>> = listOf(
"com.container.auth" to "com.plugin.auth",
"com.container.auth" to "com.container.auth"
)
.flatMap { (containerAuthority, pluginAuthority) ->
val same = containerAuthority == pluginAuthority
listOf(
"content://$containerAuthority" to "content://$containerAuthority",
"content://$containerAuthority/" to if (same) "content://$pluginAuthority/" else "content://",
"content://$containerAuthority/path" to if (same) "content://$pluginAuthority/path" else "content://path",
"content://$containerAuthority/$pluginAuthority" to "content://$pluginAuthority",
"content://$containerAuthority/$pluginAuthority/" to "content://$pluginAuthority/",
"content://$containerAuthority/$pluginAuthority/path" to "content://$pluginAuthority/path",
"content://$containerAuthority/$containerAuthority/$pluginAuthority" to "content://$pluginAuthority",
"content://$containerAuthority/$containerAuthority/$pluginAuthority/" to "content://$pluginAuthority/",
"content://$containerAuthority/$containerAuthority/$pluginAuthority/path" to "content://$pluginAuthority/path",
"content://$containerAuthority/$pluginAuthority/$containerAuthority" to if (same) "content://$pluginAuthority" else "content://$pluginAuthority/$containerAuthority",
"content://$containerAuthority/$pluginAuthority/$containerAuthority/" to if (same) "content://$pluginAuthority/" else "content://$pluginAuthority/$containerAuthority/",
"content://$containerAuthority/$pluginAuthority/$containerAuthority/path" to if (same) "content://$pluginAuthority/path" else "content://$pluginAuthority/$containerAuthority/path",
"content://$containerAuthority/$pluginAuthority/$containerAuthority/$pluginAuthority" to if (same) "content://$pluginAuthority" else "content://$pluginAuthority/$containerAuthority/$pluginAuthority",
"content://$containerAuthority/$pluginAuthority/$containerAuthority/$pluginAuthority/" to if (same) "content://$pluginAuthority/" else "content://$pluginAuthority/$containerAuthority/$pluginAuthority/",
"content://$containerAuthority/$pluginAuthority/$containerAuthority/$pluginAuthority/path" to if (same) "content://$pluginAuthority/path" else "content://$pluginAuthority/$containerAuthority/$pluginAuthority/path",
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority" to "content://$pluginAuthority/path/$containerAuthority",
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority/" to "content://$pluginAuthority/path/$containerAuthority/",
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority/file" to "content://$pluginAuthority/path/$containerAuthority/file",
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority" to "content://$pluginAuthority/path/$pluginAuthority",
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/" to "content://$pluginAuthority/path/$pluginAuthority/",
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/file" to "content://$pluginAuthority/path/$pluginAuthority/file",
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority/$pluginAuthority" to "content://$pluginAuthority/path/$containerAuthority/$pluginAuthority",
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority/$pluginAuthority/" to "content://$pluginAuthority/path/$containerAuthority/$pluginAuthority/",
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority/$pluginAuthority/file" to "content://$pluginAuthority/path/$containerAuthority/$pluginAuthority/file",
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/$containerAuthority" to "content://$pluginAuthority/path/$pluginAuthority/$containerAuthority",
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/$containerAuthority/" to "content://$pluginAuthority/path/$pluginAuthority/$containerAuthority/",
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/$containerAuthority/file" to "content://$pluginAuthority/path/$pluginAuthority/$containerAuthority/file"
)
.map { arrayOf(containerAuthority, pluginAuthority, it.first, it.second) }
}
}

@Before
fun init() {
manager = PluginContentProviderManager().apply {
addContentProviderInfo(
"partKey",
PluginManifest.ProviderInfo("pluginClassName", pluginAuthority, true),
ContainerProviderInfo("containerClassName", containerAuthority),
pluginAuthority
)
}
}

@Test
fun testConvert2PluginUri() {
mockStatic(Uri::class.java).use {
it.`when`<Uri> { Uri.parse(anyString()) }
.thenAnswer { invocation -> mockUri(invocation.getArgument(0)) }

Assert.assertEquals(expected, manager.convert2PluginUri(mockUri(input)).toString())
}
}
}
}

private fun mockUri(input: String): Uri {
val uri = mock(Uri::class.java)
`when`(uri.toString()).thenReturn(input)

val startIndex = "content://".length
val indexOf = input.indexOf('/', startIndex)
val endIndex = if (indexOf < 0) input.length else indexOf
val authority = input.substring(startIndex, endIndex)
`when`(uri.authority).thenReturn(authority)

return uri
}
Loading