/Shadow.git new_branch_name
git checkout -b new_branch_name FETCH_HEAD
```
# Pull Request
由于PR会修改代码,因此即便是在开源初期,我们也会对PR谨慎处理。
请注意以下问题:
1. 不要提交无意义改动。
1. 除非是提交复现问题的测试用例,请确保`gradlew testSdk`构建成功(需要连接Android设备)
1. 测试机需要至少有API 28,API 19两种机器,以保证ART和Dalvik虚拟机都能正常工作。
1. 尽量原子化的提交,配有较为清晰的提交信息。
我们会根据大家的PR再调整PR的要求的。
# 开发指引
## Debug编译期代码(Gradle插件、Transform等)
Shadow的`coding`和`core.gradle-plugin`、`core.manifest-parser`、`core.transform`,`core.transform-kit`
等模块都是在插件工程的编译期执行的。如果需要Debug它们,需要额外的配置。
1. 添加`Remote JVM Debug` Configuration。在Android Studio的`Run`菜单中找到`Edit Configuration`。 点"+"(Add New
Configuration),选择`Remote JVM Debug`,配置参数一般采用默认值不用修改。
`Name`可以任意修改成方便识别的名字,稍后在工具栏执行时选择。 复制`Command line arguments for remote JVM`。
一般的默认值是:`-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005`。
将这行复制到`gradle.properties`中的`org.gradle.jvmargs=-Xmx4096m`后面作为更多的参数,注意加上空格。 然后将其中的`suspend=n`
改为`suspend=y`,表示让JVM启动Gradle时等待Debugger连接,再继续执行。
2. 终止正在运行的Gradle Daemon。在命令行执行`./gradlew --stop`,终止掉没有采用新参数的JVM进程。
3. Debug编译期代码。通过`./gradlew`或者Android Studio的Gradle sync,或运行`sample-host`等任务, 都会在一启动时因为前面的`suspend=y`
卡住。这时再选择刚刚添加的`Remote JVM Debug` Configuration, 点击`Debug`执行按钮,即可连接上Gradle JVM。如果在`ShadowPlugin`
或者某个Transform代码中设置了断点, 就会正常在断点处暂停。 注意选择`Remote JVM Debug` Configuration的位置同选择`sample-host`
等模块在同一个菜单中。 并且Android Studio可以同时执行多个Configuration,先运行`sample-host`, 再Debug `Remote JVM Debug`
Configuration是没有问题的。
4. 在其他不是Shadow源码工程中,也可以同样设置`gradle.properties`参数,在其中执行Gradle任务。
然后切换到Shadow源码工程中执行Debug `Remote JVM Debug` Configuration, 也可以Debug Shadow在其他工程中的编译期代码执行情况。
5. 还原。回退对`gradle.properties`的修改,然后执行`./gradlew --stop`。以上所有改动的作用即可恢复。
## 虚拟机
启动虚拟机
```shell
~/Library/Android/sdk/emulator/emulator @Pixel_XL_API_28 -noaudio -no-boot-anim -wipe-data -no-snapstorage
```
其中`Pixel_XL_API_28`来自:
```shell
~/Library/Android/sdk/emulator/emulator -list-avds
```
`-noaudio`可以避免耳机切换到通话模式。
`-wipe-data -no-snapstorage`使得虚拟机完全恢复到新建状态。
`-no-boot-anim`稍微加快点启动。
## 启动指定自动化测试用例
随着Android Studio更新,在#1263 解决之前,只能通过命令行执行自动化测试。
如下命令,传入类名#方法可以指定单个方法。只传入类名可以测试整个类。
如果测试方法是抽象类中的,需要传入一个具体的实现类。
```shell
./gradlew :test-dynamic-host:connectedTarget28DebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.tencent.shadow.test.cases.plugin_main.ApplicationContextSubDirTest#testGetDatabasePath
```
## 清理工作区
由于复合构建的存在,Gradle clean任务不能总是很好的完成清理工作区的目的。
```shell
git clean -fdx -e .idea -e local.properties
```
================================================
FILE: LICENSE.txt
================================================
Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of THL A29 Limited nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: PRIVACY.md
================================================
# Tencent Shadow SDK个人信息保护规则
_生效日期:2022年4月6日_
## 引言
Tencent Shadow SDK (以下简称“SDK产品”)由深圳市腾讯计算机系统有限公司(以下简称“我们”)开发, 公司注册地为深圳市南山区粤海街道麻岭社区科技中一路腾讯大厦35层。
**我们在此特别声明:**
**1. 本SDK产品功能的实现将不需要收集、获取、传输、分享或者使用终端用户的任何个人信息。**
**2. 我们不会因为开发者适配、集成和装载本SDK产品而向其提供、传输或共享任何的个人信息。**
**3. 如果开发者因提供其产品或服务而需要处理终端用户的个人信息, 由开发者独自承担相应的法律责任。**
**4. 请终端用户注意, 在开发者将本SDK产品适配、集成或装载到开发者产品或服务前, 我们已经要求相关开发者仔细阅读我们在官网公示的相关服务协议、本规则及开发者合规指南(或具有同样性质的相关法律文件), 并已经要求开发者依据开发者的产品收集使用个人信息的情况进行合规自查。**
**5. 如果我们更新、改进或修改了本SDK产品, 并因此导致我们需要处理终端用户的个人信息的, 我们将会依据适用法律的要求对本规则进行修订, 并将修订后的内容及时告知开发者和终端用户, 我们将要求开发者适时更新其隐私政策,并以弹框形式通知终端用户并且获得其同意。**
**6. 对于本规则的任何内容存在疑问的, 可以通过如下的方式与我们取得联系:**
(1) 通过 https://kf.qq.com/ 与我们联系进行在线咨询;
(2) 发送邮件至 Dataprivacy@tencent.com ;
(3) 邮寄信件至:中国广东省深圳市南山区海天二路33号腾讯滨海大厦 数据隐私保护部(收)邮编:518054。
================================================
FILE: README.md
================================================
# Shadow

[](http://makeapullrequest.com)
## 介绍
Shadow是一个腾讯自主研发的Android插件框架,经过线上亿级用户量检验。
Shadow不仅开源分享了插件技术的关键代码,还完整的分享了上线部署所需要的所有设计。
与市面上其他插件框架相比,Shadow主要具有以下特点:
* **复用独立安装App的源码**:插件App的源码原本就是可以正常安装运行的。
* **零反射无Hack实现插件技术**:从理论上就已经确定无需对任何系统做兼容开发,更无任何隐藏API调用,和Google限制非公开SDK接口访问的策略完全不冲突。
* **全动态插件框架**:一次性实现完美的插件框架很难,但Shadow将这些实现全部动态化起来,使插件框架的代码成为了插件的一部分。插件的迭代不再受宿主打包了旧版本插件框架所限制。
* **宿主增量极小**:得益于全动态实现,真正合入宿主程序的代码量极小(15KB,160方法数左右)。
* **Kotlin实现**:core.loader,core.transform核心代码完全用Kotlin实现,代码简洁易维护。
### 支持特性
* 四大组件
* Fragment(代码添加和Xml添加)
* DataBinding(无需特别支持,但已验证可正常工作)
* 跨进程使用插件Service
* 自定义Theme
* 插件访问宿主类
* So加载
* 分段加载插件(多Apk分别加载或多Apk以此依赖加载)
* 一个Activity中加载多个Apk中的View
* 等等……
## 编译与开发环境
### 环境准备
建议直接用最新的稳定版本Android Studio打开工程。目前项目已适配`Android Studio Arctic Fox | 2020.3.1`,
低版本的Android Studio可能因为Gradle版本过高而无法正常打开项目。
然后在IDE中选择`sample-app`或`sample-host`模块直接运行,分别体验同一份代码在正常安装情况下和插件情况下的运行情况。

Shadow的所有代码都位于`projects`目录下的3个目录,分别是:
* `sdk`包含SDK的所有代码
* `test`包含SDK的自动化测试代码
* `sample`包含演示代码
其中`sample`应该是大家体验Shadow的最佳环境。
详见`sample`目录中的[README](projects/sample/README.md)介绍。
### 兼容性
Shadow项目有较为完善的自动化测试,因此最新代码对外部环境的版本兼容性可以参考自动化测试的配置。
* [pr-check.yml](.github/workflows/pr-check.yml) 虚拟机自动化测试,包含Android测试机版本和编译环境JDK等版本。
* [pr-check-gradle-plugin.yml](.github/workflows/pr-check-gradle-plugin.yml) AGP兼容性测试。
其中指向的[test_JDK17.sh](projects/test/gradle-plugin-agp-compat-test/test_JDK17.sh)和
[test_JDK11.sh](projects/test/gradle-plugin-agp-compat-test/test_JDK11.sh)中定义了被测试的AGP版本。
## 自己写的测试代码出错?
以我们多年的插件环境下业务开发经验,插件框架是不可能一步到位实现完美的。
因此,我们相信大部分业务在接入时都是需要一定的二次开发工作。
Shadow现有的代码满足的是我们自己的业务现在的需求。得益于全动态的设计,
插件框架和插件本身都是动态发布的,插件包里既有插件代码也有插件框架代码,
所以可以根据新版本插件的需要同时开发插件框架。
例如,ShadowActivity没有实现全所有Activity方法,你写的测试代码可能用到了,
就会出现Method Not Found错误,只需要在ShadowActivity中实现对应方法就可以了。
大部分方法的实现都只是需要简单的转调就能工作正常。
如果遇到不会实现的功能,可以提Issue。最好附上测试代码。
## 后续开发
* 原理与设计说明文档
* 多插件支持的演示工程
* 自动化测试用例补充
* 开源包含下载能力的manager实现
## 贡献代码
详见[CONTRIBUTING.md](CONTRIBUTING.md)
## 许可协议
Tencent Shadow采用`BSD 3-Clause License`,详见[LICENSE](LICENSE.txt)。
## 个人信息保护规则声明
详见[PRIVACY.md](PRIVACY.md)
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
//buildscript不能从其他gradle文件中apply,所以这段buildscript脚本存在于多个子构建中。
//请更新buildscript时同步更新。
buildscript {
loadVersions:
{// 读取versions.properties到ext中,供项目中直接用变量引用版本号
def versions_properties_path = 'buildScripts/gradle/versions.properties'
def versions = new Properties()
versions.load(file(versions_properties_path).newReader())
versions.forEach { key, stringValue ->
def value = stringValue?.isInteger() ? stringValue as Integer : stringValue
ext.set(key, value)
}
}
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath "com.android.tools.build:gradle:$build_gradle_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.tencent.shadow.coding:aar-to-jar-plugin'
classpath 'com.tencent.shadow.coding:common-jar-settings'
}
}
apply from: 'buildScripts/gradle/common.gradle'
apply from: "buildScripts/gradle/maven.gradle"
apply from: "buildScripts/gradle/fix_issue_1263.gradle"
================================================
FILE: buildScripts/gradle/common.gradle
================================================
def gitShortRev() {
def gitCommit = ""
def proc = "git rev-parse --short HEAD".execute()
proc.in.eachLine { line -> gitCommit = line }
proc.err.eachLine { line -> println line }
proc.waitFor()
return gitCommit
}
allprojects {
def versionName, versionSuffix
if ("${System.env.CI}".equalsIgnoreCase("true")) {
versionName = System.getenv("GITHUB_REF_SLUG")
} else {
versionName = project.VERSION_NAME
}
if ("${System.env.PUBLISH_RELEASE}".equalsIgnoreCase("true")) {
versionSuffix = ""
} else if ("${System.env.CI}".equalsIgnoreCase("true")) {
versionSuffix = "-${System.env.GITHUB_SHA_SHORT}-SNAPSHOT"
} else {
versionSuffix = "-${gitShortRev()}-SNAPSHOT"
}
ext.ARTIFACT_VERSION = versionName + versionSuffix
ext.TEST_HOST_APP_APPLICATION_ID = 'com.tencent.shadow.test.hostapp'
ext.SAMPLE_HOST_APP_APPLICATION_ID = 'com.tencent.shadow.sample.host'
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
}
================================================
FILE: buildScripts/gradle/fix_issue_1263.gradle
================================================
/**
* 这个脚本通过hook create*ApkListingFileRedirect任务,
* 在它执行完成后,即生成了apk_ide_redirect_file,也应该生成了apk之后,
* 补充一个复制build/intermediates/apk到build/outputs/apk的操作。
*
* 采用这个修复方式是因为Shadow的打包代码设计不是很合理,难以通过少量改动,
* 保证引用项目不引入任何兼容性问题。
*
* 详见issue #1263
*/
buildscript {
dependencies {
classpath files(rootProject.buildscript.configurations.classpath)
}
}
def taskList = [
":sample-loader:createDebugApkListingFileRedirect",
":sample-loader:createReleaseApkListingFileRedirect",
":sample-runtime:createDebugApkListingFileRedirect",
":sample-runtime:createReleaseApkListingFileRedirect",
":sample-manager:createDebugApkListingFileRedirect",
":sample-manager:createReleaseApkListingFileRedirect",
":sample-app:createPluginDebugApkListingFileRedirect",
":sample-app:createPluginReleaseApkListingFileRedirect",
":sample-base:createPluginDebugApkListingFileRedirect",
":sample-base:createPluginReleaseApkListingFileRedirect",
":test-dynamic-loader:createDebugApkListingFileRedirect",
":test-dynamic-loader:createReleaseApkListingFileRedirect",
":test-dynamic-runtime:createDebugApkListingFileRedirect",
":test-dynamic-runtime:createReleaseApkListingFileRedirect",
":test-dynamic-manager:createDebugApkListingFileRedirect",
":test-dynamic-manager:createReleaseApkListingFileRedirect",
":plugin-service-for-host:createPluginDebugApkListingFileRedirect",
":plugin-service-for-host:createPluginReleaseApkListingFileRedirect",
":test-plugin-androidx-cases:createPluginDebugApkListingFileRedirect",
":test-plugin-androidx-cases:createPluginReleaseApkListingFileRedirect",
":test-plugin-general-cases:createPluginDebugApkListingFileRedirect",
":test-plugin-general-cases:createPluginReleaseApkListingFileRedirect",
":sample-hello-apk:createDebugApkListingFileRedirect",
":sample-hello-apk:createReleaseApkListingFileRedirect",
]
afterEvaluate {
taskList.forEach {
def t = tasks.findByPath(it)
copyApkAfterTask(t)
}
}
def copyApkAfterTask(t) {
t.doLast {
def redirectFile = t.getOutputs().getFiles().singleFile
def listingFile = redirectFile.readLines().get(1).replaceFirst("listingFile=", "")
def metadataFile = new File(redirectFile.parentFile, listingFile)
def metadata = new org.json.JSONObject(metadataFile.text)
def outputFile = metadata.getJSONArray("elements").getJSONObject(0).getString("outputFile")
def apkFile = new File(metadataFile.parentFile, outputFile)
def testRelativePath = redirectFile.relativePath(apkFile)
def needCopy = !testRelativePath.matches("^(\\.\\.${File.separatorChar})+outputs${File.separatorChar}.+")
if (needCopy) {
def matchPath = new File("/build/intermediates").toPath().toString()
def intermediatesDir = new File(apkFile.toPath().normalize().toString().find("^.+?$matchPath"))
def outputsDir = new File(intermediatesDir.parentFile, "outputs")
def r = copy {
from intermediatesDir
into outputsDir
include 'apk/**'
}
if (r.didWork) {
getLogger().info("copy apk from ${intermediatesDir.path} to ${outputsDir.path}")
}
}
}
}
================================================
FILE: buildScripts/gradle/maven.gradle
================================================
task buildSdk() {
dependsOn gradle.includedBuild('core').task(':gradle-plugin:assemble')
dependsOn gradle.includedBuild('core').task(':manifest-parser:assemble')
dependsOn gradle.includedBuild('core').task(':common:assemble')
dependsOn gradle.includedBuild('core').task(':loader:assemble')
dependsOn gradle.includedBuild('core').task(':manager:assemble')
dependsOn gradle.includedBuild('core').task(':runtime:assemble')
dependsOn gradle.includedBuild('core').task(':activity-container:assemble')
dependsOn gradle.includedBuild('core').task(':transform-kit:assemble')
dependsOn gradle.includedBuild('core').task(':transform-kit:testJar')
dependsOn gradle.includedBuild('core').task(':transform:assemble')
dependsOn gradle.includedBuild('core').task(':load-parameters:assemble')
dependsOn gradle.includedBuild('core').task(':utils:assemble')
dependsOn gradle.includedBuild('dynamic').task(':dynamic-apk:assemble')
dependsOn gradle.includedBuild('dynamic').task(':dynamic-host:assemble')
dependsOn gradle.includedBuild('dynamic').task(':dynamic-loader:assemble')
dependsOn gradle.includedBuild('dynamic').task(':dynamic-loader-impl:assemble')
dependsOn gradle.includedBuild('dynamic').task(':dynamic-manager:assemble')
dependsOn gradle.includedBuild('dynamic').task(':dynamic-host-multi-loader-ext:assemble')
dependsOn gradle.includedBuild('dynamic').task(':dynamic-manager-multi-loader-ext:assemble')
}
task jvmTestSdk() {
dependsOn gradle.includedBuild('coding').task(':test')
dependsOn gradle.includedBuild('core').task(':test')
dependsOn gradle.includedBuild('dynamic').task(':test')
dependsOn ':common-jar-settings-test:testJavacBootclasspath'
}
task androidTestSdk() {
dependsOn gradle.includedBuild('core').task(':manager-db-test:connectedDebugAndroidTest')
dependsOn ':test-dynamic-host:connectedTarget28DebugAndroidTest'
}
task testSdk() {
dependsOn jvmTestSdk
dependsOn androidTestSdk
}
apply plugin: 'maven-publish'
static def getDependencyNode(scope, groupId, artifactId, version) {
Node node = new Node(null, 'dependency')
node.appendNode('groupId', groupId)
node.appendNode('artifactId', artifactId)
node.appendNode('version', version)
node.appendNode('scope', scope)
return node
}
def gitShortRev() {
def gitCommit = ""
def proc = "git rev-parse --short HEAD".execute()
proc.in.eachLine { line -> gitCommit = line }
proc.err.eachLine { line -> println line }
proc.waitFor()
return gitCommit
}
def setScm(scm) {
scm.appendNode('connection', "https://github.com/${System.getenv("GITHUB_ACTOR")}/Shadow.git")
def commit
if ("${System.env.CI}".equalsIgnoreCase("true")) {
commit = System.getenv("GITHUB_SHA")
} else {
commit = gitShortRev()
}
scm.appendNode('url', "https://github.com/${System.getenv("GITHUB_ACTOR")}/Shadow/commit/$commit")
}
def setGeneratePomFileAndDepends(publicationName) {
model {
tasks."generatePomFileFor${publicationName.capitalize()}Publication" {
destination = file("$buildDir/pom/$publicationName-pom.xml")
dependsOn(buildSdk)
}
}
}
def sourceJar(String name, String path) {
return tasks.create("source${name.capitalize()}Jar", Jar) {
group = "publishing"
description = "package ${name} source to jar"
from "$path/src/main/java"
from "$path/src/main/kotlin"
destinationDir = file("$path/build/libs/")
classifier = 'sources'
}
}
def publicationVersion = project.ARTIFACT_VERSION
def coreGroupId = 'com.tencent.shadow.core'
def corePath = 'projects/sdk/core'
def dynamicGroupId = 'com.tencent.shadow.dynamic'
def dynamicPath = 'projects/sdk/dynamic'
task getPublicationVersion() {
doLast {
println "publicationVersion:$publicationVersion"
}
}
publishing {
publications {
gradlePlugin(MavenPublication) {
groupId coreGroupId
artifactId 'gradle-plugin'
version publicationVersion
artifact("$corePath/gradle-plugin/build/libs/gradle-plugin.jar")
artifact sourceJar("gradlePlugin", "$corePath/gradle-plugin")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', 'org.jetbrains.kotlin', 'kotlin-stdlib-jdk7', kotlin_version))
dependencies.append(getDependencyNode('compile', 'com.googlecode.json-simple', 'json-simple', json_simple_version))
dependencies.append(getDependencyNode('compile', coreGroupId, 'transform-kit', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'transform', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'runtime', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'activity-container', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'manifest-parser', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
// Plugin Marker Artifacts
// https://docs.gradle.org/current/userguide/plugins.html#sec:plugin_markers
pluginMarker(MavenPublication) {
def pluginId = 'com.tencent.shadow.plugin'
groupId pluginId
artifactId pluginId + '.gradle.plugin'
version publicationVersion
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', coreGroupId, 'gradle-plugin', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
manifestParser(MavenPublication) {
groupId coreGroupId
artifactId 'manifest-parser'
version publicationVersion
artifact("$corePath/manifest-parser/build/libs/manifest-parser.jar")
artifact sourceJar("manifestParser", "$corePath/manifest-parser")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', 'org.jetbrains.kotlin', 'kotlin-stdlib-jdk7', kotlin_version))
dependencies.append(getDependencyNode('compile', 'com.squareup', 'javapoet', javapoet_version))
dependencies.append(getDependencyNode('compile', coreGroupId, 'runtime', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
common(MavenPublication) {
groupId coreGroupId
artifactId 'common'
version publicationVersion
artifact("$corePath/common/build/libs/common.jar")
artifact sourceJar("common", "$corePath/common")
pom.withXml {
def root = asNode()
def scm = root.appendNode('scm')
setScm(scm)
}
}
loadParameters(MavenPublication) {
groupId coreGroupId
artifactId 'load-parameters'
version publicationVersion
artifact("$corePath/load-parameters/build/libs/load-parameters.jar")
artifact sourceJar("loadParameters", "$corePath/load-parameters")
pom.withXml {
def root = asNode()
def scm = root.appendNode('scm')
setScm(scm)
}
}
coreLoader(MavenPublication) {
groupId coreGroupId
artifactId 'loader'
version publicationVersion
artifact("$corePath/loader/build/libs/loader.jar")
artifact sourceJar("loader", "$corePath/loader")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', 'org.jetbrains.kotlin', 'kotlin-stdlib-jdk7', kotlin_version))
dependencies.append(getDependencyNode('compile', coreGroupId, 'runtime', publicationVersion))
dependencies.append(getDependencyNode('provided', coreGroupId, 'activity-container', publicationVersion))
dependencies.append(getDependencyNode('provided', coreGroupId, 'common', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'load-parameters', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
coreManager(MavenPublication) {
groupId coreGroupId
artifactId 'manager'
version publicationVersion
artifact("$corePath/manager/build/libs/manager.jar")
artifact sourceJar("manager", "$corePath/manager")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('provided', coreGroupId, 'common', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'load-parameters', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'utils', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
runtime(MavenPublication) {
groupId coreGroupId
artifactId 'runtime'
version publicationVersion
artifact("$corePath/runtime/build/libs/runtime.jar")
artifact sourceJar("runtime", "$corePath/runtime")
pom.withXml {
def root = asNode()
def scm = root.appendNode('scm')
setScm(scm)
}
}
activityContainer(MavenPublication) {
groupId coreGroupId
artifactId 'activity-container'
version publicationVersion
artifact("$corePath/activity-container/build/libs/activity-container.jar")
artifact sourceJar("activity-container", "$corePath/activity-container")
pom.withXml {
def root = asNode()
def scm = root.appendNode('scm')
setScm(scm)
}
}
transformKit(MavenPublication) {
groupId coreGroupId
artifactId 'transform-kit'
version publicationVersion
artifact("$corePath/transform-kit/build/libs/transform-kit.jar")
artifact sourceJar("transformKit", "$corePath/transform-kit")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', 'org.jetbrains.kotlin', 'kotlin-stdlib-jdk7', kotlin_version))
dependencies.append(getDependencyNode('compile', 'org.javassist', 'javassist', javassist_version))
def scm = root.appendNode('scm')
setScm(scm)
}
}
transformKitTest(MavenPublication) {
groupId coreGroupId
artifactId 'transform-kit-test'
version publicationVersion
artifact("$corePath/transform-kit/build/libs/test-transform-kit.jar")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', 'junit', 'junit', junit_version))
def scm = root.appendNode('scm')
setScm(scm)
}
}
transform(MavenPublication) {
groupId coreGroupId
artifactId 'transform'
version publicationVersion
artifact("$corePath/transform/build/libs/transform.jar")
artifact sourceJar("transform", "$corePath/transform")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', coreGroupId, 'transform-kit', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
dynamicApk(MavenPublication) {
groupId dynamicGroupId
artifactId 'apk'
version publicationVersion
artifact("$dynamicPath/dynamic-apk/build/libs/dynamic-apk.jar")
artifact sourceJar("dynamicApk", "$dynamicPath/dynamic-apk")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', coreGroupId, 'common', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
dynamicHost(MavenPublication) {
groupId dynamicGroupId
artifactId 'host'
version publicationVersion
artifact("$dynamicPath/dynamic-host/build/libs/dynamic-host.jar")
artifact sourceJar("dynamicHost", "$dynamicPath/dynamic-host")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', dynamicGroupId, 'apk', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'common', publicationVersion))
dependencies.append(getDependencyNode('compile', coreGroupId, 'utils', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
dynamicHostMultiLoaderExt(MavenPublication) {
groupId dynamicGroupId
artifactId 'host-multi-loader-ext'
version publicationVersion
artifact("$dynamicPath/dynamic-host-multi-loader-ext/build/libs/dynamic-host-multi-loader-ext.jar")
artifact sourceJar("dynamicHostMultiLoaderExt", "$dynamicPath/dynamic-host-multi-loader-ext")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', coreGroupId, 'common', publicationVersion))
dependencies.append(getDependencyNode('compile', dynamicGroupId, 'host', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
dynamicLoader(MavenPublication) {
groupId dynamicGroupId
artifactId 'loader'
version publicationVersion
artifact("$dynamicPath/dynamic-loader/build/libs/dynamic-loader.jar")
artifact sourceJar("dynamicLoader", "$dynamicPath/dynamic-loader")
pom.withXml {
def root = asNode()
def scm = root.appendNode('scm')
setScm(scm)
}
}
dynamicLoaderImpl(MavenPublication) {
groupId dynamicGroupId
artifactId 'loader-impl'
version publicationVersion
artifact("$dynamicPath/dynamic-loader-impl/build/libs/dynamic-loader-impl.jar")
artifact sourceJar("dynamicLoaderImpl", "$dynamicPath/dynamic-loader-impl")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', 'org.jetbrains.kotlin', 'kotlin-stdlib-jdk7', kotlin_version))
dependencies.append(getDependencyNode('compile', coreGroupId, 'loader', publicationVersion))
dependencies.append(getDependencyNode('provided', coreGroupId, 'activity-container', publicationVersion))
dependencies.append(getDependencyNode('provided', coreGroupId, 'common', publicationVersion))
dependencies.append(getDependencyNode('provided', dynamicGroupId, 'host', publicationVersion))
dependencies.append(getDependencyNode('compile', dynamicGroupId, 'loader', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
dynamicManager(MavenPublication) {
groupId dynamicGroupId
artifactId 'manager'
version publicationVersion
artifact("$dynamicPath/dynamic-manager/build/libs/dynamic-manager.jar")
artifact sourceJar("dynamicManager", "$dynamicPath/dynamic-manager")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', coreGroupId, 'manager', publicationVersion))
dependencies.append(getDependencyNode('compile', dynamicGroupId, 'loader', publicationVersion))
dependencies.append(getDependencyNode('provided', coreGroupId, 'common', publicationVersion))
dependencies.append(getDependencyNode('provided', dynamicGroupId, 'host', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
dynamicManagerMultiLoaderExt(MavenPublication) {
groupId dynamicGroupId
artifactId 'manager-multi-loader-ext'
version publicationVersion
artifact("$dynamicPath/dynamic-manager-multi-loader-ext/build/libs/dynamic-manager-multi-loader-ext.jar")
artifact sourceJar("dynamicManagerMultiLoaderExt", "$dynamicPath/dynamic-manager-multi-loader-ext")
pom.withXml {
def root = asNode()
def dependencies = root.appendNode('dependencies')
dependencies.append(getDependencyNode('compile', coreGroupId, 'manager', publicationVersion))
dependencies.append(getDependencyNode('compile', dynamicGroupId, 'loader', publicationVersion))
dependencies.append(getDependencyNode('compile', dynamicGroupId, 'manager', publicationVersion))
dependencies.append(getDependencyNode('provided', coreGroupId, 'common', publicationVersion))
dependencies.append(getDependencyNode('provided', dynamicGroupId, 'host-multi-loader-ext', publicationVersion))
def scm = root.appendNode('scm')
setScm(scm)
}
}
coreUtils(MavenPublication) {
groupId coreGroupId
artifactId 'utils'
version publicationVersion
artifact("$corePath/utils/build/libs/utils.jar")
artifact sourceJar("utils", "$corePath/utils")
pom.withXml {
def root = asNode()
def scm = root.appendNode('scm')
setScm(scm)
}
}
}
repositories {
def useLocalCredential = false
Properties properties = new Properties()
def propertiesFile = project.rootProject.file('local.properties')
if (propertiesFile.exists()) {
properties.load(propertiesFile.newDataInputStream())
if ("${properties.getProperty('gpr.local')}".equalsIgnoreCase('true')) {
def user = properties.getProperty('gpr.user')
def key = properties.getProperty('gpr.key')
maven {
name = "GitHubPackages"
credentials {
username = user
password = key
}
url "https://maven.pkg.github.com/${user}/shadow"
}
useLocalCredential = true
}
}
if (!useLocalCredential && "${System.env.CI}".equalsIgnoreCase("true")) {
maven {
name = "GitHubPackages"
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
url "https://maven.pkg.github.com/" + "${System.env.GITHUB_REPOSITORY}".toLowerCase()
}
} else {
mavenLocal()
}
}
}
setGeneratePomFileAndDepends('gradlePlugin')
setGeneratePomFileAndDepends('manifestParser')
setGeneratePomFileAndDepends('common')
setGeneratePomFileAndDepends('loadParameters')
setGeneratePomFileAndDepends('coreLoader')
setGeneratePomFileAndDepends('coreManager')
setGeneratePomFileAndDepends('coreUtils')
setGeneratePomFileAndDepends('runtime')
setGeneratePomFileAndDepends('activityContainer')
setGeneratePomFileAndDepends('transformKit')
setGeneratePomFileAndDepends('transformKitTest')
setGeneratePomFileAndDepends('transform')
setGeneratePomFileAndDepends('dynamicApk')
setGeneratePomFileAndDepends('dynamicHost')
setGeneratePomFileAndDepends('dynamicHostMultiLoaderExt')
setGeneratePomFileAndDepends('dynamicLoader')
setGeneratePomFileAndDepends('dynamicLoaderImpl')
setGeneratePomFileAndDepends('dynamicManager')
setGeneratePomFileAndDepends('dynamicManagerMultiLoaderExt')
================================================
FILE: buildScripts/gradle/versions.properties
================================================
COMPILE_SDK_VERSION=33
MIN_SDK_VERSION=14
TARGET_SDK_VERSION=28
VERSION_CODE=1
VERSION_NAME=local
android_build_tools_version=30.0.3
android_support_annotations_version=28.0.0
android_support_version=27.1.1
androidx_activity_version=1.4.0
androidx_appcompat_version=1.4.1
androidx_test_junit_version=1.1.2
androidx_test_version=1.3.0
build_gradle_version=7.4.2
commons_io_android_version=2.5
commons_io_jvm_version=2.9.0
espresso_version=3.3.0
javapoet_version=1.11.1
javassist_version=3.28.0-GA
json_simple_version=1.1
junit_version=4.12
kotlin_version=1.5.31
slf4j_version=1.7.25
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionUrl=https\://mirrors.tencent.com/gradle/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx4096m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useAndroidX=true
org.gradle.caching=true
android.nonTransitiveRClass=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: projects/sample/README.md
================================================
# Sample
在Shadow框架下,应用由几部分构成。
宿主应用打包了很简单的一些接口,并在Manifest中注册了壳子代理组件,
还打包了插件管理器(manager)的动态升级逻辑。
manager负责下载、安装插件,还带有一个动态的View表达Loading态。
而"插件"则不光包含业务App,还包含Shadow的核心实现,即loader和runtime。
"插件"中的业务App和loader、runtime是同一个版本的代码编译出的,
因此loader可以包含一些业务逻辑,针对业务进行特殊处理。
由于loader是多实例的,因此同一个宿主中可以有多种不同的loader实现。
manager在加载"插件"时,首先需要先加载"插件"中的runtime和loader,
再通过loader的Binder(插件应该处于独立进程中避免native库冲突)操作loader进而加载业务App。
在这个Sample目录下,提供了两种示例工程:
## 源码依赖SDK的Sample(`projects/sample/source`)
***
要测试这个Sample请用Android Studio直接打开clone版本库的根目录。
***
* `sample-host`是宿主应用
* `sample-manager`是插件管理器的动态实现
* `sample-plugin/sample-loader`是loader的动态实现,业务主要在这里定义插件组件和壳子代理组件的配对关系等。
* `sample-constant`是在前3者中共用的相同字符串常量。
* `sample-plugin/sample-runtime`是runtime的动态实现,业务主要在这里定义壳子代理组件的实际类。
* `sample-plugin/sample-base-lib`是业务App的一部分基础代码,是一个aar库。
* `sample-plugin/sample-base`是一个apk模块壳子,将`sample-base-lib`打包在其中。既用于正常安装运行开发`sample-base-lib`
,又用于编译`sample-base`插件。
* `sample-plugin/sample-app`是依赖`sample-base-lib`开发的更多业务代码。它编译出的插件apk没有打包`sample-base-lib`
,会在插件运行时依赖`sample-base`插件。
`sample-app`和`sample-base`构成了一个多插件示例,请注意`sample-app/build.gradle`中的`dependsOn = ['sample-base']`设置。
这些工程中对Shadow SDK的依赖完全是源码级的依赖,因此修改Shadow SDK的源码后可以直接运行生效。
使用时可以直接在Android Studio中选择运行`sample-host`模块。
`sample-host`在构建中会自动打包manager和"插件"到assets中,在运行时自动释放模拟下载过程。
## 二进制Maven依赖SDK的Sample(`projects/sample/maven`)
***
要测试这个Sample请用Android Studio *分别* 打开`projects/sample/maven/host-project`
,`projects/sample/maven/manager-project`,`projects/sample/maven/plugin-project`三个目录。
***
源码依赖SDK的Sample中对Shadow SDK的依赖配置不适用于正式业务接入。
Shadow实现了完整的Maven发布脚本,支持方便的Maven依赖。
`maven`目录下的3个目录分别演示了3个工程。
这3个工程在实际业务中大概率上是3个不同的代码库。
因此,在这个演示中没有试图做着3个工程间的任何依赖关系,
甚至**3个工程中依赖的Shadow版本都是独立配置的**,
使用时请注意这一点。
### 自行发布SDK到Maven仓库方法
在`buildScripts/gradle/maven.gradle`文件中配置了Shadow的Maven发布脚本。
正式使用时,请修改其中的两个GroupID变量:`coreGroupId`、`dynamicGroupId`,
以及`setScm`方法中的两个URL到自己的版本库地址上。
然后将`mavenLocal()`改为自己发布的目标Maven仓库。
执行`./gradlew publish`即可将Shadow SDK发布到Maven仓库。
构件的版本号可以在`build/pom`目录中查看生成的pom文件中查看。
在这个Sample的3个工程的`build.gradle`文件中都有`shadow_version`定义,
将这个定义值改为刚刚发布的版本号(生成的pom中写的版本号)。
### 运行方法
这个演示工程没有实现下载功能,而是假设下载的文件直接位于指定路径。
因此运行前需要手工用adb命令将指定内容push到指定位置。
编译插件,在`plugin-project`目录中运行:
```
./gradlew packageDebugPlugin
adb push build/plugin-debug.zip /data/local/tmp
```
编译PluginManager,在`manager-project`目录中运行:
```
./gradlew assembleDebug
adb push sample-manager/build/outputs/apk/debug/sample-manager-debug.apk /data/local/tmp
```
最后可以用Android Studio打开`host-project`直接运行`sample-host`模块。
`plugin-project`中的`plugin-normal-apk`模块也可以直接安装运行,演示不使用Shadow时插件的运行情况。
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api/.gitignore
================================================
/build
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api/README.md
================================================
演示如何将自定义接口动态化,使得宿主能够使用apk中的实现
sample-hello-api:定义宿主api接口
sample-hello-api-holder:将 api 动态化,宿主通过这个包提供的方法来获取apk中的实现
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api/build.gradle
================================================
apply plugin: 'com.android.library'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api/src/main/java/com/tencent/shadow/sample/api/hello/HelloFactory.java
================================================
/*
* 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.sample.api.hello;
import android.content.Context;
/**
* @author 林学渊
* @email linxy59@mail2.sysu.edu.cn
* @date 2021/9/6
* @description 宿主只能通过工厂模式获取 apk 中的类
* @usage null
*/
public interface HelloFactory {
IHelloWorldImpl build(Context context);
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api/src/main/java/com/tencent/shadow/sample/api/hello/IHelloWorld.java
================================================
package com.tencent.shadow.sample.api.hello;
import android.content.Context;
import android.widget.TextView;
/**
* @author 林学渊
* @email linxy59@mail2.sysu.edu.cn
* @date 2021/9/6
* @description 定义在宿主里的接口,使用插件apk中的实现
* @usage 插件打印 hello world
*/
public interface IHelloWorld {
void sayHelloWorld(Context context, TextView textView);
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api/src/main/java/com/tencent/shadow/sample/api/hello/IHelloWorldImpl.java
================================================
/*
* 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.sample.api.hello;
import android.os.Bundle;
/**
* @author 林学渊
* @email linxy59@mail2.sysu.edu.cn
* @date 2021/9/6
* @description 给接口 IHelloWorld 包装一层生命周期
* 可参考 com.tencent.shadow.sample.apk.hello.DynamicHello 中管理该生命周期
* @usage hello.apk 里可以感知加载的过程
*/
public interface IHelloWorldImpl extends IHelloWorld {
void onCreate(Bundle bundle);
void onSaveInstanceState(Bundle outState);
void onDestroy();
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api-holder/.gitignore
================================================
/build
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api-holder/README.md
================================================
演示如何将自定义接口动态化,使得宿主能够使用apk中的实现
sample-hello-api:定义宿主api接口
sample-hello-api-holder:将 api 动态化,宿主通过这个包提供的方法来获取apk中的实现
宿主引入 apk 包,implementation project(':sample-hello-api-holder')
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api-holder/build.gradle
================================================
apply plugin: 'com.android.library'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
// 使用 api 而不是 compileOnly:发布 aar 时会传递依赖,而不是打包进 aar
api 'com.tencent.shadow.core:utils'
api 'com.tencent.shadow.core:common'
api 'com.tencent.shadow.dynamic:dynamic-apk'
api project(':sample-hello-api')
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api-holder/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api-holder/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api-holder/src/main/java/com/tencent/shadow/sample/apk/hello/DynamicHello.java
================================================
/*
* 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.sample.apk.hello;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.TextView;
import com.tencent.shadow.core.common.Logger;
import com.tencent.shadow.core.common.LoggerFactory;
import com.tencent.shadow.sample.api.hello.IHelloWorld;
import com.tencent.shadow.sample.api.hello.IHelloWorldImpl;
import java.io.File;
import static com.tencent.shadow.core.utils.Md5.md5File;
public final class DynamicHello implements IHelloWorld {
final private HelloWorldUpdater mUpdater;
private IHelloWorldImpl mHelloWorldImpl;
private String mCurrentImplMd5;
private static final Logger mLogger = LoggerFactory.getLogger(DynamicHello.class);
public DynamicHello(HelloWorldUpdater updater) {
if (updater.getLatest() == null) {
throw new IllegalArgumentException("构造DynamicPluginManager时传入的PluginManagerUpdater" +
"必须已经已有本地文件,即getLatest()!=null");
}
mUpdater = updater;
}
@Override
public void sayHelloWorld(Context context, TextView textView) {
if (mLogger.isInfoEnabled()) {
mLogger.info("sayHelloWorld context:" + context);
}
updateImpl(context);
mHelloWorldImpl.sayHelloWorld(context, textView);
mUpdater.update();
}
public void release() {
if (mLogger.isInfoEnabled()) {
mLogger.info("release");
}
if (mHelloWorldImpl != null) {
mHelloWorldImpl.onDestroy();
mHelloWorldImpl = null;
}
}
private void updateImpl(Context context) {
File latestImplApk = mUpdater.getLatest();
String md5 = md5File(latestImplApk);
if (mLogger.isInfoEnabled()) {
mLogger.info("TextUtils.equals(mCurrentImplMd5, md5) : " + (TextUtils.equals(mCurrentImplMd5, md5)));
}
if (!TextUtils.equals(mCurrentImplMd5, md5)) {
HelloImplLoader implLoader = new HelloImplLoader(context, latestImplApk);
IHelloWorldImpl newImpl = implLoader.load();
Bundle state;
if (mHelloWorldImpl != null) {
state = new Bundle();
mHelloWorldImpl.onSaveInstanceState(state);
mHelloWorldImpl.onDestroy();
} else {
state = null;
}
newImpl.onCreate(state);
mHelloWorldImpl = newImpl;
mCurrentImplMd5 = md5;
}
}
public IHelloWorld getHelloWorkdImpl() {
return mHelloWorldImpl;
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api-holder/src/main/java/com/tencent/shadow/sample/apk/hello/HelloImplLoader.java
================================================
/*
* 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.sample.apk.hello;
import android.content.Context;
import com.tencent.shadow.core.common.InstalledApk;
import com.tencent.shadow.dynamic.apk.ApkClassLoader;
import com.tencent.shadow.dynamic.apk.ChangeApkContextWrapper;
import com.tencent.shadow.dynamic.apk.ImplLoader;
import com.tencent.shadow.sample.api.hello.HelloFactory;
import com.tencent.shadow.sample.api.hello.IHelloWorldImpl;
import java.io.File;
final class HelloImplLoader extends ImplLoader {
//指定实现类在apk中的路径
private static final String FACTORY_CLASS_NAME = "com.tencent.shadow.dynamic.impl.HelloFactoryImpl";
private static final String[] REMOTE_PLUGIN_MANAGER_INTERFACES = new String[]
{
"com.tencent.shadow.core.common",
//注意将宿主自定义接口加入白名单
"com.tencent.shadow.sample.api.hello"
};
final private Context applicationContext;
final private InstalledApk installedApk;
HelloImplLoader(Context context, File apk) {
applicationContext = context.getApplicationContext();
File root = new File(applicationContext.getFilesDir(), "HelloImplLoader");
File odexDir = new File(root, Long.toString(apk.lastModified(), Character.MAX_RADIX));
odexDir.mkdirs();
installedApk = new InstalledApk(apk.getAbsolutePath(), odexDir.getAbsolutePath(), null);
}
IHelloWorldImpl load() {
ApkClassLoader apkClassLoader = new ApkClassLoader(
installedApk,
getClass().getClassLoader(),
loadWhiteList(installedApk),
1
);
Context contextForApi = new ChangeApkContextWrapper(
applicationContext,
installedApk.apkFilePath,
apkClassLoader
);
try {
HelloFactory factory = apkClassLoader.getInterface(
HelloFactory.class,
FACTORY_CLASS_NAME
);
return factory.build(contextForApi);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
protected String[] getCustomWhiteList() {
return REMOTE_PLUGIN_MANAGER_INTERFACES;
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-api-holder/src/main/java/com/tencent/shadow/sample/apk/hello/HelloWorldUpdater.java
================================================
/*
* 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.sample.apk.hello;
import java.io.File;
import java.util.concurrent.Future;
/**
* apk文件升级器
*
* 注意这个类不负责什么时候该升级 实现IHelloWorld的apk文件,
* 它只提供需要升级时的功能,如下载和向远端查询文件是否还可用。
*/
public interface HelloWorldUpdater {
/**
* 更新
*/
Future update();
/**
* 获取本地最新可用的
*
* @return null表示本地没有可用的
*/
File getLatest();
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-apk/.gitignore
================================================
/build
================================================
FILE: projects/sample/dynamic-apk/sample-hello-apk/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
//region hello.apk 演示
applicationId 'com.tencent.shadow.sample.hello.host' // 必须保证和 host 的 applicationId 一致
//endregion
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
lintOptions {
abortOnError false
}
}
dependencies {
// 自定义接口在宿主内
// hello.apk 不必要自己打包一份,只需要通过白名单访问宿主的接口定义,所以是 compileOnly
compileOnly project(":sample-hello-api")
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-apk/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class org.slf4j.**{*;}
-keep class com.tencent.shadow.dynamic.impl.**{*;}
-keep class com.tencent.shadow.dynamic.loader.**{*;}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-apk/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/dynamic-apk/sample-hello-apk/src/main/java/com/tencent/shadow/dynamic/impl/HelloFactoryImpl.java
================================================
package com.tencent.shadow.dynamic.impl;
import android.content.Context;
import com.tencent.shadow.sample.api.hello.HelloFactory;
import com.tencent.shadow.sample.api.hello.IHelloWorldImpl;
import com.tencent.shadow.sample.api.hello.SampleHelloWorld;
/**
* @author 林学渊
* @email linxy59@mail2.sysu.edu.cn
* @date 2021/9/6
* @description 此类包名及类名固定
* @usage null
*/
public final class HelloFactoryImpl implements HelloFactory {
@Override
public IHelloWorldImpl build(Context context) {
return new SampleHelloWorld();
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-apk/src/main/java/com/tencent/shadow/dynamic/impl/WhiteList.java
================================================
/*
* 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.dynamic.impl;
/**
* 此类包名及类名固定
* classLoader的白名单
* hello.apk 可以加载宿主中位于白名单内的类
*/
public interface WhiteList {
String[] sWhiteList = new String[]
{
"com.tencent.host.shadow",
"com.tencent.shadow.test.lib.constant",
};
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-apk/src/main/java/com/tencent/shadow/sample/api/hello/SampleHelloWorld.java
================================================
package com.tencent.shadow.sample.api.hello;
import android.content.Context;
import android.os.Bundle;
import android.widget.TextView;
/**
* @author 林学渊
* @email linxy59@mail2.sysu.edu.cn
* @date 2021/9/6
* @description 实现宿主自定义接口
* @usage null
*/
public class SampleHelloWorld implements IHelloWorldImpl {
@Override
public void sayHelloWorld(Context context, TextView textView) {
String text = "这是apk中的实现:" + SampleHelloWorld.class.toString();
if (textView == null) {
return;
}
textView.setText(text);
}
@Override
public void onCreate(Bundle bundle) {
}
@Override
public void onSaveInstanceState(Bundle outState) {
}
@Override
public void onDestroy() {
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-apk/src/main/res/layout/activity_load_plugin.xml
================================================
================================================
FILE: projects/sample/dynamic-apk/sample-hello-host/.gitignore
================================================
/build
================================================
FILE: projects/sample/dynamic-apk/sample-hello-host/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
//region hello.apk 演示
applicationId 'com.tencent.shadow.sample.hello.host'
//endregion
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
testInstrumentationRunner "com.tencent.shadow.test.CustomAndroidJUnitRunner"
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
//region hello.apk 演示
sourceSets {
debug {
assets.srcDir('build/generated/assets/sample-hello-apk/debug/')
}
release {
assets.srcDir('build/generated/assets/sample-hello-apk/release/')
}
}
//endregion
lintOptions {
checkReleaseBuilds false
abortOnError false
}
}
dependencies {
implementation "commons-io:commons-io:$commons_io_android_version"//sample-hello-host从assets中复制插件用的
implementation "org.slf4j:slf4j-api:$slf4j_version"
//region hello.apk 演示
implementation project(':sample-hello-api-holder')
//endregion
}
def createCopyTask(projectName, buildType, name, apkName, inputFile, taskName) {
def outputFile = file("${getBuildDir()}/generated/assets/${name}/${buildType}/${apkName}")
outputFile.getParentFile().mkdirs()
return tasks.create("copy${buildType.capitalize()}${name.capitalize()}Task", Copy) {
group = 'build'
description = "复制${name}到assets中."
from(inputFile.getParent()) {
include(inputFile.name)
rename { outputFile.name }
}
into(outputFile.getParent())
}.dependsOn("${projectName}:${taskName}")
}
//region hello.apk 演示
def generateHelloAssets(generateAssetsTask, buildType) {
def moduleName = 'sample-hello-apk'
def pluginManagerApkFile = file(
"${project(":sample-hello-apk").getBuildDir()}" +
"/outputs/apk/${buildType}/" +
"${moduleName}-${buildType}.apk"
)
generateAssetsTask.dependsOn createCopyTask(
':sample-hello-apk',
buildType,
moduleName,
'hello.apk',
pluginManagerApkFile,
"assemble${buildType.capitalize()}"
)
}
//endregion
tasks.whenTaskAdded { task ->
if (task.name == "generateDebugAssets") {
//region hello.apk 演示
generateHelloAssets(task, 'debug')
//endregion
}
if (task.name == "generateReleaseAssets") {
//region hello.apk 演示
generateHelloAssets(task, 'release')
//endregion
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-host/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class org.slf4j.**{*;}
-dontwarn org.slf4j.impl.**
-keep class com.tencent.shadow.dynamic.host.**{*;}
-keep class com.tencent.shadow.core.common.**{*;}
-keep class com.tencent.shadow.core.runtime.container.**{*;}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-host/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/dynamic-apk/sample-hello-host/src/main/java/com/tencent/shadow/sample/host/AndroidLogLoggerFactory.java
================================================
/*
* 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.sample.host;
import android.util.Log;
import com.tencent.shadow.core.common.ILoggerFactory;
import com.tencent.shadow.core.common.Logger;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class AndroidLogLoggerFactory implements ILoggerFactory {
private static final int LOG_LEVEL_TRACE = 5;
private static final int LOG_LEVEL_DEBUG = 4;
private static final int LOG_LEVEL_INFO = 3;
private static final int LOG_LEVEL_WARN = 2;
private static final int LOG_LEVEL_ERROR = 1;
private static AndroidLogLoggerFactory sInstance = new AndroidLogLoggerFactory();
public static ILoggerFactory getInstance() {
return sInstance;
}
final private ConcurrentMap loggerMap = new ConcurrentHashMap();
public Logger getLogger(String name) {
Logger simpleLogger = loggerMap.get(name);
if (simpleLogger != null) {
return simpleLogger;
} else {
Logger newInstance = new IVLogger(name);
Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
class IVLogger implements Logger {
private String name;
IVLogger(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
private void log(int level, String message, Throwable t) {
final String tag = String.valueOf(name);
switch (level) {
case LOG_LEVEL_TRACE:
case LOG_LEVEL_DEBUG:
if (t == null)
Log.d(tag, message);
else
Log.d(tag, message, t);
break;
case LOG_LEVEL_INFO:
if (t == null)
Log.i(tag, message);
else
Log.i(tag, message, t);
break;
case LOG_LEVEL_WARN:
if (t == null)
Log.w(tag, message);
else
Log.w(tag, message, t);
break;
case LOG_LEVEL_ERROR:
if (t == null)
Log.e(tag, message);
else
Log.e(tag, message, t);
break;
default:
break;
}
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public void trace(String msg) {
log(LOG_LEVEL_TRACE, msg, null);
}
@Override
public void trace(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String msg, Throwable throwable) {
log(LOG_LEVEL_TRACE, msg, throwable);
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public void debug(String msg) {
log(LOG_LEVEL_DEBUG, msg, null);
}
@Override
public void debug(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String msg, Throwable throwable) {
log(LOG_LEVEL_DEBUG, msg, throwable);
}
@Override
public boolean isInfoEnabled() {
return true;
}
@Override
public void info(String msg) {
log(LOG_LEVEL_INFO, msg, null);
}
@Override
public void info(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String msg, Throwable throwable) {
log(LOG_LEVEL_INFO, msg, throwable);
}
@Override
public boolean isWarnEnabled() {
return true;
}
@Override
public void warn(String msg) {
log(LOG_LEVEL_WARN, msg, null);
}
@Override
public void warn(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String msg, Throwable throwable) {
log(LOG_LEVEL_WARN, msg, throwable);
}
@Override
public boolean isErrorEnabled() {
return true;
}
@Override
public void error(String msg) {
log(LOG_LEVEL_ERROR, msg, null);
}
@Override
public void error(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String msg, Throwable throwable) {
log(LOG_LEVEL_ERROR, msg, throwable);
}
}
}
class FormattingTuple {
static public FormattingTuple NULL = new FormattingTuple(null);
private String message;
private Throwable throwable;
private Object[] argArray;
public FormattingTuple(String message) {
this(message, null, null);
}
public FormattingTuple(String message, Object[] argArray, Throwable throwable) {
this.message = message;
this.throwable = throwable;
this.argArray = argArray;
}
public String getMessage() {
return message;
}
public Object[] getArgArray() {
return argArray;
}
public Throwable getThrowable() {
return throwable;
}
}
final class MessageFormatter {
static final char DELIM_START = '{';
static final char DELIM_STOP = '}';
static final String DELIM_STR = "{}";
private static final char ESCAPE_CHAR = '\\';
/**
* Performs single argument substitution for the 'messagePattern' passed as
* parameter.
*
* For example,
*
*
* MessageFormatter.format("Hi {}.", "there");
*
*
* will return the string "Hi there.".
*
*
* @param messagePattern The message pattern which will be parsed and formatted
* @param arg The argument to be substituted in place of the formatting anchor
* @return The formatted message
*/
final public static FormattingTuple format(String messagePattern, Object arg) {
return arrayFormat(messagePattern, new Object[]{arg});
}
/**
* Performs a two argument substitution for the 'messagePattern' passed as
* parameter.
*
* For example,
*
*
* MessageFormatter.format("Hi {}. My name is {}.", "Alice", "Bob");
*
*
* will return the string "Hi Alice. My name is Bob.".
*
* @param messagePattern The message pattern which will be parsed and formatted
* @param arg1 The argument to be substituted in place of the first formatting
* anchor
* @param arg2 The argument to be substituted in place of the second formatting
* anchor
* @return The formatted message
*/
final public static FormattingTuple format(final String messagePattern, Object arg1, Object arg2) {
return arrayFormat(messagePattern, new Object[]{arg1, arg2});
}
static final Throwable getThrowableCandidate(Object[] argArray) {
if (argArray == null || argArray.length == 0) {
return null;
}
final Object lastEntry = argArray[argArray.length - 1];
if (lastEntry instanceof Throwable) {
return (Throwable) lastEntry;
}
return null;
}
final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) {
Throwable throwableCandidate = getThrowableCandidate(argArray);
Object[] args = argArray;
if (throwableCandidate != null) {
args = trimmedCopy(argArray);
}
return arrayFormat(messagePattern, args, throwableCandidate);
}
private static Object[] trimmedCopy(Object[] argArray) {
if (argArray == null || argArray.length == 0) {
throw new IllegalStateException("non-sensical empty or null argument array");
}
final int trimemdLen = argArray.length - 1;
Object[] trimmed = new Object[trimemdLen];
System.arraycopy(argArray, 0, trimmed, 0, trimemdLen);
return trimmed;
}
final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray, Throwable throwable) {
if (messagePattern == null) {
return new FormattingTuple(null, argArray, throwable);
}
if (argArray == null) {
return new FormattingTuple(messagePattern);
}
int i = 0;
int j;
// use string builder for better multicore performance
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
int L;
for (L = 0; L < argArray.length; L++) {
j = messagePattern.indexOf(DELIM_STR, i);
if (j == -1) {
// no more variables
if (i == 0) { // this is a simple string
return new FormattingTuple(messagePattern, argArray, throwable);
} else { // add the tail string which contains no variables and return
// the result.
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
} else {
if (isEscapedDelimeter(messagePattern, j)) {
if (!isDoubleEscaped(messagePattern, j)) {
L--; // DELIM_START was escaped, thus should not be incremented
sbuf.append(messagePattern, i, j - 1);
sbuf.append(DELIM_START);
i = j + 1;
} else {
// The escape character preceding the delimiter start is
// itself escaped: "abc x:\\{}"
// we have to consume one backward slash
sbuf.append(messagePattern, i, j - 1);
deeplyAppendParameter(sbuf, argArray[L], new HashMap());
i = j + 2;
}
} else {
// normal case
sbuf.append(messagePattern, i, j);
deeplyAppendParameter(sbuf, argArray[L], new HashMap());
i = j + 2;
}
}
}
// append the characters following the last {} pair.
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
final static boolean isEscapedDelimeter(String messagePattern, int delimeterStartIndex) {
if (delimeterStartIndex == 0) {
return false;
}
char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1);
if (potentialEscape == ESCAPE_CHAR) {
return true;
} else {
return false;
}
}
final static boolean isDoubleEscaped(String messagePattern, int delimeterStartIndex) {
if (delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR) {
return true;
} else {
return false;
}
}
// special treatment of array values was suggested by 'lizongbo'
private static void deeplyAppendParameter(StringBuilder sbuf, Object o, Map seenMap) {
if (o == null) {
sbuf.append("null");
return;
}
if (!o.getClass().isArray()) {
safeObjectAppend(sbuf, o);
} else {
// check for primitive array types because they
// unfortunately cannot be cast to Object[]
if (o instanceof boolean[]) {
booleanArrayAppend(sbuf, (boolean[]) o);
} else if (o instanceof byte[]) {
byteArrayAppend(sbuf, (byte[]) o);
} else if (o instanceof char[]) {
charArrayAppend(sbuf, (char[]) o);
} else if (o instanceof short[]) {
shortArrayAppend(sbuf, (short[]) o);
} else if (o instanceof int[]) {
intArrayAppend(sbuf, (int[]) o);
} else if (o instanceof long[]) {
longArrayAppend(sbuf, (long[]) o);
} else if (o instanceof float[]) {
floatArrayAppend(sbuf, (float[]) o);
} else if (o instanceof double[]) {
doubleArrayAppend(sbuf, (double[]) o);
} else {
objectArrayAppend(sbuf, (Object[]) o, seenMap);
}
}
}
private static void safeObjectAppend(StringBuilder sbuf, Object o) {
try {
String oAsString = o.toString();
sbuf.append(oAsString);
} catch (Throwable t) {
sbuf.append("[FAILED toString()]");
}
}
private static void objectArrayAppend(StringBuilder sbuf, Object[] a, Map seenMap) {
sbuf.append('[');
if (!seenMap.containsKey(a)) {
seenMap.put(a, null);
final int len = a.length;
for (int i = 0; i < len; i++) {
deeplyAppendParameter(sbuf, a[i], seenMap);
if (i != len - 1)
sbuf.append(", ");
}
// allow repeats in siblings
seenMap.remove(a);
} else {
sbuf.append("...");
}
sbuf.append(']');
}
private static void booleanArrayAppend(StringBuilder sbuf, boolean[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void byteArrayAppend(StringBuilder sbuf, byte[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void charArrayAppend(StringBuilder sbuf, char[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void shortArrayAppend(StringBuilder sbuf, short[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void intArrayAppend(StringBuilder sbuf, int[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void longArrayAppend(StringBuilder sbuf, long[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void floatArrayAppend(StringBuilder sbuf, float[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void doubleArrayAppend(StringBuilder sbuf, double[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-host/src/main/java/com/tencent/shadow/sample/host/HostApplication.java
================================================
/*
* 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.sample.host;
import android.app.Application;
import android.os.Build;
import android.os.StrictMode;
import android.webkit.WebView;
import com.tencent.shadow.core.common.LoggerFactory;
public class HostApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
detectNonSdkApiUsageOnAndroidP();
setWebViewDataDirectorySuffix();
LoggerFactory.setILoggerFactory(new AndroidLogLoggerFactory());
PluginHelper.getInstance().init(this);
}
private static void setWebViewDataDirectorySuffix() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return;
}
WebView.setDataDirectorySuffix(Application.getProcessName());
}
private static void detectNonSdkApiUsageOnAndroidP() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return;
}
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
builder.detectNonSdkApiUsage();
StrictMode.setVmPolicy(builder.build());
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-host/src/main/java/com/tencent/shadow/sample/host/MainActivity.java
================================================
/*
* 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.sample.host;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.tencent.shadow.sample.api.hello.IHelloWorld;
import com.tencent.shadow.sample.host.api.HelloWorldApiHolder;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(android.R.style.Theme_NoTitleBar);
LinearLayout rootView = new LinearLayout(this);
rootView.setOrientation(LinearLayout.VERTICAL);
rootView.addView(createTextView("演示自定义 api 的动态化,宿主 api 的实现在 hello.apk 中", null));
final TextView textView = createTextView("等待apk实现", null);
rootView.addView(createButton("宿主自定义接口的动态化", new View.OnClickListener() {
@Override
public void onClick(View v) {
PluginHelper.getInstance().singlePool.execute(new Runnable() {
@Override
public void run() {
//hello.apk 里实现了 IHelloWorld
final IHelloWorld api = HelloWorldApiHolder.getHelloWorld(PluginHelper.getInstance().helloApkFile);
if (api == null) {
return;
}
runOnUiThread(new Runnable() {
@Override
public void run() {
api.sayHelloWorld(MainActivity.this, textView);
}
});
}
});
}
}));
rootView.addView(textView);
setContentView(rootView);
}
public Button createButton(String title, View.OnClickListener listener) {
Button button = new Button(this);
button.setText(title);
button.setOnClickListener(listener);
return button;
}
public TextView createTextView(String title, View.OnClickListener listener) {
TextView textView = new TextView(this);
textView.setText(title);
textView.setOnClickListener(listener);
return textView;
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-host/src/main/java/com/tencent/shadow/sample/host/PluginHelper.java
================================================
/*
* 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.sample.host;
import android.content.Context;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PluginHelper {
/**
* 动态加载的 hello.apk
*/
public final static String sHelloApkName = "hello.apk";
public File helloApkFile;
public ExecutorService singlePool = Executors.newSingleThreadExecutor();
private Context mContext;
private static PluginHelper sInstance = new PluginHelper();
public static PluginHelper getInstance() {
return sInstance;
}
private PluginHelper() {
}
public void init(Context context) {
helloApkFile = new File(context.getFilesDir(), sHelloApkName);
mContext = context.getApplicationContext();
singlePool.execute(new Runnable() {
@Override
public void run() {
preparePlugin();
}
});
}
private void preparePlugin() {
try {
InputStream is = mContext.getAssets().open(sHelloApkName);
FileUtils.copyInputStreamToFile(is, helloApkFile);
} catch (IOException e) {
throw new RuntimeException("从assets中复制apk出错", e);
}
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-host/src/main/java/com/tencent/shadow/sample/host/api/FixedPathPmUpdater.java
================================================
/*
* 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.sample.host.api;
import com.tencent.shadow.sample.apk.hello.HelloWorldUpdater;
import java.io.File;
import java.util.concurrent.Future;
public class FixedPathPmUpdater implements HelloWorldUpdater {
final private File apk;
FixedPathPmUpdater(File apk) {
this.apk = apk;
}
@Override
public Future update() {
return null;
}
@Override
public File getLatest() {
return apk;
}
}
================================================
FILE: projects/sample/dynamic-apk/sample-hello-host/src/main/java/com/tencent/shadow/sample/host/api/HelloWorldApiHolder.java
================================================
/*
* 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.sample.host.api;
import com.tencent.shadow.sample.api.hello.IHelloWorld;
import com.tencent.shadow.sample.apk.hello.DynamicHello;
import java.io.File;
public class HelloWorldApiHolder {
public static IHelloWorld getHelloWorld(File apk) {
final FixedPathPmUpdater fixedPathPmUpdater = new FixedPathPmUpdater(apk);
File tempPm = fixedPathPmUpdater.getLatest();
if (tempPm != null) {
return new DynamicHello(fixedPathPmUpdater);
}
return null;
}
}
================================================
FILE: projects/sample/maven/host-project/.gitignore
================================================
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
.externalNativeBuild
================================================
FILE: projects/sample/maven/host-project/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.shadow_version = '2.2.1'
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
maven {
name = "GitHubPackages"
url "https://maven.pkg.github.com/tencent/shadow"
//一个只读账号兼容Github Packages暂时不支持匿名下载
//https://github.community/t/download-from-github-package-registry-without-authentication/14407
credentials {
username = 'readonlypat'
password = '\u0067hp_s3VOOZnLf1bTyvHWblPfaessrVYyEU4JdNbs'
}
}
mavenLocal()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: projects/sample/maven/host-project/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
distributionUrl=https\://mirrors.tencent.com/gradle/gradle-6.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: projects/sample/maven/host-project/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx4096m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
================================================
FILE: projects/sample/maven/host-project/gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: projects/sample/maven/host-project/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: projects/sample/maven/host-project/introduce-shadow-lib/.gitignore
================================================
/build
================================================
FILE: projects/sample/maven/host-project/introduce-shadow-lib/build.gradle
================================================
apply plugin: 'com.android.library'
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation "com.tencent.shadow.dynamic:host:$shadow_version"
}
================================================
FILE: projects/sample/maven/host-project/introduce-shadow-lib/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: projects/sample/maven/host-project/introduce-shadow-lib/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/maven/host-project/introduce-shadow-lib/src/main/java/com/tencent/shadow/sample/introduce_shadow_lib/AndroidLoggerFactory.java
================================================
package com.tencent.shadow.sample.introduce_shadow_lib;
import android.util.Log;
import com.tencent.shadow.core.common.ILoggerFactory;
import com.tencent.shadow.core.common.Logger;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class AndroidLoggerFactory implements ILoggerFactory {
private static final int LOG_LEVEL_TRACE = 5;
private static final int LOG_LEVEL_DEBUG = 4;
private static final int LOG_LEVEL_INFO = 3;
private static final int LOG_LEVEL_WARN = 2;
private static final int LOG_LEVEL_ERROR = 1;
private static AndroidLoggerFactory sInstance = new AndroidLoggerFactory();
public static ILoggerFactory getInstance() {
return sInstance;
}
final private ConcurrentMap loggerMap = new ConcurrentHashMap();
public Logger getLogger(String name) {
Logger simpleLogger = loggerMap.get(name);
if (simpleLogger != null) {
return simpleLogger;
} else {
Logger newInstance = new IVLogger(name);
Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
class IVLogger implements Logger {
private String name;
IVLogger(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
private void log(int level, String message, Throwable t) {
final String tag = String.valueOf(name);
switch (level) {
case LOG_LEVEL_TRACE:
case LOG_LEVEL_DEBUG:
if (t == null)
Log.d(tag, message);
else
Log.d(tag, message, t);
break;
case LOG_LEVEL_INFO:
if (t == null)
Log.i(tag, message);
else
Log.i(tag, message, t);
break;
case LOG_LEVEL_WARN:
if (t == null)
Log.w(tag, message);
else
Log.w(tag, message, t);
break;
case LOG_LEVEL_ERROR:
if (t == null)
Log.e(tag, message);
else
Log.e(tag, message, t);
break;
default:
break;
}
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public void trace(String msg) {
log(LOG_LEVEL_TRACE, msg, null);
}
@Override
public void trace(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String msg, Throwable throwable) {
log(LOG_LEVEL_TRACE, msg, throwable);
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public void debug(String msg) {
log(LOG_LEVEL_DEBUG, msg, null);
}
@Override
public void debug(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String msg, Throwable throwable) {
log(LOG_LEVEL_DEBUG, msg, throwable);
}
@Override
public boolean isInfoEnabled() {
return true;
}
@Override
public void info(String msg) {
log(LOG_LEVEL_INFO, msg, null);
}
@Override
public void info(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String msg, Throwable throwable) {
log(LOG_LEVEL_INFO, msg, throwable);
}
@Override
public boolean isWarnEnabled() {
return true;
}
@Override
public void warn(String msg) {
log(LOG_LEVEL_WARN, msg, null);
}
@Override
public void warn(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String msg, Throwable throwable) {
log(LOG_LEVEL_WARN, msg, throwable);
}
@Override
public boolean isErrorEnabled() {
return true;
}
@Override
public void error(String msg) {
log(LOG_LEVEL_ERROR, msg, null);
}
@Override
public void error(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String msg, Throwable throwable) {
log(LOG_LEVEL_ERROR, msg, throwable);
}
}
}
class FormattingTuple {
static public FormattingTuple NULL = new FormattingTuple(null);
private String message;
private Throwable throwable;
private Object[] argArray;
public FormattingTuple(String message) {
this(message, null, null);
}
public FormattingTuple(String message, Object[] argArray, Throwable throwable) {
this.message = message;
this.throwable = throwable;
this.argArray = argArray;
}
public String getMessage() {
return message;
}
public Object[] getArgArray() {
return argArray;
}
public Throwable getThrowable() {
return throwable;
}
}
final class MessageFormatter {
static final char DELIM_START = '{';
static final char DELIM_STOP = '}';
static final String DELIM_STR = "{}";
private static final char ESCAPE_CHAR = '\\';
/**
* Performs single argument substitution for the 'messagePattern' passed as
* parameter.
*
* For example,
*
*
* MessageFormatter.format("Hi {}.", "there");
*
*
* will return the string "Hi there.".
*
*
* @param messagePattern The message pattern which will be parsed and formatted
* @param arg The argument to be substituted in place of the formatting anchor
* @return The formatted message
*/
final public static FormattingTuple format(String messagePattern, Object arg) {
return arrayFormat(messagePattern, new Object[]{arg});
}
/**
* Performs a two argument substitution for the 'messagePattern' passed as
* parameter.
*
* For example,
*
*
* MessageFormatter.format("Hi {}. My name is {}.", "Alice", "Bob");
*
*
* will return the string "Hi Alice. My name is Bob.".
*
* @param messagePattern The message pattern which will be parsed and formatted
* @param arg1 The argument to be substituted in place of the first formatting
* anchor
* @param arg2 The argument to be substituted in place of the second formatting
* anchor
* @return The formatted message
*/
final public static FormattingTuple format(final String messagePattern, Object arg1, Object arg2) {
return arrayFormat(messagePattern, new Object[]{arg1, arg2});
}
static final Throwable getThrowableCandidate(Object[] argArray) {
if (argArray == null || argArray.length == 0) {
return null;
}
final Object lastEntry = argArray[argArray.length - 1];
if (lastEntry instanceof Throwable) {
return (Throwable) lastEntry;
}
return null;
}
final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) {
Throwable throwableCandidate = getThrowableCandidate(argArray);
Object[] args = argArray;
if (throwableCandidate != null) {
args = trimmedCopy(argArray);
}
return arrayFormat(messagePattern, args, throwableCandidate);
}
private static Object[] trimmedCopy(Object[] argArray) {
if (argArray == null || argArray.length == 0) {
throw new IllegalStateException("non-sensical empty or null argument array");
}
final int trimemdLen = argArray.length - 1;
Object[] trimmed = new Object[trimemdLen];
System.arraycopy(argArray, 0, trimmed, 0, trimemdLen);
return trimmed;
}
final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray, Throwable throwable) {
if (messagePattern == null) {
return new FormattingTuple(null, argArray, throwable);
}
if (argArray == null) {
return new FormattingTuple(messagePattern);
}
int i = 0;
int j;
// use string builder for better multicore performance
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
int L;
for (L = 0; L < argArray.length; L++) {
j = messagePattern.indexOf(DELIM_STR, i);
if (j == -1) {
// no more variables
if (i == 0) { // this is a simple string
return new FormattingTuple(messagePattern, argArray, throwable);
} else { // add the tail string which contains no variables and return
// the result.
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
} else {
if (isEscapedDelimeter(messagePattern, j)) {
if (!isDoubleEscaped(messagePattern, j)) {
L--; // DELIM_START was escaped, thus should not be incremented
sbuf.append(messagePattern, i, j - 1);
sbuf.append(DELIM_START);
i = j + 1;
} else {
// The escape character preceding the delimiter start is
// itself escaped: "abc x:\\{}"
// we have to consume one backward slash
sbuf.append(messagePattern, i, j - 1);
deeplyAppendParameter(sbuf, argArray[L], new HashMap());
i = j + 2;
}
} else {
// normal case
sbuf.append(messagePattern, i, j);
deeplyAppendParameter(sbuf, argArray[L], new HashMap());
i = j + 2;
}
}
}
// append the characters following the last {} pair.
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
final static boolean isEscapedDelimeter(String messagePattern, int delimeterStartIndex) {
if (delimeterStartIndex == 0) {
return false;
}
char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1);
if (potentialEscape == ESCAPE_CHAR) {
return true;
} else {
return false;
}
}
final static boolean isDoubleEscaped(String messagePattern, int delimeterStartIndex) {
if (delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR) {
return true;
} else {
return false;
}
}
// special treatment of array values was suggested by 'lizongbo'
private static void deeplyAppendParameter(StringBuilder sbuf, Object o, Map seenMap) {
if (o == null) {
sbuf.append("null");
return;
}
if (!o.getClass().isArray()) {
safeObjectAppend(sbuf, o);
} else {
// check for primitive array types because they
// unfortunately cannot be cast to Object[]
if (o instanceof boolean[]) {
booleanArrayAppend(sbuf, (boolean[]) o);
} else if (o instanceof byte[]) {
byteArrayAppend(sbuf, (byte[]) o);
} else if (o instanceof char[]) {
charArrayAppend(sbuf, (char[]) o);
} else if (o instanceof short[]) {
shortArrayAppend(sbuf, (short[]) o);
} else if (o instanceof int[]) {
intArrayAppend(sbuf, (int[]) o);
} else if (o instanceof long[]) {
longArrayAppend(sbuf, (long[]) o);
} else if (o instanceof float[]) {
floatArrayAppend(sbuf, (float[]) o);
} else if (o instanceof double[]) {
doubleArrayAppend(sbuf, (double[]) o);
} else {
objectArrayAppend(sbuf, (Object[]) o, seenMap);
}
}
}
private static void safeObjectAppend(StringBuilder sbuf, Object o) {
try {
String oAsString = o.toString();
sbuf.append(oAsString);
} catch (Throwable t) {
sbuf.append("[FAILED toString()]");
}
}
private static void objectArrayAppend(StringBuilder sbuf, Object[] a, Map seenMap) {
sbuf.append('[');
if (!seenMap.containsKey(a)) {
seenMap.put(a, null);
final int len = a.length;
for (int i = 0; i < len; i++) {
deeplyAppendParameter(sbuf, a[i], seenMap);
if (i != len - 1)
sbuf.append(", ");
}
// allow repeats in siblings
seenMap.remove(a);
} else {
sbuf.append("...");
}
sbuf.append(']');
}
private static void booleanArrayAppend(StringBuilder sbuf, boolean[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void byteArrayAppend(StringBuilder sbuf, byte[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void charArrayAppend(StringBuilder sbuf, char[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void shortArrayAppend(StringBuilder sbuf, short[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void intArrayAppend(StringBuilder sbuf, int[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void longArrayAppend(StringBuilder sbuf, long[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void floatArrayAppend(StringBuilder sbuf, float[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void doubleArrayAppend(StringBuilder sbuf, double[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
}
================================================
FILE: projects/sample/maven/host-project/introduce-shadow-lib/src/main/java/com/tencent/shadow/sample/introduce_shadow_lib/FixedPathPmUpdater.java
================================================
package com.tencent.shadow.sample.introduce_shadow_lib;
import com.tencent.shadow.dynamic.host.PluginManagerUpdater;
import java.io.File;
import java.util.concurrent.Future;
/**
* 这个Updater没有任何升级能力。直接将指定路径作为其升级结果。
*/
public class FixedPathPmUpdater implements PluginManagerUpdater {
final private File apk;
FixedPathPmUpdater(File apk) {
this.apk = apk;
}
@Override
public boolean wasUpdating() {
return false;
}
@Override
public Future update() {
return null;
}
@Override
public File getLatest() {
return apk;
}
@Override
public Future isAvailable(final File file) {
return null;
}
}
================================================
FILE: projects/sample/maven/host-project/introduce-shadow-lib/src/main/java/com/tencent/shadow/sample/introduce_shadow_lib/InitApplication.java
================================================
package com.tencent.shadow.sample.introduce_shadow_lib;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import com.tencent.shadow.core.common.LoggerFactory;
import com.tencent.shadow.dynamic.host.DynamicPluginManager;
import com.tencent.shadow.dynamic.host.DynamicRuntime;
import com.tencent.shadow.dynamic.host.PluginManager;
import java.io.File;
import java.util.concurrent.Future;
import static android.os.Process.myPid;
public class InitApplication {
/**
* 这个PluginManager对象在Manager升级前后是不变的。它内部持有具体实现,升级时更换具体实现。
*/
private static PluginManager sPluginManager;
public static PluginManager getPluginManager() {
return sPluginManager;
}
public static void onApplicationCreate(Application application) {
//Log接口Manager也需要使用,所以主进程也初始化。
LoggerFactory.setILoggerFactory(new AndroidLoggerFactory());
if (isProcess(application, ":plugin")) {
//在全动态架构中,Activity组件没有打包在宿主而是位于被动态加载的runtime,
//为了防止插件crash后,系统自动恢复crash前的Activity组件,此时由于没有加载runtime而发生classNotFound异常,导致二次crash
//因此这里恢复加载上一次的runtime
DynamicRuntime.recoveryRuntime(application);
}
if (isProcess(application, application.getPackageName())) {
FixedPathPmUpdater fixedPathPmUpdater
= new FixedPathPmUpdater(new File("/data/local/tmp/sample-manager-debug.apk"));
boolean needWaitingUpdate
= fixedPathPmUpdater.wasUpdating()//之前正在更新中,暗示更新出错了,应该放弃之前的缓存
|| fixedPathPmUpdater.getLatest() == null;//没有本地缓存
Future update = fixedPathPmUpdater.update();
if (needWaitingUpdate) {
try {
update.get();//这里是阻塞的,需要业务自行保证更新Manager足够快。
} catch (Exception e) {
throw new RuntimeException("Sample程序不容错", e);
}
}
sPluginManager = new DynamicPluginManager(fixedPathPmUpdater);
}
}
private static boolean isProcess(Context context, String processName) {
String currentProcName = "";
ActivityManager manager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == myPid()) {
currentProcName = processInfo.processName;
break;
}
}
return currentProcName.endsWith(processName);
}
}
================================================
FILE: projects/sample/maven/host-project/introduce-shadow-lib/src/main/java/com/tencent/shadow/sample/introduce_shadow_lib/MainPluginProcessService.java
================================================
package com.tencent.shadow.sample.introduce_shadow_lib;
import com.tencent.shadow.dynamic.host.PluginProcessService;
/**
* 一个PluginProcessService(简称PPS)代表一个插件进程。插件进程由PPS启动触发启动。
* 新建PPS子类允许一个宿主中有多个互不影响的插件进程。
*/
public class MainPluginProcessService extends PluginProcessService {
}
================================================
FILE: projects/sample/maven/host-project/introduce-shadow-lib/src/main/res/values/themes.xml
================================================
================================================
FILE: projects/sample/maven/host-project/sample-host/.gitignore
================================================
/build
================================================
FILE: projects/sample/maven/host-project/sample-host/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
defaultConfig {
applicationId "com.tencent.shadow.sample.host"
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
}
dependencies {
implementation project(':introduce-shadow-lib')
//如果introduce-shadow-lib发布到Maven,在pom中写明此依赖,宿主就不用写这个依赖了。
implementation "com.tencent.shadow.dynamic:host:$shadow_version"
}
================================================
FILE: projects/sample/maven/host-project/sample-host/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class com.tencent.shadow.core.common.**{*;}
-keep class com.tencent.shadow.core.runtime.**{*;}
-keep class com.tencent.shadow.dynamic.host.**{*;}
================================================
FILE: projects/sample/maven/host-project/sample-host/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/maven/host-project/sample-host/src/main/java/com/tencent/shadow/sample/host/MainActivity.java
================================================
package com.tencent.shadow.sample.host;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.tencent.shadow.dynamic.host.EnterCallback;
import com.tencent.shadow.dynamic.host.PluginManager;
import com.tencent.shadow.sample.introduce_shadow_lib.InitApplication;
public class MainActivity extends Activity {
public static final int FROM_ID_START_ACTIVITY = 1001;
public static final int FROM_ID_CALL_SERVICE = 1002;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final LinearLayout linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
TextView textView = new TextView(this);
textView.setText("宿主App");
Button button = new Button(this);
button.setText("启动插件");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
v.setEnabled(false);//防止点击重入
PluginManager pluginManager = InitApplication.getPluginManager();
pluginManager.enter(MainActivity.this, FROM_ID_START_ACTIVITY, new Bundle(), new EnterCallback() {
@Override
public void onShowLoadingView(View view) {
MainActivity.this.setContentView(view);//显示Manager传来的Loading页面
}
@Override
public void onCloseLoadingView() {
MainActivity.this.setContentView(linearLayout);
}
@Override
public void onEnterComplete() {
v.setEnabled(true);
}
});
}
});
linearLayout.addView(textView);
linearLayout.addView(button);
Button callServiceButton = new Button(this);
callServiceButton.setText("调用插件Service,结果打印到Log");
callServiceButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
v.setEnabled(false);//防止点击重入
PluginManager pluginManager = InitApplication.getPluginManager();
pluginManager.enter(MainActivity.this, FROM_ID_CALL_SERVICE, null, null);
}
});
linearLayout.addView(callServiceButton);
setContentView(linearLayout);
}
}
================================================
FILE: projects/sample/maven/host-project/sample-host/src/main/java/com/tencent/shadow/sample/host/MyApplication.java
================================================
package com.tencent.shadow.sample.host;
import android.app.Application;
import com.tencent.shadow.sample.introduce_shadow_lib.InitApplication;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
InitApplication.onApplicationCreate(this);
}
}
================================================
FILE: projects/sample/maven/host-project/settings.gradle
================================================
include ':sample-host', ':introduce-shadow-lib'
================================================
FILE: projects/sample/maven/manager-project/.gitignore
================================================
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
.externalNativeBuild
================================================
FILE: projects/sample/maven/manager-project/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.shadow_version = '2.2.1'
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
maven {
name = "GitHubPackages"
url "https://maven.pkg.github.com/tencent/shadow"
//一个只读账号兼容Github Packages暂时不支持匿名下载
//https://github.community/t/download-from-github-package-registry-without-authentication/14407
credentials {
username = 'readonlypat'
password = '\u0067hp_s3VOOZnLf1bTyvHWblPfaessrVYyEU4JdNbs'
}
}
mavenLocal()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: projects/sample/maven/manager-project/gradle/wrapper/gradle-wrapper.properties
================================================
#Wed May 08 11:09:16 CST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
distributionUrl=https\://mirrors.tencent.com/gradle/gradle-6.6.1-bin.zip
================================================
FILE: projects/sample/maven/manager-project/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx4096m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
================================================
FILE: projects/sample/maven/manager-project/gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: projects/sample/maven/manager-project/gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: projects/sample/maven/manager-project/sample-manager/.gitignore
================================================
/build
================================================
FILE: projects/sample/maven/manager-project/sample-manager/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
defaultConfig {
applicationId "com.tencent.shadow.sample.manager"
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
lintOptions {
abortOnError false
}
}
dependencies {
implementation "com.tencent.shadow.dynamic:manager:$shadow_version"
compileOnly "com.tencent.shadow.core:common:$shadow_version"
compileOnly "com.tencent.shadow.dynamic:host:$shadow_version"
}
================================================
FILE: projects/sample/maven/manager-project/sample-manager/proguard-rules.pro
================================================
-keep class com.tencent.shadow.dynamic.impl.**{*;}
-keep class com.tencent.shadow.dynamic.loader.**{*;}
================================================
FILE: projects/sample/maven/manager-project/sample-manager/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/maven/manager-project/sample-manager/src/main/aidl/com/tencent/shadow/sample/plugin/IMyAidlInterface.aidl
================================================
// IMyAidlInterface.aidl
package com.tencent.shadow.sample.plugin;
// Declare any non-default types here with import statements
interface IMyAidlInterface {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
String basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
================================================
FILE: projects/sample/maven/manager-project/sample-manager/src/main/java/com/tencent/shadow/dynamic/impl/ManagerFactoryImpl.java
================================================
package com.tencent.shadow.dynamic.impl;
import android.content.Context;
import com.tencent.shadow.dynamic.host.ManagerFactory;
import com.tencent.shadow.dynamic.host.PluginManagerImpl;
import com.tencent.shadow.sample.manager.SamplePluginManager;
/**
* 此类包名及类名固定
*/
public final class ManagerFactoryImpl implements ManagerFactory {
@Override
public PluginManagerImpl buildManager(Context context) {
return new SamplePluginManager(context);
}
}
================================================
FILE: projects/sample/maven/manager-project/sample-manager/src/main/java/com/tencent/shadow/dynamic/impl/WhiteList.java
================================================
package com.tencent.shadow.dynamic.impl;
/**
* 此类包名及类名固定
* classLoader的白名单
* PluginManager可以加载宿主中位于白名单内的类
*/
public interface WhiteList {
String[] sWhiteList = new String[]
{
};
}
================================================
FILE: projects/sample/maven/manager-project/sample-manager/src/main/java/com/tencent/shadow/sample/manager/Constant.java
================================================
package com.tencent.shadow.sample.manager;
final public class Constant {
public static final String KEY_PLUGIN_ZIP_PATH = "pluginZipPath";
public static final String KEY_ACTIVITY_CLASSNAME = "KEY_ACTIVITY_CLASSNAME";
public static final String KEY_EXTRAS = "KEY_EXTRAS";
public static final String KEY_PLUGIN_PART_KEY = "KEY_PLUGIN_PART_KEY";
public static final int FROM_ID_START_ACTIVITY = 1001;
public static final int FROM_ID_CALL_SERVICE = 1002;
}
================================================
FILE: projects/sample/maven/manager-project/sample-manager/src/main/java/com/tencent/shadow/sample/manager/FastPluginManager.java
================================================
package com.tencent.shadow.sample.manager;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.RemoteException;
import com.tencent.shadow.core.common.Logger;
import com.tencent.shadow.core.common.LoggerFactory;
import com.tencent.shadow.core.manager.installplugin.InstalledPlugin;
import com.tencent.shadow.core.manager.installplugin.InstalledType;
import com.tencent.shadow.core.manager.installplugin.PluginConfig;
import com.tencent.shadow.dynamic.host.FailedException;
import com.tencent.shadow.dynamic.manager.PluginManagerThatUseDynamicLoader;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public abstract class FastPluginManager extends PluginManagerThatUseDynamicLoader {
private static final Logger mLogger = LoggerFactory.getLogger(FastPluginManager.class);
private ExecutorService mFixedPool = Executors.newFixedThreadPool(4);
public FastPluginManager(Context context) {
super(context);
}
public InstalledPlugin installPlugin(String zip, String hash, boolean odex) throws IOException, JSONException, InterruptedException, ExecutionException {
final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
final String uuid = pluginConfig.UUID;
List futures = new LinkedList<>();
if (pluginConfig.runTime != null && pluginConfig.pluginLoader != null) {
Future odexRuntime = mFixedPool.submit(new Callable() {
@Override
public Object call() throws Exception {
oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME,
pluginConfig.runTime.file);
return null;
}
});
futures.add(odexRuntime);
Future odexLoader = mFixedPool.submit(new Callable() {
@Override
public Object call() throws Exception {
oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER,
pluginConfig.pluginLoader.file);
return null;
}
});
futures.add(odexLoader);
}
for (Map.Entry plugin : pluginConfig.plugins.entrySet()) {
final String partKey = plugin.getKey();
final File apkFile = plugin.getValue().file;
Future extractSo = mFixedPool.submit(new Callable() {
@Override
public Object call() throws Exception {
extractSo(uuid, partKey, apkFile);
return null;
}
});
futures.add(extractSo);
if (odex) {
Future odexPlugin = mFixedPool.submit(new Callable() {
@Override
public Object call() throws Exception {
oDexPlugin(uuid, partKey, apkFile);
return null;
}
});
futures.add(odexPlugin);
}
}
for (Future future : futures) {
future.get();
}
onInstallCompleted(pluginConfig);
return getInstalledPlugins(1).get(0);
}
public void startPluginActivity(Context context, InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) throws RemoteException, TimeoutException, FailedException {
Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);
if (!(context instanceof Activity)) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
public Intent convertActivityIntent(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) throws RemoteException, TimeoutException, FailedException {
loadPlugin(installedPlugin.UUID, partKey);
return mPluginLoader.convertActivityIntent(pluginIntent);
}
private void loadPluginLoaderAndRuntime(String uuid) throws RemoteException, TimeoutException, FailedException {
if (mPpsController == null) {
bindPluginProcessService(getPluginProcessServiceName());
waitServiceConnected(10, TimeUnit.SECONDS);
}
loadRunTime(uuid);
loadPluginLoader(uuid);
}
protected void loadPlugin(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
loadPluginLoaderAndRuntime(uuid);
Map map = mPluginLoader.getLoadedPlugin();
if (!map.containsKey(partKey)) {
mPluginLoader.loadPlugin(partKey);
}
Boolean isCall = (Boolean) map.get(partKey);
if (isCall == null || !isCall) {
mPluginLoader.callApplicationOnCreate(partKey);
}
}
protected abstract String getPluginProcessServiceName();
}
================================================
FILE: projects/sample/maven/manager-project/sample-manager/src/main/java/com/tencent/shadow/sample/manager/SamplePluginManager.java
================================================
package com.tencent.shadow.sample.manager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.*;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import com.tencent.shadow.core.manager.installplugin.InstalledPlugin;
import com.tencent.shadow.dynamic.host.EnterCallback;
import com.tencent.shadow.dynamic.loader.PluginServiceConnection;
import com.tencent.shadow.sample.plugin.IMyAidlInterface;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SamplePluginManager extends FastPluginManager {
private ExecutorService executorService = Executors.newSingleThreadExecutor();
private Context mCurrentContext;
public SamplePluginManager(Context context) {
super(context);
mCurrentContext = context;
}
/**
* @return PluginManager实现的别名,用于区分不同PluginManager实现的数据存储路径
*/
@Override
protected String getName() {
return "sample-manager";
}
/**
* @return 宿主中注册的PluginProcessService实现的类名
*/
@Override
protected String getPluginProcessServiceName() {
return "com.tencent.shadow.sample.introduce_shadow_lib.MainPluginProcessService";
}
@Override
public void enter(final Context context, long fromId, Bundle bundle, final EnterCallback callback) {
if (fromId == Constant.FROM_ID_START_ACTIVITY) {
bundle.putString(Constant.KEY_PLUGIN_ZIP_PATH, "/data/local/tmp/plugin-debug.zip");
bundle.putString(Constant.KEY_PLUGIN_PART_KEY, "sample-plugin");
bundle.putString(Constant.KEY_ACTIVITY_CLASSNAME, "com.tencent.shadow.sample.plugin.MainActivity");
onStartActivity(context, bundle, callback);
} else if (fromId == Constant.FROM_ID_CALL_SERVICE) {
callPluginService(context);
} else {
throw new IllegalArgumentException("不认识的fromId==" + fromId);
}
}
private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
final String pluginZipPath = bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);
final String partKey = bundle.getString(Constant.KEY_PLUGIN_PART_KEY);
final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
if (className == null) {
throw new NullPointerException("className == null");
}
final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);
if (callback != null) {
final View view = LayoutInflater.from(mCurrentContext).inflate(R.layout.activity_load_plugin, null);
callback.onShowLoadingView(view);
}
executorService.execute(new Runnable() {
@Override
public void run() {
try {
InstalledPlugin installedPlugin
= installPlugin(pluginZipPath, null, true);//这个调用是阻塞的
Intent pluginIntent = new Intent();
pluginIntent.setClassName(
context.getPackageName(),
className
);
if (extras != null) {
pluginIntent.replaceExtras(extras);
}
startPluginActivity(context, installedPlugin, partKey, pluginIntent);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (callback != null) {
Handler uiHandler = new Handler(Looper.getMainLooper());
uiHandler.post(new Runnable() {
@Override
public void run() {
callback.onCloseLoadingView();
callback.onEnterComplete();
}
});
}
}
});
}
private void callPluginService(final Context context) {
final String pluginZipPath = "/data/local/tmp/plugin-debug.zip";
final String partKey = "sample-plugin";
final String className = "com.tencent.shadow.sample.plugin.MyService";
Intent pluginIntent = new Intent();
pluginIntent.setClassName(context.getPackageName(), className);
executorService.execute(new Runnable() {
@Override
public void run() {
try {
InstalledPlugin installedPlugin
= installPlugin(pluginZipPath, null, true);//这个调用是阻塞的
loadPlugin(installedPlugin.UUID, partKey);
Intent pluginIntent = new Intent();
pluginIntent.setClassName(context.getPackageName(), className);
boolean callSuccess = mPluginLoader.bindPluginService(pluginIntent, new PluginServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
IMyAidlInterface iMyAidlInterface = IMyAidlInterface.Stub.asInterface(iBinder);
try {
String s = iMyAidlInterface.basicTypes(1, 2, true, 4.0f, 5.0, "6");
Log.i("SamplePluginManager", "iMyAidlInterface.basicTypes : " + s);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
throw new RuntimeException("onServiceDisconnected");
}
}, Service.BIND_AUTO_CREATE);
if (!callSuccess) {
throw new RuntimeException("bind service失败 className==" + className);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
================================================
FILE: projects/sample/maven/manager-project/sample-manager/src/main/res/layout/activity_load_plugin.xml
================================================
================================================
FILE: projects/sample/maven/manager-project/settings.gradle
================================================
include ':sample-manager'
================================================
FILE: projects/sample/maven/plugin-project/.gitignore
================================================
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
.externalNativeBuild
================================================
FILE: projects/sample/maven/plugin-project/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.shadow_version = '2.2.1'
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
maven {
name = "GitHubPackages"
url "https://maven.pkg.github.com/tencent/shadow"
//一个只读账号兼容Github Packages暂时不支持匿名下载
//https://github.community/t/download-from-github-package-registry-without-authentication/14407
credentials {
username = 'readonlypat'
password = '\u0067hp_s3VOOZnLf1bTyvHWblPfaessrVYyEU4JdNbs'
}
}
mavenLocal()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: projects/sample/maven/plugin-project/gradle/wrapper/gradle-wrapper.properties
================================================
#Wed May 08 11:09:16 CST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
distributionUrl=https\://mirrors.tencent.com/gradle/gradle-6.6.1-bin.zip
================================================
FILE: projects/sample/maven/plugin-project/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx4096m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
================================================
FILE: projects/sample/maven/plugin-project/gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: projects/sample/maven/plugin-project/gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/.gitignore
================================================
/build
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'com.tencent.shadow.plugin'
android {
compileSdkVersion 30
defaultConfig {
applicationId "com.tencent.shadow.sample.plugin"
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
// 将插件applicationId设置为和宿主相同
productFlavors {
plugin {
applicationId "com.tencent.shadow.sample.host"
}
}
}
dependencies {
//Shadow Transform后业务代码会有一部分实际引用runtime中的类
//如果不以compileOnly方式依赖,会导致其他Transform或者Proguard找不到这些类
pluginCompileOnly "com.tencent.shadow.core:runtime:$shadow_version"
}
//这段buildscript配置的dependencies是为了apply plugin: 'com.tencent.shadow.plugin'能找到实现
buildscript {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
maven {
name = "GitHubPackages"
url "https://maven.pkg.github.com/tencent/shadow"
//一个只读账号兼容Github Packages暂时不支持匿名下载
//https://github.community/t/download-from-github-package-registry-without-authentication/14407
credentials {
username = 'readonlypat'
password = '\u0067hp_s3VOOZnLf1bTyvHWblPfaessrVYyEU4JdNbs'
}
}
mavenLocal()
}
dependencies {
classpath "com.tencent.shadow.core:gradle-plugin:$shadow_version"
}
}
shadow {
packagePlugin {
pluginTypes {
debug {
loaderApkConfig = new Tuple2('sample-loader-debug.apk', ':sample-loader:assembleDebug')
runtimeApkConfig = new Tuple2('sample-runtime-debug.apk', ':sample-runtime:assembleDebug')
pluginApks {
pluginApk1 {
businessName = 'sample-plugin'//businessName相同的插件,context获取的Dir是相同的。businessName留空,表示和宿主相同业务,直接使用宿主的Dir。
partKey = 'sample-plugin'
buildTask = 'assemblePluginDebug'
apkName = 'plugin-app-plugin-debug.apk'
apkPath = 'plugin-app/build/outputs/apk/plugin/debug/plugin-app-plugin-debug.apk'
}
}
}
release {
loaderApkConfig = new Tuple2('sample-loader-release.apk', ':sample-loader:assembleRelease')
runtimeApkConfig = new Tuple2('sample-runtime-release.apk', ':sample-runtime:assembleRelease')
pluginApks {
pluginApk1 {
businessName = 'demo'
partKey = 'sample-plugin'
buildTask = 'assemblePluginRelease'
apkName = 'plugin-app-plugin-release.apk'
apkPath = 'plugin-app/build/outputs/apk/plugin/release/plugin-app-plugin-release.apk'
}
}
}
}
loaderApkProjectPath = 'sample-loader'
runtimeApkProjectPath = 'sample-runtime'
version = 4
compactVersion = [1, 2, 3]
uuidNickName = "1.1.5"
}
}
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/src/main/aidl/com/tencent/shadow/sample/plugin/IMyAidlInterface.aidl
================================================
// IMyAidlInterface.aidl
package com.tencent.shadow.sample.plugin;
// Declare any non-default types here with import statements
interface IMyAidlInterface {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
String basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/src/main/java/com/tencent/shadow/sample/plugin/MainActivity.java
================================================
package com.tencent.shadow.sample.plugin;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/src/main/java/com/tencent/shadow/sample/plugin/MyService.java
================================================
package com.tencent.shadow.sample.plugin;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
public class MyService extends Service {
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
return new IMyAidlInterface.Stub() {
@Override
public String basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
return Integer.toString(anInt) + aLong + aBoolean + aFloat + aDouble + aString;
}
};
}
}
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/src/main/res/values/colors.xml
================================================
#008577
#00574B
#D81B60
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/src/main/res/values/strings.xml
================================================
Shadow Sample Plugin
================================================
FILE: projects/sample/maven/plugin-project/plugin-app/src/main/res/values/styles.xml
================================================
================================================
FILE: projects/sample/maven/plugin-project/sample-loader/.gitignore
================================================
/build
================================================
FILE: projects/sample/maven/plugin-project/sample-loader/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
defaultConfig {
applicationId "com.tencent.shadow.sample.loader"//applicationId不重要
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
}
dependencies {
implementation "com.tencent.shadow.dynamic:loader-impl:$shadow_version"
compileOnly "com.tencent.shadow.core:activity-container:$shadow_version"
compileOnly "com.tencent.shadow.core:common:$shadow_version"
//下面这行依赖是为了防止在proguard的时候找不到LoaderFactory接口
compileOnly "com.tencent.shadow.dynamic:host:$shadow_version"
}
================================================
FILE: projects/sample/maven/plugin-project/sample-loader/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#kotlin一般性配置 START
-dontwarn kotlin.**
-keepclassmembers class **$WhenMappings {
;
}
-keepclassmembers class kotlin.Metadata {
public ;
}
#kotlin一般性配置 END
#kotlin优化性能 START
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
#kotlin优化性能 END
-keep class org.slf4j.**{*;}
-dontwarn org.slf4j.impl.**
-keep class com.tencent.shadow.dynamic.host.**{*;}
-keep class com.tencent.shadow.dynamic.impl.**{*;}
-keep class com.tencent.shadow.dynamic.loader.**{*;}
-keep class com.tencent.shadow.core.common.**{*;}
-keep class com.tencent.shadow.core.loader.**{*;}
-keep class com.tencent.shadow.core.runtime.**{*;}
-dontwarn com.tencent.shadow.dynamic.host.**
-dontwarn com.tencent.shadow.dynamic.impl.**
-dontwarn com.tencent.shadow.dynamic.loader.**
-dontwarn com.tencent.shadow.core.common.**
-dontwarn com.tencent.shadow.core.loader.**
-dontwarn module-info
================================================
FILE: projects/sample/maven/plugin-project/sample-loader/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/maven/plugin-project/sample-loader/src/main/java/com/tencent/shadow/dynamic/loader/impl/CoreLoaderFactoryImpl.java
================================================
package com.tencent.shadow.dynamic.loader.impl;
import android.content.Context;
import com.tencent.shadow.core.loader.ShadowPluginLoader;
import com.tencent.shadow.sample.loader.SamplePluginLoader;
import org.jetbrains.annotations.NotNull;
/**
* 这个类的包名类名是固定的。
*
* 见com.tencent.shadow.dynamic.loader.impl.DynamicPluginLoader#CORE_LOADER_FACTORY_IMPL_NAME
*/
public class CoreLoaderFactoryImpl implements CoreLoaderFactory {
@NotNull
@Override
public ShadowPluginLoader build(@NotNull Context context) {
return new SamplePluginLoader(context);
}
}
================================================
FILE: projects/sample/maven/plugin-project/sample-loader/src/main/java/com/tencent/shadow/sample/loader/SampleComponentManager.java
================================================
package com.tencent.shadow.sample.loader;
import android.content.ComponentName;
import android.content.Context;
import com.tencent.shadow.core.loader.infos.ContainerProviderInfo;
import com.tencent.shadow.core.loader.managers.ComponentManager;
import java.util.ArrayList;
import java.util.List;
public class SampleComponentManager extends ComponentManager {
/**
* sample-runtime 模块中定义的壳子Activity,需要在宿主AndroidManifest.xml注册
*/
private static final String DEFAULT_ACTIVITY = "com.tencent.shadow.sample.runtime.PluginDefaultProxyActivity";
private static final String SINGLE_INSTANCE_ACTIVITY = "com.tencent.shadow.sample.runtime.PluginSingleInstance1ProxyActivity";
private static final String SINGLE_TASK_ACTIVITY = "com.tencent.shadow.sample.runtime.PluginSingleTask1ProxyActivity";
private Context context;
public SampleComponentManager(Context context) {
this.context = context;
}
/**
* 配置插件Activity 到 壳子Activity的对应关系
*
* @param pluginActivity 插件Activity
* @return 壳子Activity
*/
@Override
public ComponentName onBindContainerActivity(ComponentName pluginActivity) {
switch (pluginActivity.getClassName()) {
/**
* 这里配置对应的对应关系
*/
}
return new ComponentName(context, DEFAULT_ACTIVITY);
}
/**
* 配置对应宿主中预注册的壳子contentProvider的信息
*/
@Override
public ContainerProviderInfo onBindContainerContentProvider(ComponentName pluginContentProvider) {
return new ContainerProviderInfo(
"com.tencent.shadow.runtime.container.PluginContainerContentProvider",
"com.tencent.shadow.contentprovider.authority.dynamic");
}
}
================================================
FILE: projects/sample/maven/plugin-project/sample-loader/src/main/java/com/tencent/shadow/sample/loader/SamplePluginLoader.java
================================================
package com.tencent.shadow.sample.loader;
import android.content.Context;
import com.tencent.shadow.core.loader.ShadowPluginLoader;
import com.tencent.shadow.core.loader.managers.ComponentManager;
import com.tencent.shadow.sample.loader.SampleComponentManager;
/**
* 这里的类名和包名需要固定
* com.tencent.shadow.sdk.pluginloader.PluginLoaderImpl
*/
public class SamplePluginLoader extends ShadowPluginLoader {
private final static String TAG = "shadow";
private ComponentManager componentManager;
public SamplePluginLoader(Context hostAppContext) {
super(hostAppContext);
componentManager = new SampleComponentManager(hostAppContext);
}
@Override
public ComponentManager getComponentManager() {
return componentManager;
}
}
================================================
FILE: projects/sample/maven/plugin-project/sample-runtime/.gitignore
================================================
/build
================================================
FILE: projects/sample/maven/plugin-project/sample-runtime/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
defaultConfig {
applicationId "com.tencent.shadow.sample.runtime"//applicationId不重要
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
}
dependencies {
implementation "com.tencent.shadow.core:activity-container:$shadow_version"
}
================================================
FILE: projects/sample/maven/plugin-project/sample-runtime/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class org.slf4j.**{*;}
-dontwarn org.slf4j.impl.**
-keep class com.tencent.shadow.core.runtime.**{*;}
#需要keep在宿主AndroidManifest.xml注册的壳子activity
-keep class com.tencent.shadow.sample.runtime.**{*;}
#GeneratedPluginContainerActivity包含新版本API的接口,可能在业务编译时使用的低版本compileSDK中找不到
-dontwarn com.tencent.shadow.core.runtime.container.GeneratedPluginContainerActivity
================================================
FILE: projects/sample/maven/plugin-project/sample-runtime/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/maven/plugin-project/sample-runtime/src/main/java/com/tencent/shadow/sample/runtime/PluginDefaultProxyActivity.java
================================================
package com.tencent.shadow.sample.runtime;
import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
public class PluginDefaultProxyActivity extends PluginContainerActivity {
}
================================================
FILE: projects/sample/maven/plugin-project/sample-runtime/src/main/java/com/tencent/shadow/sample/runtime/PluginSingleInstance1ProxyActivity.java
================================================
package com.tencent.shadow.sample.runtime;
import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
public class PluginSingleInstance1ProxyActivity extends PluginContainerActivity {
}
================================================
FILE: projects/sample/maven/plugin-project/sample-runtime/src/main/java/com/tencent/shadow/sample/runtime/PluginSingleTask1ProxyActivity.java
================================================
package com.tencent.shadow.sample.runtime;
import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
public class PluginSingleTask1ProxyActivity extends PluginContainerActivity {
}
================================================
FILE: projects/sample/maven/plugin-project/settings.gradle
================================================
include ':plugin-app'
include ':sample-runtime'
include ':sample-loader'
================================================
FILE: projects/sample/source/sample-constant/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-constant/build.gradle
================================================
apply plugin: 'com.android.library'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
================================================
FILE: projects/sample/source/sample-constant/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: projects/sample/source/sample-constant/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-constant/src/main/java/com/tencent/shadow/sample/constant/Constant.java
================================================
/*
* 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.sample.constant;
final public class Constant {
public static final String KEY_PLUGIN_ZIP_PATH = "pluginZipPath";
public static final String KEY_ACTIVITY_CLASSNAME = "KEY_ACTIVITY_CLASSNAME";
public static final String KEY_EXTRAS = "KEY_EXTRAS";
public static final String KEY_PLUGIN_PART_KEY = "KEY_PLUGIN_PART_KEY";
public static final String PART_KEY_PLUGIN_MAIN_APP = "sample-plugin-app";
public static final String PART_KEY_PLUGIN_ANOTHER_APP = "sample-plugin-app2";
public static final String PART_KEY_PLUGIN_BASE = "sample-base";
public static final int FROM_ID_NOOP = 1000;
public static final int FROM_ID_START_ACTIVITY = 1002;
public static final int FROM_ID_CLOSE = 1003;
public static final int FROM_ID_LOAD_VIEW_TO_HOST = 1004;
}
================================================
FILE: projects/sample/source/sample-host/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-host/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
applicationId project.SAMPLE_HOST_APP_APPLICATION_ID
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
testInstrumentationRunner "com.tencent.shadow.test.CustomAndroidJUnitRunner"
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
sourceSets {
debug {
assets.srcDir('build/generated/assets/sample-manager/debug/')
assets.srcDir('build/generated/assets/plugin-zip/debug/')
}
release {
assets.srcDir('build/generated/assets/sample-manager/release/')
assets.srcDir('build/generated/assets/plugin-zip/release/')
}
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
}
dependencies {
implementation "commons-io:commons-io:$commons_io_android_version"//sample-host从assets中复制插件用的
implementation "org.slf4j:slf4j-api:$slf4j_version"
implementation 'com.tencent.shadow.core:common'
implementation 'com.tencent.shadow.dynamic:dynamic-host'
implementation project(':sample-constant')
implementation project(':sample-host-lib')
}
def createCopyTask(projectName, buildType, name, apkName, inputFile, taskName) {
def outputFile = file("${getBuildDir()}/generated/assets/${name}/${buildType}/${apkName}")
outputFile.getParentFile().mkdirs()
return tasks.create("copy${buildType.capitalize()}${name.capitalize()}Task", Copy) {
group = 'build'
description = "复制${name}到assets中."
from(inputFile.getParent()) {
include(inputFile.name)
rename { outputFile.name }
}
into(outputFile.getParent())
}.dependsOn("${projectName}:${taskName}")
}
def generateAssets(generateAssetsTask, buildType) {
def moduleName = 'sample-manager'
def pluginManagerApkFile = file(
"${project(":sample-manager").getBuildDir()}" +
"/outputs/apk/${buildType}/" +
"${moduleName}-${buildType}.apk"
)
generateAssetsTask.dependsOn createCopyTask(
':sample-manager',
buildType,
moduleName,
'pluginmanager.apk',
pluginManagerApkFile,
"assemble${buildType.capitalize()}"
)
def pluginZip = file("${getRootProject().getBuildDir()}/plugin-${buildType}.zip")
generateAssetsTask.dependsOn createCopyTask(
':sample-app',
buildType,
'plugin-zip',
"plugin-${buildType}.zip",
pluginZip,
"package${buildType.capitalize()}Plugin"
)
}
tasks.whenTaskAdded { task ->
if (task.name == "generateDebugAssets") {
generateAssets(task, 'debug')
}
if (task.name == "generateReleaseAssets") {
generateAssets(task, 'release')
}
}
================================================
FILE: projects/sample/source/sample-host/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class org.slf4j.**{*;}
-dontwarn org.slf4j.impl.**
-keep class com.tencent.shadow.dynamic.host.**{*;}
-keep class com.tencent.shadow.core.common.**{*;}
-keep class com.tencent.shadow.core.runtime.container.**{*;}
================================================
FILE: projects/sample/source/sample-host/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/AndroidLogLoggerFactory.java
================================================
/*
* 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.sample.host;
import android.util.Log;
import com.tencent.shadow.core.common.ILoggerFactory;
import com.tencent.shadow.core.common.Logger;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class AndroidLogLoggerFactory implements ILoggerFactory {
private static final int LOG_LEVEL_TRACE = 5;
private static final int LOG_LEVEL_DEBUG = 4;
private static final int LOG_LEVEL_INFO = 3;
private static final int LOG_LEVEL_WARN = 2;
private static final int LOG_LEVEL_ERROR = 1;
private static AndroidLogLoggerFactory sInstance = new AndroidLogLoggerFactory();
public static ILoggerFactory getInstance() {
return sInstance;
}
final private ConcurrentMap loggerMap = new ConcurrentHashMap();
public Logger getLogger(String name) {
Logger simpleLogger = loggerMap.get(name);
if (simpleLogger != null) {
return simpleLogger;
} else {
Logger newInstance = new IVLogger(name);
Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
class IVLogger implements Logger {
private String name;
IVLogger(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
private void log(int level, String message, Throwable t) {
final String tag = String.valueOf(name);
switch (level) {
case LOG_LEVEL_TRACE:
case LOG_LEVEL_DEBUG:
if (t == null)
Log.d(tag, message);
else
Log.d(tag, message, t);
break;
case LOG_LEVEL_INFO:
if (t == null)
Log.i(tag, message);
else
Log.i(tag, message, t);
break;
case LOG_LEVEL_WARN:
if (t == null)
Log.w(tag, message);
else
Log.w(tag, message, t);
break;
case LOG_LEVEL_ERROR:
if (t == null)
Log.e(tag, message);
else
Log.e(tag, message, t);
break;
default:
break;
}
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public void trace(String msg) {
log(LOG_LEVEL_TRACE, msg, null);
}
@Override
public void trace(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String msg, Throwable throwable) {
log(LOG_LEVEL_TRACE, msg, throwable);
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public void debug(String msg) {
log(LOG_LEVEL_DEBUG, msg, null);
}
@Override
public void debug(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String msg, Throwable throwable) {
log(LOG_LEVEL_DEBUG, msg, throwable);
}
@Override
public boolean isInfoEnabled() {
return true;
}
@Override
public void info(String msg) {
log(LOG_LEVEL_INFO, msg, null);
}
@Override
public void info(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String msg, Throwable throwable) {
log(LOG_LEVEL_INFO, msg, throwable);
}
@Override
public boolean isWarnEnabled() {
return true;
}
@Override
public void warn(String msg) {
log(LOG_LEVEL_WARN, msg, null);
}
@Override
public void warn(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String msg, Throwable throwable) {
log(LOG_LEVEL_WARN, msg, throwable);
}
@Override
public boolean isErrorEnabled() {
return true;
}
@Override
public void error(String msg) {
log(LOG_LEVEL_ERROR, msg, null);
}
@Override
public void error(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String msg, Throwable throwable) {
log(LOG_LEVEL_ERROR, msg, throwable);
}
}
}
class FormattingTuple {
static public FormattingTuple NULL = new FormattingTuple(null);
private String message;
private Throwable throwable;
private Object[] argArray;
public FormattingTuple(String message) {
this(message, null, null);
}
public FormattingTuple(String message, Object[] argArray, Throwable throwable) {
this.message = message;
this.throwable = throwable;
this.argArray = argArray;
}
public String getMessage() {
return message;
}
public Object[] getArgArray() {
return argArray;
}
public Throwable getThrowable() {
return throwable;
}
}
final class MessageFormatter {
static final char DELIM_START = '{';
static final char DELIM_STOP = '}';
static final String DELIM_STR = "{}";
private static final char ESCAPE_CHAR = '\\';
/**
* Performs single argument substitution for the 'messagePattern' passed as
* parameter.
*
* For example,
*
*
* MessageFormatter.format("Hi {}.", "there");
*
*
* will return the string "Hi there.".
*
*
* @param messagePattern The message pattern which will be parsed and formatted
* @param arg The argument to be substituted in place of the formatting anchor
* @return The formatted message
*/
final public static FormattingTuple format(String messagePattern, Object arg) {
return arrayFormat(messagePattern, new Object[]{arg});
}
/**
* Performs a two argument substitution for the 'messagePattern' passed as
* parameter.
*
* For example,
*
*
* MessageFormatter.format("Hi {}. My name is {}.", "Alice", "Bob");
*
*
* will return the string "Hi Alice. My name is Bob.".
*
* @param messagePattern The message pattern which will be parsed and formatted
* @param arg1 The argument to be substituted in place of the first formatting
* anchor
* @param arg2 The argument to be substituted in place of the second formatting
* anchor
* @return The formatted message
*/
final public static FormattingTuple format(final String messagePattern, Object arg1, Object arg2) {
return arrayFormat(messagePattern, new Object[]{arg1, arg2});
}
static final Throwable getThrowableCandidate(Object[] argArray) {
if (argArray == null || argArray.length == 0) {
return null;
}
final Object lastEntry = argArray[argArray.length - 1];
if (lastEntry instanceof Throwable) {
return (Throwable) lastEntry;
}
return null;
}
final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) {
Throwable throwableCandidate = getThrowableCandidate(argArray);
Object[] args = argArray;
if (throwableCandidate != null) {
args = trimmedCopy(argArray);
}
return arrayFormat(messagePattern, args, throwableCandidate);
}
private static Object[] trimmedCopy(Object[] argArray) {
if (argArray == null || argArray.length == 0) {
throw new IllegalStateException("non-sensical empty or null argument array");
}
final int trimemdLen = argArray.length - 1;
Object[] trimmed = new Object[trimemdLen];
System.arraycopy(argArray, 0, trimmed, 0, trimemdLen);
return trimmed;
}
final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray, Throwable throwable) {
if (messagePattern == null) {
return new FormattingTuple(null, argArray, throwable);
}
if (argArray == null) {
return new FormattingTuple(messagePattern);
}
int i = 0;
int j;
// use string builder for better multicore performance
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
int L;
for (L = 0; L < argArray.length; L++) {
j = messagePattern.indexOf(DELIM_STR, i);
if (j == -1) {
// no more variables
if (i == 0) { // this is a simple string
return new FormattingTuple(messagePattern, argArray, throwable);
} else { // add the tail string which contains no variables and return
// the result.
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
} else {
if (isEscapedDelimeter(messagePattern, j)) {
if (!isDoubleEscaped(messagePattern, j)) {
L--; // DELIM_START was escaped, thus should not be incremented
sbuf.append(messagePattern, i, j - 1);
sbuf.append(DELIM_START);
i = j + 1;
} else {
// The escape character preceding the delimiter start is
// itself escaped: "abc x:\\{}"
// we have to consume one backward slash
sbuf.append(messagePattern, i, j - 1);
deeplyAppendParameter(sbuf, argArray[L], new HashMap());
i = j + 2;
}
} else {
// normal case
sbuf.append(messagePattern, i, j);
deeplyAppendParameter(sbuf, argArray[L], new HashMap());
i = j + 2;
}
}
}
// append the characters following the last {} pair.
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
final static boolean isEscapedDelimeter(String messagePattern, int delimeterStartIndex) {
if (delimeterStartIndex == 0) {
return false;
}
char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1);
if (potentialEscape == ESCAPE_CHAR) {
return true;
} else {
return false;
}
}
final static boolean isDoubleEscaped(String messagePattern, int delimeterStartIndex) {
if (delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR) {
return true;
} else {
return false;
}
}
// special treatment of array values was suggested by 'lizongbo'
private static void deeplyAppendParameter(StringBuilder sbuf, Object o, Map seenMap) {
if (o == null) {
sbuf.append("null");
return;
}
if (!o.getClass().isArray()) {
safeObjectAppend(sbuf, o);
} else {
// check for primitive array types because they
// unfortunately cannot be cast to Object[]
if (o instanceof boolean[]) {
booleanArrayAppend(sbuf, (boolean[]) o);
} else if (o instanceof byte[]) {
byteArrayAppend(sbuf, (byte[]) o);
} else if (o instanceof char[]) {
charArrayAppend(sbuf, (char[]) o);
} else if (o instanceof short[]) {
shortArrayAppend(sbuf, (short[]) o);
} else if (o instanceof int[]) {
intArrayAppend(sbuf, (int[]) o);
} else if (o instanceof long[]) {
longArrayAppend(sbuf, (long[]) o);
} else if (o instanceof float[]) {
floatArrayAppend(sbuf, (float[]) o);
} else if (o instanceof double[]) {
doubleArrayAppend(sbuf, (double[]) o);
} else {
objectArrayAppend(sbuf, (Object[]) o, seenMap);
}
}
}
private static void safeObjectAppend(StringBuilder sbuf, Object o) {
try {
String oAsString = o.toString();
sbuf.append(oAsString);
} catch (Throwable t) {
sbuf.append("[FAILED toString()]");
}
}
private static void objectArrayAppend(StringBuilder sbuf, Object[] a, Map seenMap) {
sbuf.append('[');
if (!seenMap.containsKey(a)) {
seenMap.put(a, null);
final int len = a.length;
for (int i = 0; i < len; i++) {
deeplyAppendParameter(sbuf, a[i], seenMap);
if (i != len - 1)
sbuf.append(", ");
}
// allow repeats in siblings
seenMap.remove(a);
} else {
sbuf.append("...");
}
sbuf.append(']');
}
private static void booleanArrayAppend(StringBuilder sbuf, boolean[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void byteArrayAppend(StringBuilder sbuf, byte[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void charArrayAppend(StringBuilder sbuf, char[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void shortArrayAppend(StringBuilder sbuf, short[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void intArrayAppend(StringBuilder sbuf, int[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void longArrayAppend(StringBuilder sbuf, long[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void floatArrayAppend(StringBuilder sbuf, float[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void doubleArrayAppend(StringBuilder sbuf, double[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/HostApplication.java
================================================
/*
* 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.sample.host;
import static android.os.Process.myPid;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.os.StrictMode;
import android.webkit.WebView;
import com.tencent.shadow.core.common.LoggerFactory;
import com.tencent.shadow.dynamic.host.DynamicRuntime;
import com.tencent.shadow.dynamic.host.PluginManager;
import com.tencent.shadow.sample.host.lib.HostUiLayerProvider;
import com.tencent.shadow.sample.host.manager.Shadow;
import java.io.File;
public class HostApplication extends Application {
private static HostApplication sApp;
private PluginManager mPluginManager;
@Override
public void onCreate() {
super.onCreate();
sApp = this;
detectNonSdkApiUsageOnAndroidP();
setWebViewDataDirectorySuffix();
LoggerFactory.setILoggerFactory(new AndroidLogLoggerFactory());
if (isProcess(this, ":plugin")) {
//在全动态架构中,Activity组件没有打包在宿主而是位于被动态加载的runtime,
//为了防止插件crash后,系统自动恢复crash前的Activity组件,此时由于没有加载runtime而发生classNotFound异常,导致二次crash
//因此这里恢复加载上一次的runtime
DynamicRuntime.recoveryRuntime(this);
}
if (isProcess(this, getPackageName())) {
PluginHelper.getInstance().init(this);
}
HostUiLayerProvider.init(this);
}
private static void setWebViewDataDirectorySuffix() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return;
}
WebView.setDataDirectorySuffix(Application.getProcessName());
}
private static void detectNonSdkApiUsageOnAndroidP() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return;
}
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
builder.detectNonSdkApiUsage();
StrictMode.setVmPolicy(builder.build());
}
public static HostApplication getApp() {
return sApp;
}
public void loadPluginManager(File apk) {
if (mPluginManager == null) {
mPluginManager = Shadow.getPluginManager(apk);
}
}
public PluginManager getPluginManager() {
return mPluginManager;
}
private static boolean isProcess(Context context, String processName) {
String currentProcName = "";
ActivityManager manager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == myPid()) {
currentProcName = processInfo.processName;
break;
}
}
return currentProcName.endsWith(processName);
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/MainActivity.java
================================================
/*
* 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.sample.host;
import static com.tencent.shadow.sample.constant.Constant.PART_KEY_PLUGIN_BASE;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import com.tencent.shadow.sample.constant.Constant;
import com.tencent.shadow.sample.host.plugin_view.HostAddPluginViewActivity;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(R.style.TestHostTheme);
LinearLayout rootView = new LinearLayout(this);
rootView.setOrientation(LinearLayout.VERTICAL);
TextView infoTextView = new TextView(this);
infoTextView.setText(R.string.main_activity_info);
rootView.addView(infoTextView);
final Spinner partKeySpinner = new Spinner(this);
ArrayAdapter partKeysAdapter = new ArrayAdapter<>(this, R.layout.part_key_adapter);
partKeysAdapter.addAll(
Constant.PART_KEY_PLUGIN_MAIN_APP,
Constant.PART_KEY_PLUGIN_ANOTHER_APP
);
partKeySpinner.setAdapter(partKeysAdapter);
rootView.addView(partKeySpinner);
Button startPluginButton = new Button(this);
startPluginButton.setText(R.string.start_plugin);
startPluginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String partKey = (String) partKeySpinner.getSelectedItem();
Intent intent = new Intent(MainActivity.this, PluginLoadActivity.class);
switch (partKey) {
//为了演示多进程多插件,其实两个插件内容完全一样,除了所在进程
case Constant.PART_KEY_PLUGIN_MAIN_APP:
intent.putExtra(Constant.KEY_PLUGIN_PART_KEY, PART_KEY_PLUGIN_BASE);
break;
case Constant.PART_KEY_PLUGIN_ANOTHER_APP:
intent.putExtra(Constant.KEY_PLUGIN_PART_KEY, partKey);
;
break;
}
switch (partKey) {
//为了演示多进程多插件,其实两个插件内容完全一样,除了所在进程
case Constant.PART_KEY_PLUGIN_MAIN_APP:
case Constant.PART_KEY_PLUGIN_ANOTHER_APP:
intent.putExtra(Constant.KEY_ACTIVITY_CLASSNAME, "com.tencent.shadow.sample.plugin.app.lib.gallery.splash.SplashActivity");
break;
}
startActivity(intent);
}
});
rootView.addView(startPluginButton);
Button startHostAddPluginViewActivityButton = new Button(this);
startHostAddPluginViewActivityButton.setText("宿主添加插件View");
startHostAddPluginViewActivityButton.setOnClickListener(v -> {
Intent intent = new Intent(this, HostAddPluginViewActivity.class);
startActivity(intent);
});
rootView.addView(startHostAddPluginViewActivityButton);
setContentView(rootView);
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/Plugin2ProcessPPS.java
================================================
/*
* 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.sample.host;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.util.Log;
import com.tencent.shadow.dynamic.host.PluginProcessService;
import com.tencent.shadow.sample.host.lib.LoadPluginCallback;
public class Plugin2ProcessPPS extends PluginProcessService {
public Plugin2ProcessPPS() {
LoadPluginCallback.setCallback(new LoadPluginCallback.Callback() {
@Override
public void beforeLoadPlugin(String partKey) {
Log.d("Plugin2ProcessPPS", "beforeLoadPlugin(" + partKey + ")");
}
@Override
public void afterLoadPlugin(String partKey, ApplicationInfo applicationInfo, ClassLoader pluginClassLoader, Resources pluginResources) {
Log.d("Plugin2ProcessPPS", "afterLoadPlugin(" + partKey + "," + applicationInfo.className + "{metaData=" + applicationInfo.metaData + "}" + "," + pluginClassLoader + ")");
}
});
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/PluginHelper.java
================================================
/*
* 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.sample.host;
import android.content.Context;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PluginHelper {
/**
* 动态加载的插件管理apk
*/
public final static String sPluginManagerName = "pluginmanager.apk";
/**
* 动态加载的插件包,里面包含以下几个部分,插件apk,插件框架apk(loader apk和runtime apk), apk信息配置关系json文件
*/
public final static String sPluginZip = BuildConfig.DEBUG ? "plugin-debug.zip" : "plugin-release.zip";
public File pluginManagerFile;
public File pluginZipFile;
public ExecutorService singlePool = Executors.newSingleThreadExecutor();
private Context mContext;
private static PluginHelper sInstance = new PluginHelper();
public static PluginHelper getInstance() {
return sInstance;
}
private PluginHelper() {
}
public void init(Context context) {
pluginManagerFile = new File(context.getFilesDir(), sPluginManagerName);
pluginZipFile = new File(context.getFilesDir(), sPluginZip);
mContext = context.getApplicationContext();
singlePool.execute(new Runnable() {
@Override
public void run() {
preparePlugin();
}
});
}
private void preparePlugin() {
try {
//noinspection ResultOfMethodCallIgnored
pluginManagerFile.setWritable(true);
InputStream is = mContext.getAssets().open(sPluginManagerName);
FileUtils.copyInputStreamToFile(is, pluginManagerFile);
InputStream zip = mContext.getAssets().open(sPluginZip);
FileUtils.copyInputStreamToFile(zip, pluginZipFile);
} catch (IOException e) {
throw new RuntimeException("从assets中复制apk出错", e);
}
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/PluginLoadActivity.java
================================================
/*
* 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.sample.host;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.ViewGroup;
import com.tencent.shadow.dynamic.host.EnterCallback;
import com.tencent.shadow.sample.constant.Constant;
public class PluginLoadActivity extends Activity {
private ViewGroup mViewGroup;
private Handler mHandler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_load);
mViewGroup = findViewById(R.id.container);
startPlugin();
}
public void startPlugin() {
PluginHelper.getInstance().singlePool.execute(new Runnable() {
@Override
public void run() {
HostApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);
Bundle bundle = new Bundle();
bundle.putString(Constant.KEY_PLUGIN_ZIP_PATH, PluginHelper.getInstance().pluginZipFile.getAbsolutePath());
bundle.putString(Constant.KEY_PLUGIN_PART_KEY, getIntent().getStringExtra(Constant.KEY_PLUGIN_PART_KEY));
bundle.putString(Constant.KEY_ACTIVITY_CLASSNAME, getIntent().getStringExtra(Constant.KEY_ACTIVITY_CLASSNAME));
HostApplication.getApp().getPluginManager()
.enter(PluginLoadActivity.this, Constant.FROM_ID_START_ACTIVITY, bundle, new EnterCallback() {
@Override
public void onShowLoadingView(final View view) {
mHandler.post(new Runnable() {
@Override
public void run() {
mViewGroup.addView(view);
}
});
}
@Override
public void onCloseLoadingView() {
finish();
}
@Override
public void onEnterComplete() {
}
});
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
HostApplication.getApp().getPluginManager().enter(this, Constant.FROM_ID_CLOSE, null, null);
mViewGroup.removeAllViews();
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/PluginProcessPPS.java
================================================
/*
* 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.sample.host;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.util.Log;
import com.tencent.shadow.dynamic.host.PluginProcessService;
import com.tencent.shadow.sample.host.lib.LoadPluginCallback;
public class PluginProcessPPS extends PluginProcessService {
public PluginProcessPPS() {
LoadPluginCallback.setCallback(new LoadPluginCallback.Callback() {
@Override
public void beforeLoadPlugin(String partKey) {
Log.d("PluginProcessPPS", "beforeLoadPlugin(" + partKey + ")");
}
@Override
public void afterLoadPlugin(String partKey, ApplicationInfo applicationInfo, ClassLoader pluginClassLoader, Resources pluginResources) {
Log.d("PluginProcessPPS", "afterLoadPlugin(" + partKey + "," + applicationInfo.className + "{metaData=" + applicationInfo.metaData + "}" + "," + pluginClassLoader + ")");
}
});
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/manager/FixedPathPmUpdater.java
================================================
/*
* 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.sample.host.manager;
import android.os.Build;
import com.tencent.shadow.dynamic.host.PluginManagerUpdater;
import java.io.File;
import java.util.concurrent.Future;
public class FixedPathPmUpdater implements PluginManagerUpdater {
final private File apk;
FixedPathPmUpdater(File apk) {
this.apk = apk;
//在API 33以上的系统上,禁止动态加载文件可写入,满足系统安全限制
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
//noinspection ResultOfMethodCallIgnored
apk.setWritable(false);
}
}
@Override
public boolean wasUpdating() {
return false;
}
@Override
public Future update() {
return null;
}
@Override
public File getLatest() {
return apk;
}
@Override
public Future isAvailable(final File file) {
return null;
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/manager/Shadow.java
================================================
/*
* 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.sample.host.manager;
import com.tencent.shadow.dynamic.host.DynamicPluginManager;
import com.tencent.shadow.dynamic.host.PluginManager;
import java.io.File;
public class Shadow {
public static PluginManager getPluginManager(File apk) {
final FixedPathPmUpdater fixedPathPmUpdater = new FixedPathPmUpdater(apk);
File tempPm = fixedPathPmUpdater.getLatest();
if (tempPm != null) {
return new DynamicPluginManager(fixedPathPmUpdater);
}
return null;
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/plugin_view/HostAddPluginViewActivity.java
================================================
package com.tencent.shadow.sample.host.plugin_view;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.tencent.shadow.sample.host.lib.HostAddPluginViewContainer;
import com.tencent.shadow.sample.host.lib.HostAddPluginViewContainerHolder;
public class HostAddPluginViewActivity extends Activity implements HostAddPluginViewContainer {
private ViewGroup mPluginViewContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout activityContentView = new LinearLayout(this);
activityContentView.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams wrapContent = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
TextView note = new TextView(this);
note.setLayoutParams(wrapContent);
note.setText("需要先启动插件sample-plugin-app后,才能点下面的加载插件View");
Button loadButton = new Button(this);
loadButton.setText("加载插件View");
loadButton.setOnClickListener(this::loadPluginView);
loadButton.setLayoutParams(wrapContent);
ViewGroup pluginViewContainer = new LinearLayout(this);
pluginViewContainer.setLayoutParams(wrapContent);
mPluginViewContainer = pluginViewContainer;
View[] views = {
note,
loadButton,
pluginViewContainer
};
for (View view : views) {
activityContentView.addView(view);
}
setContentView(activityContentView);
}
private void loadPluginView(View view) {
//简化逻辑,只允许点一次
view.setEnabled(false);
//因为当前Activity和插件都在:plugin进程,不能直接操作主进程的manager对象,所以通过一个广播调用manager。
Intent intent = new Intent();
intent.setPackage(getPackageName());
intent.setAction("sample_host.manager.startPluginService");
final int id = System.identityHashCode(this);
HostAddPluginViewContainerHolder.instances.put(id, this);
intent.putExtra("id", id);
sendBroadcast(intent);
}
@Override
public void addView(View view) {
mPluginViewContainer.addView(view);
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/java/com/tencent/shadow/sample/host/plugin_view/MainProcessManagerReceiver.java
================================================
package com.tencent.shadow.sample.host.plugin_view;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.tencent.shadow.sample.constant.Constant;
import com.tencent.shadow.sample.host.HostApplication;
public class MainProcessManagerReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
HostApplication.getApp().getPluginManager()
.enter(context, Constant.FROM_ID_LOAD_VIEW_TO_HOST, intent.getExtras(), null);
}
}
================================================
FILE: projects/sample/source/sample-host/src/main/res/layout/activity_jump_to_plugin.xml
================================================
================================================
FILE: projects/sample/source/sample-host/src/main/res/layout/activity_load.xml
================================================
================================================
FILE: projects/sample/source/sample-host/src/main/res/layout/part_key_adapter.xml
================================================
================================================
FILE: projects/sample/source/sample-host/src/main/res/values/strings.xml
================================================
这是一个全动态的测试程序,插件管理(test-dynamic-manager),
插件框架(test-dynamic-loader及test-dynamic-runtime),
以及插件本身,都是动态加载的
启动插件
================================================
FILE: projects/sample/source/sample-host/src/main/res/values/themes.xml
================================================
================================================
FILE: projects/sample/source/sample-host-lib/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-host-lib/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'com.tencent.shadow.internal.aar-to-jar'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
consumerProguardFiles 'sample-host-lib.pro'
}
}
}
================================================
FILE: projects/sample/source/sample-host-lib/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: projects/sample/source/sample-host-lib/sample-host-lib.pro
================================================
#让宿主在打包时能够keep住插件要使用到的类名和方法
-keep class com.tencent.shadow.sample.host.lib.*{
public *;
}
================================================
FILE: projects/sample/source/sample-host-lib/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-host-lib/src/main/java/com/tencent/shadow/sample/host/lib/HostAddPluginViewContainer.java
================================================
package com.tencent.shadow.sample.host.lib;
import android.view.View;
public interface HostAddPluginViewContainer {
void addView(View view);
}
================================================
FILE: projects/sample/source/sample-host-lib/src/main/java/com/tencent/shadow/sample/host/lib/HostAddPluginViewContainerHolder.java
================================================
package com.tencent.shadow.sample.host.lib;
import java.util.HashMap;
import java.util.Map;
public class HostAddPluginViewContainerHolder {
public final static Map instances = new HashMap<>();
}
================================================
FILE: projects/sample/source/sample-host-lib/src/main/java/com/tencent/shadow/sample/host/lib/HostUiLayerProvider.java
================================================
/*
* 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.sample.host.lib;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
/**
* 这是一个将要打包到宿主中的类。原本的目的是宿主依赖插件,宿主
*/
public class HostUiLayerProvider {
private static HostUiLayerProvider sInstance;
public static void init(Context mHostApplicationContext) {
sInstance = new HostUiLayerProvider(mHostApplicationContext);
}
public static HostUiLayerProvider getInstance() {
return sInstance;
}
final private Context mHostApplicationContext;
private HostUiLayerProvider(Context mHostApplicationContext) {
this.mHostApplicationContext = mHostApplicationContext;
}
public View buildHostUiLayer() {
return LayoutInflater.from(mHostApplicationContext)
.inflate(R.layout.host_ui_layer_layout, null, false);
}
}
================================================
FILE: projects/sample/source/sample-host-lib/src/main/java/com/tencent/shadow/sample/host/lib/LoadPluginCallback.java
================================================
package com.tencent.shadow.sample.host.lib;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
public class LoadPluginCallback {
private static Callback sCallback;
public static void setCallback(Callback callback) {
sCallback = callback;
}
public static Callback getCallback() {
return sCallback;
}
public interface Callback {
void beforeLoadPlugin(String partKey);
void afterLoadPlugin(String partKey, ApplicationInfo applicationInfo, ClassLoader pluginClassLoader, Resources pluginResources);
}
}
================================================
FILE: projects/sample/source/sample-host-lib/src/main/res/layout/host_ui_layer_layout.xml
================================================
================================================
FILE: projects/sample/source/sample-manager/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-manager/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
applicationId project.SAMPLE_HOST_APP_APPLICATION_ID
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
lintOptions {
abortOnError false
}
}
dependencies {
implementation 'com.tencent.shadow.dynamic:dynamic-manager'
implementation 'com.tencent.shadow.core:manager'
implementation 'com.tencent.shadow.dynamic:dynamic-loader'
implementation project(':sample-constant')
compileOnly 'com.tencent.shadow.core:common'
compileOnly 'com.tencent.shadow.dynamic:dynamic-host'
}
================================================
FILE: projects/sample/source/sample-manager/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class org.slf4j.**{*;}
-keep class com.tencent.shadow.dynamic.impl.**{*;}
-keep class com.tencent.shadow.dynamic.loader.**{*;}
================================================
FILE: projects/sample/source/sample-manager/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-manager/src/main/java/com/tencent/shadow/dynamic/impl/ManagerFactoryImpl.java
================================================
/*
* 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.dynamic.impl;
import android.content.Context;
import com.tencent.shadow.dynamic.host.ManagerFactory;
import com.tencent.shadow.dynamic.host.PluginManagerImpl;
import com.tencent.shadow.sample.manager.SamplePluginManager;
/**
* 此类包名及类名固定
*/
public final class ManagerFactoryImpl implements ManagerFactory {
@Override
public PluginManagerImpl buildManager(Context context) {
return new SamplePluginManager(context);
}
}
================================================
FILE: projects/sample/source/sample-manager/src/main/java/com/tencent/shadow/dynamic/impl/WhiteList.java
================================================
/*
* 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.dynamic.impl;
/**
* 此类包名及类名固定
* classLoader的白名单
* PluginManager可以加载宿主中位于白名单内的类
*/
public interface WhiteList {
String[] sWhiteList = new String[]
{
"com.tencent.host.shadow",
"com.tencent.shadow.test.lib.constant",
};
}
================================================
FILE: projects/sample/source/sample-manager/src/main/java/com/tencent/shadow/sample/manager/FastPluginManager.java
================================================
/*
* 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.sample.manager;
import android.content.Context;
import android.os.RemoteException;
import android.util.Pair;
import com.tencent.shadow.core.common.Logger;
import com.tencent.shadow.core.common.LoggerFactory;
import com.tencent.shadow.core.manager.installplugin.InstalledPlugin;
import com.tencent.shadow.core.manager.installplugin.InstalledType;
import com.tencent.shadow.core.manager.installplugin.PluginConfig;
import com.tencent.shadow.dynamic.host.FailedException;
import com.tencent.shadow.dynamic.manager.PluginManagerThatUseDynamicLoader;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public abstract class FastPluginManager extends PluginManagerThatUseDynamicLoader {
private static final Logger mLogger = LoggerFactory.getLogger(FastPluginManager.class);
private ExecutorService mFixedPool = Executors.newFixedThreadPool(4);
public FastPluginManager(Context context) {
super(context);
}
public InstalledPlugin installPlugin(String zip, String hash, boolean odex) throws IOException, JSONException, InterruptedException, ExecutionException {
final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
final String uuid = pluginConfig.UUID;
List futures = new LinkedList<>();
List>> extractSoFutures = new LinkedList<>();
if (pluginConfig.runTime != null && pluginConfig.pluginLoader != null) {
Future odexRuntime = mFixedPool.submit(new Callable() {
@Override
public Object call() throws Exception {
oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME,
pluginConfig.runTime.file);
return null;
}
});
futures.add(odexRuntime);
Future odexLoader = mFixedPool.submit(new Callable() {
@Override
public Object call() throws Exception {
oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER,
pluginConfig.pluginLoader.file);
return null;
}
});
futures.add(odexLoader);
}
for (Map.Entry plugin : pluginConfig.plugins.entrySet()) {
final String partKey = plugin.getKey();
final File apkFile = plugin.getValue().file;
Future> extractSo = mFixedPool.submit(() -> extractSo(uuid, partKey, apkFile));
futures.add(extractSo);
extractSoFutures.add(extractSo);
if (odex) {
Future odexPlugin = mFixedPool.submit(new Callable() {
@Override
public Object call() throws Exception {
oDexPlugin(uuid, partKey, apkFile);
return null;
}
});
futures.add(odexPlugin);
}
}
for (Future future : futures) {
future.get();
}
Map soDirMap = new HashMap<>();
for (Future> future : extractSoFutures) {
Pair pair = future.get();
soDirMap.put(pair.first, pair.second);
}
onInstallCompleted(pluginConfig, soDirMap);
return getInstalledPlugins(1).get(0);
}
protected void callApplicationOnCreate(String partKey) throws RemoteException {
Map map = mPluginLoader.getLoadedPlugin();
Boolean isCall = (Boolean) map.get(partKey);
if (isCall == null || !isCall) {
mPluginLoader.callApplicationOnCreate(partKey);
}
}
private void loadPluginLoaderAndRuntime(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
if (mPpsController == null) {
bindPluginProcessService(getPluginProcessServiceName(partKey));
waitServiceConnected(10, TimeUnit.SECONDS);
}
loadRunTime(uuid);
loadPluginLoader(uuid);
}
protected void loadPlugin(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
loadPluginLoaderAndRuntime(uuid, partKey);
Map map = mPluginLoader.getLoadedPlugin();
if (!map.containsKey(partKey)) {
mPluginLoader.loadPlugin(partKey);
}
}
protected abstract String getPluginProcessServiceName(String partKey);
}
================================================
FILE: projects/sample/source/sample-manager/src/main/java/com/tencent/shadow/sample/manager/SamplePluginManager.java
================================================
/*
* 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.sample.manager;
import static com.tencent.shadow.sample.constant.Constant.PART_KEY_PLUGIN_ANOTHER_APP;
import static com.tencent.shadow.sample.constant.Constant.PART_KEY_PLUGIN_BASE;
import static com.tencent.shadow.sample.constant.Constant.PART_KEY_PLUGIN_MAIN_APP;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.RemoteException;
import android.view.LayoutInflater;
import android.view.View;
import com.tencent.shadow.core.manager.installplugin.InstalledPlugin;
import com.tencent.shadow.dynamic.host.EnterCallback;
import com.tencent.shadow.sample.constant.Constant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SamplePluginManager extends FastPluginManager {
private ExecutorService executorService = Executors.newSingleThreadExecutor();
private Context mCurrentContext;
public SamplePluginManager(Context context) {
super(context);
mCurrentContext = context;
}
/**
* @return PluginManager实现的别名,用于区分不同PluginManager实现的数据存储路径
*/
@Override
protected String getName() {
return "test-dynamic-manager";
}
/**
* @return 宿主中注册的PluginProcessService实现的类名
*/
@Override
protected String getPluginProcessServiceName(String partKey) {
if (PART_KEY_PLUGIN_MAIN_APP.equals(partKey)) {
return "com.tencent.shadow.sample.host.PluginProcessPPS";
} else if (PART_KEY_PLUGIN_BASE.equals(partKey)) {
return "com.tencent.shadow.sample.host.PluginProcessPPS";
} else if (PART_KEY_PLUGIN_ANOTHER_APP.equals(partKey)) {
return "com.tencent.shadow.sample.host.Plugin2ProcessPPS";//在这里支持多个插件
} else {
//如果有默认PPS,可用return代替throw
throw new IllegalArgumentException("unexpected plugin load request: " + partKey);
}
}
@Override
public void enter(final Context context, long fromId, Bundle bundle, final EnterCallback callback) {
if (fromId == Constant.FROM_ID_NOOP) {
//do nothing.
} else if (fromId == Constant.FROM_ID_START_ACTIVITY) {
onStartActivity(context, bundle, callback);
} else if (fromId == Constant.FROM_ID_CLOSE) {
close();
} else if (fromId == Constant.FROM_ID_LOAD_VIEW_TO_HOST) {
loadViewToHost(context, bundle);
} else {
throw new IllegalArgumentException("不认识的fromId==" + fromId);
}
}
private void loadViewToHost(final Context context, Bundle bundle) {
Intent pluginIntent = new Intent();
pluginIntent.setClassName(
context.getPackageName(),
"com.tencent.shadow.sample.plugin.app.lib.usecases.service.HostAddPluginViewService"
);
pluginIntent.putExtras(bundle);
try {
mPluginLoader.startPluginService(pluginIntent);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
final String pluginZipPath = bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);
final String partKey = bundle.getString(Constant.KEY_PLUGIN_PART_KEY);
final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
if (className == null) {
throw new NullPointerException("className == null");
}
final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);
if (callback != null) {
final View view = LayoutInflater.from(mCurrentContext).inflate(R.layout.activity_load_plugin, null);
callback.onShowLoadingView(view);
}
executorService.execute(new Runnable() {
@Override
public void run() {
try {
InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);
loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_MAIN_APP);
callApplicationOnCreate(PART_KEY_PLUGIN_BASE);
callApplicationOnCreate(PART_KEY_PLUGIN_MAIN_APP);
Intent pluginIntent = new Intent();
pluginIntent.setClassName(
context.getPackageName(),
className
);
if (extras != null) {
pluginIntent.replaceExtras(extras);
}
Intent intent = mPluginLoader.convertActivityIntent(pluginIntent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mPluginLoader.startActivityInPluginProcess(intent);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (callback != null) {
callback.onCloseLoadingView();
}
}
});
}
}
================================================
FILE: projects/sample/source/sample-manager/src/main/res/layout/activity_load_plugin.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-plugin/sample-app/build.gradle
================================================
buildscript {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath 'com.tencent.shadow.core:runtime'
classpath 'com.tencent.shadow.core:activity-container'
classpath 'com.tencent.shadow.core:gradle-plugin'
classpath "org.javassist:javassist:$javassist_version"
}
}
apply plugin: 'com.android.application'
apply plugin: 'com.tencent.shadow.plugin'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
applicationId 'com.tencent.shadow.sample.plugin.app'
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
// 将插件applicationId设置为和宿主相同
productFlavors {
plugin {
applicationId project.SAMPLE_HOST_APP_APPLICATION_ID
}
}
lintOptions {
abortOnError false
}
// 将插件的资源ID分区改为和宿主0x7F不同的值
aaptOptions {
additionalParameters "--package-id", "0x7E", "--allow-reserved-package-id"
}
}
dependencies {
//注意sample-host-lib要用compileOnly编译而不打包在插件中。在packagePlugin任务中配置hostWhiteList允许插件访问宿主的类。
pluginCompileOnly project(":sample-host-lib")
normalImplementation project(":sample-host-lib")
pluginCompileOnly project(":sample-base-lib")
normalImplementation project(":sample-base-lib")
//Shadow Transform后业务代码会有一部分实际引用runtime中的类
//如果不以compileOnly方式依赖,会导致其他Transform或者Proguard找不到这些类
pluginCompileOnly 'com.tencent.shadow.core:runtime'
}
preBuild.dependsOn(":sample-host-lib:jarDebugPackage")
def createDuplicateApkTask(buildType) {
def apkDir = file("${getBuildDir()}/outputs/apk/plugin/$buildType")
return tasks.create("duplicatePlugin${buildType.capitalize()}ApkTask", Copy) {
group = 'build'
description = "复制一个sample-app-plugin-${buildType}.apk用于测试目的"
from(apkDir) {
include("sample-app-plugin-${buildType}.apk")
rename { "sample-app-plugin-${buildType}2.apk" }
}
into(apkDir)
}.dependsOn(":sample-app:assemblePlugin${buildType.capitalize()}")
}
tasks.whenTaskAdded { task ->
if (task.name == "assemblePluginDebug") {
def createTask = createDuplicateApkTask('debug')
task.finalizedBy(createTask)
}
if (task.name == "assemblePluginRelease") {
def createTask = createDuplicateApkTask('release')
task.finalizedBy(createTask)
}
}
shadow {
transform {
// useHostContext = ['abc']
}
packagePlugin {
pluginTypes {
debug {
loaderApkConfig = new Tuple2('sample-loader-debug.apk', ':sample-loader:assembleDebug')
runtimeApkConfig = new Tuple2('sample-runtime-debug.apk', ':sample-runtime:assembleDebug')
pluginApks {
pluginApk1 {
businessName = 'sample-plugin-app'
partKey = 'sample-plugin-app'
buildTask = ':sample-app:assemblePluginDebug'
apkPath = 'projects/sample/source/sample-plugin/sample-app/build/outputs/apk/plugin/debug/sample-app-plugin-debug.apk'
hostWhiteList = ["com.tencent.shadow.sample.host.lib"]
dependsOn = ['sample-base']
}
pluginApk2 {
businessName = 'sample-plugin-app2'
partKey = 'sample-plugin-app2'
buildTask = ':sample-app:assemblePluginDebug'
apkPath = 'projects/sample/source/sample-plugin/sample-app/build/outputs/apk/plugin/debug/sample-app-plugin-debug2.apk'
hostWhiteList = ["com.tencent.shadow.sample.host.lib"]
dependsOn = ['sample-base']
}
sampleBase {
businessName = 'sample-plugin-app'
partKey = 'sample-base'
buildTask = ':sample-base:assemblePluginDebug'
apkPath = 'projects/sample/source/sample-plugin/sample-base/build/outputs/apk/plugin/debug/sample-base-plugin-debug.apk'
hostWhiteList = ["com.tencent.shadow.sample.host.lib"]
}
}
}
release {
loaderApkConfig = new Tuple2('sample-loader-release.apk', ':sample-loader:assembleRelease')
runtimeApkConfig = new Tuple2('sample-runtime-release.apk', ':sample-runtime:assembleRelease')
pluginApks {
pluginApk1 {
businessName = 'sample-plugin-app'
partKey = 'sample-plugin-app'
buildTask = ':sample-app:assemblePluginRelease'
apkPath = 'projects/sample/source/sample-plugin/sample-app/build/outputs/apk/plugin/release/sample-app-plugin-release.apk'
hostWhiteList = ["com.tencent.shadow.sample.host.lib"]
dependsOn = ['sample-base']
}
pluginApk2 {
businessName = 'sample-plugin-app2'
partKey = 'sample-plugin-app2'
buildTask = ':sample-app:assemblePluginRelease'
apkPath = 'projects/sample/source/sample-plugin/sample-app/build/outputs/apk/plugin/release/sample-app-plugin-release2.apk'
hostWhiteList = ["com.tencent.shadow.sample.host.lib"]
dependsOn = ['sample-base']
}
sampleBase {
businessName = 'sample-plugin-app'
partKey = 'sample-base'
buildTask = ':sample-base:assemblePluginRelease'
apkPath = 'projects/sample/source/sample-plugin/sample-base/build/outputs/apk/plugin/release/sample-base-plugin-release.apk'
hostWhiteList = ["com.tencent.shadow.sample.host.lib"]
}
}
}
}
loaderApkProjectPath = 'projects/sample/source/sample-plugin/sample-loader'
runtimeApkProjectPath = 'projects/sample/source/sample-plugin/sample-runtime'
archiveSuffix = System.getenv("PluginSuffix") ?: ""
archivePrefix = 'plugin'
destinationDir = "${getRootProject().getBuildDir()}"
version = 4
compactVersion = [1, 2, 3]
uuidNickName = "1.1.5"
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/cubershi/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
#这是Shadow在编译期将AndroidManifest.xml中所需信息生成的Java类,没有被代码自然引用,所以需要手工keep住。
-keep class com.tencent.shadow.core.manifest_parser.PluginManifest{*;}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/assets/web/test.html
================================================
location.search:
a
b
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/UseCaseApplication.java
================================================
package com.tencent.shadow.sample.plugin.app.lib;
import static com.tencent.shadow.sample.plugin.app.lib.gallery.cases.UseCaseManager.useCases;
import android.app.Application;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.UseCaseManager;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCaseCategory;
import com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityOnCreate;
import com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityOptionMenu;
import com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityOrientation;
import com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityReCreate;
import com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityReCreateBySystem;
import com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivitySetTheme;
import com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityWindowSoftMode;
import com.tencent.shadow.sample.plugin.app.lib.usecases.context.ActivityContextSubDirTestActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.context.ApplicationContextSubDirTestActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.dialog.TestDialogActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.fragment.TestDialogFragmentActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.fragment.TestDynamicFragmentActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.fragment.TestXmlFragmentActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.host_communication.PluginUseHostClassActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.packagemanager.TestPackageManagerActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.provider.TestDBContentProviderActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.provider.TestFileProviderActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.receiver.TestDynamicReceiverActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.receiver.TestReceiverActivity;
import com.tencent.shadow.sample.plugin.app.lib.usecases.webview.WebViewActivity;
public class UseCaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initCase();
}
private static void initCase() {
if (UseCaseManager.sInit) {
throw new RuntimeException("不能重复调用init");
}
UseCaseManager.sInit = true;
UseCaseCategory activityCategory = new UseCaseCategory("Activity测试用例", new UseCase[]{
new TestActivityOnCreate.Case(),
new TestActivityReCreate.Case(),
new TestActivityReCreateBySystem.Case(),
new TestActivityOrientation.Case(),
new TestActivityWindowSoftMode.Case(),
new TestActivitySetTheme.Case(),
new TestActivityOptionMenu.Case(),
new WebViewActivity.Case()
});
useCases.add(activityCategory);
UseCaseCategory broadcastReceiverCategory = new UseCaseCategory("广播测试用例", new UseCase[]{
new TestReceiverActivity.Case(),
new TestDynamicReceiverActivity.Case()
});
useCases.add(broadcastReceiverCategory);
UseCaseCategory providerCategory = new UseCaseCategory("ContentProvider测试用例", new UseCase[]{
new TestDBContentProviderActivity.Case(),
new TestFileProviderActivity.Case()
});
useCases.add(providerCategory);
UseCaseCategory fragmentCategory = new UseCaseCategory("fragment测试用例", new UseCase[]{
new TestDynamicFragmentActivity.Case(),
new TestXmlFragmentActivity.Case(),
new TestDialogFragmentActivity.Case()
});
useCases.add(fragmentCategory);
UseCaseCategory dialogCategory = new UseCaseCategory("Dialog测试用例", new UseCase[]{
new TestDialogActivity.Case(),
});
useCases.add(dialogCategory);
UseCaseCategory packageManagerCategory = new UseCaseCategory("PackageManager测试用例", new UseCase[]{
new TestPackageManagerActivity.Case(),
});
useCases.add(packageManagerCategory);
UseCaseCategory contextCategory = new UseCaseCategory("Context相关测试用例", new UseCase[]{
new ActivityContextSubDirTestActivity.Case(),
new ApplicationContextSubDirTestActivity.Case(),
});
useCases.add(contextCategory);
UseCaseCategory communicationCategory = new UseCaseCategory("插件和宿主通信相关测试用例", new UseCase[]{
new PluginUseHostClassActivity.Case(),
});
useCases.add(communicationCategory);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/activity/TestActivityOnCreate.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.activity;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
import com.tencent.shadow.sample.plugin.app.lib.gallery.util.ToastUtil;
public class TestActivityOnCreate extends Activity {
public static class Case extends UseCase {
@Override
public String getName() {
return "生命周期测试";
}
@Override
public String getSummary() {
return "测试Activity的生命周期方法是否正确回调";
}
@Override
public Class getPageClass() {
return TestActivityOnCreate.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_activity_lifecycle);
ToastUtil.showToast(this, "onCreate");
}
@Override
protected void onStart() {
super.onStart();
ToastUtil.showToast(this, "onStart");
}
@Override
protected void onRestart() {
super.onRestart();
ToastUtil.showToast(this, "onRestart");
}
@Override
protected void onResume() {
super.onResume();
ToastUtil.showToast(this, "onResume");
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
ToastUtil.showToast(this, "onSaveInstanceState");
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
ToastUtil.showToast(this, "onRestoreInstanceState");
}
@Override
protected void onStop() {
super.onStop();
ToastUtil.showToast(this, "onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
ToastUtil.showToast(this, "onDestroy");
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/activity/TestActivityOptionMenu.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.activity;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.Menu;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class TestActivityOptionMenu extends Activity {
public static class Case extends UseCase {
@Override
public String getName() {
return "Activity Menu测试";
}
@Override
public String getSummary() {
return "测试Activity的 onCreateOptionsMenu";
}
@Override
public Class getPageClass() {
return TestActivityOptionMenu.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
setTheme(R.style.PluginAppThemeLight);
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_activity_settheme);
setTitle("看右边的 menu ->");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.case_test_activity_option_menu, menu);
return super.onCreateOptionsMenu(menu);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/activity/TestActivityOrientation.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.activity;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
import com.tencent.shadow.sample.plugin.app.lib.gallery.util.ToastUtil;
public class TestActivityOrientation extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "横竖屏切换测试";
}
@Override
public String getSummary() {
return "测试横竖屏切换时,Activity的生命周期变化是否和AndroidManifest.xml中配置的config相关";
}
@Override
public Class getPageClass() {
return TestActivityOrientation.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_orientation);
ToastUtil.showToast(this, "onCreate");
}
public void setOrientation(View view) {
int orientation = getRequestedOrientation();
if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/activity/TestActivityReCreate.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.activity;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.TextView;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
import com.tencent.shadow.sample.plugin.app.lib.gallery.util.ToastUtil;
public class TestActivityReCreate extends Activity {
public static class Case extends UseCase {
@Override
public String getName() {
return "ReCreate";
}
@Override
public String getSummary() {
return "测试Activity的调用ReCreate是否工作正常";
}
@Override
public Class getPageClass() {
return TestActivityReCreate.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_recreate);
TextView textView = findViewById(R.id.tv_msg);
boolean isRecreate = getIntent().getBooleanExtra("reCreate", false);
textView.setText("isRecreate:" + isRecreate);
ToastUtil.showToast(this, "onCreate");
}
public void reCreate(View view) {
getIntent().putExtra("reCreate", true);
recreate();
}
@Override
protected void onStart() {
super.onStart();
ToastUtil.showToast(this, "onStart");
}
@Override
protected void onRestart() {
super.onRestart();
ToastUtil.showToast(this, "onRestart");
}
@Override
protected void onResume() {
super.onResume();
ToastUtil.showToast(this, "onResume");
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
ToastUtil.showToast(this, "onSaveInstanceState");
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
ToastUtil.showToast(this, "onRestoreInstanceState");
}
@Override
protected void onStop() {
super.onStop();
ToastUtil.showToast(this, "onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
ToastUtil.showToast(this, "onDestroy");
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/activity/TestActivityReCreateBySystem.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.activity;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class TestActivityReCreateBySystem extends Activity {
public static class Case extends UseCase {
@Override
public String getName() {
return "ReCreateBySystem";
}
@Override
public String getSummary() {
return "不保留活动进行测试,需要手动到开发者模式中开启";
}
@Override
public Class getPageClass() {
return TestActivityReCreateBySystem.class;
}
@Override
public Bundle getPageParams() {
Bundle bundle = new Bundle();
bundle.putString("url", "https://www.baidu.com");
return bundle;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_re_create_by_system);
String url = "url : " + getIntent().getStringExtra("url");
((TextView) findViewById(R.id.url_tv)).setText(url);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/activity/TestActivitySetTheme.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.activity;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
import com.tencent.shadow.sample.plugin.app.lib.gallery.util.ToastUtil;
public class TestActivitySetTheme extends Activity {
public static class Case extends UseCase {
@Override
public String getName() {
return "Activity 主题测试";
}
@Override
public String getSummary() {
return "测试Activity的 setTheme 方法";
}
@Override
public Class getPageClass() {
return TestActivitySetTheme.class;
}
}
int currentTheme = 0;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
int currentTheme = getIntent().getIntExtra("theme", 0);
currentTheme++;
setTheme(currentTheme % 2 == 0 ? R.style.TestPluginTheme : R.style.PluginAppThemeLight);
ToastUtil.showToast(TestActivitySetTheme.this, currentTheme % 2 == 0 ? "R.style.TestPluginTheme" : "R.style.PluginAppThemeLight");
//setTheme必须在super.onCreate之前调用才行
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_activity_settheme);
final View btn = findViewById(R.id.button);
btn.setEnabled(true);
final int finalCurrentTheme = currentTheme;
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btn.setEnabled(false);
Intent intent = new Intent(TestActivitySetTheme.this, TestActivitySetTheme.class);
intent.putExtra("theme", finalCurrentTheme);
startActivity(intent);
btn.setEnabled(true);
}
});
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/activity/TestActivityWindowSoftMode.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class TestActivityWindowSoftMode extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "windowSoftInputMode测试";
}
@Override
public String getSummary() {
return "测试插件中设置windowSoftInputMode是否生效";
}
@Override
public Class getPageClass() {
return TestActivityWindowSoftMode.class;
}
}
private EditText mEditText;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_softmode);
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
boolean is_state_visible = layoutParams.softInputMode == WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
TextView textView = findViewById(R.id.result);
textView.setText("SOFT_INPUT_STATE_VISIBLE:" + is_state_visible);
mEditText = findViewById(R.id.edit_view);
mEditText.requestFocus();
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/application/TestApplicationActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.application;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.TextView;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.TestApplication;
public class TestApplicationActivity extends BaseActivity {
private TextView mText;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_common);
mText = findViewById(R.id.text);
mText.setText("isCallOnCreate:" + TestApplication.getInstance().isOnCreate);
}
public void doClick(View view) {
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/context/ActivityContextSubDirTestActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class ActivityContextSubDirTestActivity extends SubDirContextThemeWrapperTestActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "ActivityContextSubDir测试";
}
@Override
public String getSummary() {
return "测试Activity作为Context因BusinessName不同而隔离的相关特性";
}
@Override
public Class getPageClass() {
return ActivityContextSubDirTestActivity.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
fillTestValues(this);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/context/ApplicationContextSubDirTestActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class ApplicationContextSubDirTestActivity extends SubDirContextThemeWrapperTestActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "ApplicationContextSubDir测试";
}
@Override
public String getSummary() {
return "测试Application作为Context因BusinessName不同而隔离的相关特性";
}
@Override
public Class getPageClass() {
return ApplicationContextSubDirTestActivity.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
fillTestValues(getApplication());
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/context/SubDirContextThemeWrapperTestActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.context;
import static android.os.Environment.DIRECTORY_MUSIC;
import static android.os.Environment.DIRECTORY_PODCASTS;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.util.UiUtil;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
abstract class SubDirContextThemeWrapperTestActivity extends BaseActivity {
private LinearLayout mRootView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
ScrollView scrollView = new ScrollView(this);
scrollView.addView(linearLayout);
setContentView(scrollView);
mRootView = linearLayout;
}
protected void fillTestValues(Context testContext) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
makeItem("getDataDir()", "TAG_GET_DATA_DIR",
testContext.getDataDir().getAbsolutePath()
);
}
makeItem("getFilesDir()", "TAG_GET_FILES_DIR",
testContext.getFilesDir().getAbsolutePath()
);
makeItem("openFileInput(\"foo\")", "TAG_OPEN_FILE_INPUT_FOO",
getOpenFileInputAbsolutePath(testContext, "foo")
);
makeItem("openFileInput(\"bar\")", "TAG_OPEN_FILE_INPUT_BAR",
getOpenFileInputAbsolutePath(testContext, "bar")
);
makeItem("openFileOutput(\"foo\", MODE_PRIVATE)", "TAG_OPEN_FILE_OUTPUT_FOO",
getOpenFileOutputAbsolutePath(testContext, "foo")
);
makeItem("openFileOutput(\"bar\", MODE_PRIVATE)", "TAG_OPEN_FILE_OUTPUT_BAR",
getOpenFileOutputAbsolutePath(testContext, "bar")
);
makeItem("deleteFile(\"foo\")", "TAG_DELETE_FILE_FOO",
isDeleteFileSuccess(testContext, "foo")
);
makeItem("deleteFile(\"bar\")", "TAG_DELETE_FILE_BAR",
isDeleteFileSuccess(testContext, "bar")
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
makeItem("getNoBackupFilesDir()", "TAG_GET_NBF_DIR",
testContext.getNoBackupFilesDir().getAbsolutePath()
);
}
File externalMusicDir = testContext.getExternalFilesDir(DIRECTORY_MUSIC);
makeItem("getExternalFilesDir(DIRECTORY_MUSIC)", "TAG_GET_EFD_MUSIC",
externalMusicDir == null ? "null" : externalMusicDir.getAbsolutePath()
);
File externalPodcastsDir = testContext.getExternalFilesDir(DIRECTORY_PODCASTS);
makeItem("getExternalFilesDir(DIRECTORY_MUSIC)", "TAG_GET_EFD_PODCASTS",
externalPodcastsDir == null ? "null" : externalPodcastsDir.getAbsolutePath()
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
File[] externalMusicDirs = testContext.getExternalFilesDirs(DIRECTORY_MUSIC);
makeItem("getExternalFilesDirs(DIRECTORY_MUSIC)", "TAG_GET_EFDS_MUSIC",
Arrays.toString(externalMusicDirs)
);
File[] externalPodcastsDirs = testContext.getExternalFilesDirs(DIRECTORY_PODCASTS);
makeItem("getExternalFilesDirs(DIRECTORY_MUSIC)", "TAG_GET_EFDS_PODCASTS",
Arrays.toString(externalPodcastsDirs)
);
}
makeItem("getObbDir()", "TAG_GET_OBB_DIR",
testContext.getObbDir().getAbsolutePath()
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
makeItem("getObbDirs()", "TAG_GET_OBB_DIRS",
Arrays.toString(testContext.getObbDirs())
);
}
makeItem("getCacheDir()", "TAG_GET_CACHE_DIR",
testContext.getCacheDir().getAbsolutePath()
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
makeItem("getCodeCacheDir()", "TAG_GET_CODE_CACHE_DIR",
testContext.getCodeCacheDir().getAbsolutePath()
);
}
File externalCacheDir = testContext.getExternalCacheDir();
makeItem("getExternalCacheDir()", "TAG_GET_EXT_CACHE_DIR",
externalCacheDir == null ? "null" : externalCacheDir.getAbsolutePath()
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
makeItem("getExternalCacheDirs()", "TAG_GET_EXT_CACHE_DIRS",
Arrays.toString(testContext.getExternalCacheDirs())
);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
makeItem("getExternalMediaDirs()", "TAG_GET_EXT_MEDIA_DIRS",
Arrays.toString(testContext.getExternalMediaDirs())
);
}
makeItem("getDir(\"foo\",MODE_PRIVATE)", "TAG_GET_DIR_FOO",
testContext.getDir("foo", MODE_PRIVATE).getAbsolutePath()
);
makeItem("getDir(\"bar\",MODE_PRIVATE)", "TAG_GET_DIR_BAR",
testContext.getDir("bar", MODE_PRIVATE).getAbsolutePath()
);
makeItem("getSharedPreferences(\"foo\",MODE_PRIVATE)", "TAG_GET_SP_FOO",
getSharedPreferencesAbsolutePath(testContext, "foo")
);
makeItem("getSharedPreferences(\"bar\",MODE_PRIVATE)", "TAG_GET_SP_BAR",
getSharedPreferencesAbsolutePath(testContext, "bar")
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
makeItem("deleteSharedPreferences(\"foo\")", "TAG_DEL_SP_FOO",
isDeleteSharedPreferencesSuccess(testContext, "foo")
);
makeItem("deleteSharedPreferences(\"bar\")", "TAG_DEL_SP_BAR",
isDeleteSharedPreferencesSuccess(testContext, "bar")
);
}
makeItem("openOrCreateDatabase(\"foo\",MODE_PRIVATE,null)", "TAG_OOCD3_FOO",
testContext.openOrCreateDatabase("foo", MODE_PRIVATE, null).getPath()
);
testContext.deleteDatabase("foo");
makeItem("openOrCreateDatabase(\"bar\",MODE_PRIVATE,null)", "TAG_OOCD3_BAR",
testContext.openOrCreateDatabase("bar", MODE_PRIVATE, null).getPath()
);
testContext.deleteDatabase("bar");
makeItem("openOrCreateDatabase(\"foo\",MODE_PRIVATE,null,null)", "TAG_OOCD4_FOO",
testContext.openOrCreateDatabase("foo", MODE_PRIVATE, null, null).getPath()
);
testContext.deleteDatabase("foo");
makeItem("openOrCreateDatabase(\"bar\",MODE_PRIVATE,null,null)", "TAG_OOCD4_BAR",
testContext.openOrCreateDatabase("bar", MODE_PRIVATE, null, null).getPath()
);
testContext.deleteDatabase("bar");
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
String value = "";
try {
testContext.moveDatabaseFrom(this, "foo");
} catch (Exception e) {
value = e.getMessage();
}
makeItem("moveDatabaseFrom(this,\"foo\")", "TAG_MOVE_DB_FROM_FOO",
value
);
try {
testContext.moveDatabaseFrom(this, "bar");
} catch (Exception e) {
value = e.getMessage();
}
makeItem("moveDatabaseFrom(this,\"bar\")", "TAG_MOVE_DB_FROM_BAR",
value
);
}
makeItem("deleteDatabase(\"foo_d\")", "TAG_DELETE_DB_FOO",
isDeleteDatabaseSuccess(testContext, "foo_d")
);
makeItem("deleteDatabase(\"bar_d\")", "TAG_DELETE_DB_BAR",
isDeleteDatabaseSuccess(testContext, "bar_d")
);
makeItem("getDatabasePath(\"foo\")", "TAG_GET_DATABASE_PATH_FOO",
testContext.getDatabasePath("foo").getAbsolutePath()
);
makeItem("getDatabasePath(\"bar\")", "TAG_GET_DATABASE_PATH_BAR",
testContext.getDatabasePath("bar").getAbsolutePath()
);
Context hostContext = getApplication().getBaseContext();
hostContext.openOrCreateDatabase("foo", MODE_PRIVATE, null);
testContext.openOrCreateDatabase("bar", MODE_PRIVATE, null);
String[] databaseListArray = testContext.databaseList();
List databaseList = new LinkedList<>();
Collections.addAll(databaseList, databaseListArray);
Iterator iterator = databaseList.iterator();
String s;
while (iterator.hasNext()) {
s = iterator.next();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
if (s.endsWith("-wal") || s.endsWith("-shm")) {
iterator.remove();
}
} else {
if (s.endsWith("-journal")) {
iterator.remove();
}
}
}
makeItem("databaseList()", "TAG_DATABASE_LIST",
Arrays.toString(databaseList.toArray())
);
hostContext.deleteDatabase("foo");
testContext.deleteDatabase("bar");
}
private String getOpenFileInputAbsolutePath(Context context, String name) {
String result = "";
try {
context.openFileInput(name);
} catch (FileNotFoundException e) {
String message = e.getMessage();
int i = message.indexOf(name);
result = message.substring(0, i + name.length());
}
return result;
}
private String getOpenFileOutputAbsolutePath(Context context, String name) {
File file = new File(context.getFilesDir(), name);
if (file.exists()) {
throw new RuntimeException("测试文件不能提前存在");
}
try {
FileOutputStream fileOutputStream = context.openFileOutput(name, MODE_PRIVATE);
fileOutputStream.write(1);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (!file.delete()) {
throw new RuntimeException("测试文件应该被创建出来了");
}
return file.getAbsolutePath();
}
private String isDeleteFileSuccess(Context context, String name) {
File foo = new File(context.getFilesDir(), name);
try {
boolean newFile = foo.createNewFile();
if (!newFile) {
throw new RuntimeException("没能创建新文件");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
if (context.deleteFile(name)) {
return "success";
} else {
return "fail";
}
}
private String getSharedPreferencesAbsolutePath(Context context, final String name) {
SharedPreferences sharedPreferences
= context.getSharedPreferences(name, MODE_PRIVATE);
boolean commit = sharedPreferences.edit().putString("test", "test").commit();
if (!commit) {
throw new RuntimeException("commit failed");
}
Context hostContext = getApplication().getBaseContext();
File dataDir = hostContext.getFilesDir().getParentFile();
File sharedPrefsDir = new File(dataDir, "shared_prefs");
File[] files = sharedPrefsDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String fileName) {
return fileName.contains(name);
}
});
if (files.length != 1) {
throw new RuntimeException("匹配文件数量不对。");
}
String result = files[0].getAbsolutePath();
if (!files[0].delete()) {
throw new RuntimeException("删除测试文件失败");
}
return result;
}
@RequiresApi(api = Build.VERSION_CODES.N)
private String isDeleteSharedPreferencesSuccess(Context context, String name) {
File foo = new File(getSharedPreferencesAbsolutePath(context, name));
try {
boolean newFile = foo.createNewFile();
if (!newFile) {
throw new RuntimeException("没能创建新文件");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
if (context.deleteSharedPreferences(name)) {
return "success";
} else {
return "fail";
}
}
private String isDeleteDatabaseSuccess(Context context, String name) {
File foo = context.getDatabasePath(name);
try {
boolean newFile = foo.createNewFile();
if (!newFile) {
throw new RuntimeException("没能创建新文件");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
if (context.deleteDatabase(name)) {
return "success";
} else {
return "fail";
}
}
private void makeItem(
String labelText,
final String viewTag,
String value
) {
ViewGroup item = UiUtil.makeItem(this, labelText, viewTag, value);
mRootView.addView(item);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/dialog/TestDialog.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.dialog;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.support.annotation.NonNull;
import android.view.Window;
public class TestDialog extends Dialog {
public TestDialog(@NonNull Context context) {
super(context);
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/dialog/TestDialogActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.dialog;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class TestDialogActivity extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "Dialog 相关测试";
}
@Override
public String getSummary() {
return "测试show Dialog";
}
@Override
public Class getPageClass() {
return TestDialogActivity.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_dialog_activity);
}
public void show(View view) {
TestDialog dialog = new TestDialog(this);
dialog.setContentView(R.layout.layout_dialog);
dialog.show();
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/fragment/TestDialogFragment.java
================================================
package com.tencent.shadow.sample.plugin.app.lib.usecases.fragment;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.TextView;
import com.tencent.shadow.sample.plugin.app.lib.R;
public class TestDialogFragment extends DialogFragment {
public static TestDialogFragment newInstance(Bundle bundle) {
TestDialogFragment testFragment = new TestDialogFragment();
testFragment.setArguments(bundle);
return testFragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return super.onCreateDialog(savedInstanceState);
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
Window window = getDialog().getWindow();
// window.setWindowAnimations(android.R.style.Animation_Toast);
window.setWindowAnimations(R.style.dialog_exit_fade_out);
View view = inflater.inflate(R.layout.layout_fragment_test, null, false);
TextView textView = view.findViewById(R.id.tv_msg);
Bundle bundle = getArguments();
if (bundle != null) {
String msg = bundle.getString("msg");
if (!TextUtils.isEmpty(msg)) {
textView.setText(msg);
}
}
return view;
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/fragment/TestDialogFragmentActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class TestDialogFragmentActivity extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "DialogFragment相关测试";
}
@Override
public String getSummary() {
return "测试DialogFragment使用setWindowAnimations";
}
@Override
public Class getPageClass() {
return TestDialogFragmentActivity.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_fragment_activity);
String msg = "这是TestDialogFragment";
Bundle bundle = new Bundle();
bundle.putString("msg", msg);
TestDialogFragment testFragment = TestDialogFragment.newInstance(bundle);
testFragment.show(getFragmentManager(), "TestDialogFragment");
getWindow().setWindowAnimations(R.style.dialog_exit_fade_out);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/fragment/TestDynamicFragmentActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class TestDynamicFragmentActivity extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "代码添加fragment相关测试";
}
@Override
public String getSummary() {
return "测试通过代码添加一个fragment";
}
@Override
public Class getPageClass() {
return TestDynamicFragmentActivity.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_fragment_activity);
String msg = "这是一个动态添加的fragment";
Bundle bundle = new Bundle();
bundle.putString("msg", msg);
TestFragment testFragment = TestFragment.newInstance(bundle);
getFragmentManager().beginTransaction().add(R.id.fragment_container, testFragment).commit();
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/fragment/TestFragment.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.fragment;
import android.app.Fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.tencent.shadow.sample.plugin.app.lib.R;
public class TestFragment extends Fragment {
public static TestFragment newInstance(Bundle bundle) {
TestFragment testFragment = new TestFragment();
testFragment.setArguments(bundle);
return testFragment;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.layout_fragment_test, null, false);
TextView textView = view.findViewById(R.id.tv_msg);
Bundle bundle = getArguments();
if (bundle != null) {
String msg = bundle.getString("msg");
if (!TextUtils.isEmpty(msg)) {
textView.setText(msg);
}
}
return view;
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/fragment/TestXmlFragmentActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class TestXmlFragmentActivity extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "xml中使用fragment相关测试";
}
@Override
public String getSummary() {
return "测试在Activity现实xml中定义的fragment";
}
@Override
public Class getPageClass() {
return TestXmlFragmentActivity.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_fragment_xml_activity);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/host_communication/PluginUseHostClassActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.host_communication;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.LinearLayout;
import com.tencent.shadow.sample.host.lib.HostUiLayerProvider;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class PluginUseHostClassActivity extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "插件使用宿主类测试";
}
@Override
public String getSummary() {
return "测试插件中调用宿主类的方法";
}
@Override
public Class getPageClass() {
return PluginUseHostClassActivity.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout linearLayout = new LinearLayout(this);
HostUiLayerProvider hostUiLayerProvider = HostUiLayerProvider.getInstance();
View hostUiLayer = hostUiLayerProvider.buildHostUiLayer();
linearLayout.addView(hostUiLayer);
setContentView(linearLayout);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/packagemanager/TestPackageManagerActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.packagemanager;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.TextView;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class TestPackageManagerActivity extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "PackageManager调用测试";
}
@Override
public String getSummary() {
return "测试PackageManager相关api的调用,确保插件调用相关api时可以正确获取到插件相关的信息";
}
@Override
public Class getPageClass() {
return TestPackageManagerActivity.class;
}
}
private TextView mTvTextView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_packagemanager);
mTvTextView = findViewById(R.id.text);
}
public void getApplicationInfo(View view) {
try {
ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(), 0);
mTvTextView.setText("ApplicationInfo className:" + applicationInfo.className +
"\nnativeLibraryDir:" + applicationInfo.nativeLibraryDir
+ "\nmetaData:" + applicationInfo.metaData);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
public void getActivityInfo(View view) {
try {
ActivityInfo activityInfo = getPackageManager().getActivityInfo(new ComponentName(this, this.getClass()), 0);
mTvTextView.setText("activityInfo name:" + activityInfo.name
+ "\npackageName:" + activityInfo.packageName);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
public void getPackageInfo(View view) {
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
mTvTextView.setText("packageInfo versionName:" + packageInfo.versionName
+ "\nversionCode:" + packageInfo.versionCode);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/provider/TestDBContentProviderActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.provider;
import android.content.ContentValues;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class TestDBContentProviderActivity extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "ContentProvider DB相关测试";
}
@Override
public String getSummary() {
return "测试通过ContentProvider来操作数据库";
}
@Override
public Class getPageClass() {
return TestDBContentProviderActivity.class;
}
}
private static final String TAG = "ContentProviderActivity";
private TextView mTextView;
private Handler mHandler = new Handler();
private ContentObserver mObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
Log.d(TAG, uri + " onChange");
Toast.makeText(TestDBContentProviderActivity.this, uri + " onChange", Toast.LENGTH_SHORT).show();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_provider_db);
mTextView = findViewById(R.id.text);
getContentResolver().registerContentObserver(TestProviderInfo.TestEntry.CONTENT_URI,
false, mObserver);
}
public void insert(View view) {
ContentValues contentValues = new ContentValues();
contentValues.put(TestProviderInfo.TestEntry.COLUMN_NAME, "test");
contentValues.put(TestProviderInfo.TestEntry._ID, System.currentTimeMillis());
getContentResolver().insert(TestProviderInfo.TestEntry.CONTENT_URI, contentValues);
query(view);
}
public void query(View view) {
Cursor cursor = getContentResolver().query(TestProviderInfo.TestEntry.CONTENT_URI, null, null, null, null);
if (cursor != null) {
StringBuilder s = new StringBuilder();
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndex(TestProviderInfo.TestEntry._ID));
String name = cursor.getString(cursor.getColumnIndex(TestProviderInfo.TestEntry.COLUMN_NAME));
s.append("id:").append(id).append(" name:").append(name).append(" \n");
}
mTextView.setText(s);
cursor.close();
} else {
Toast.makeText(this, "请先插入数据", Toast.LENGTH_SHORT).show();
}
}
public void update(View view) {
Cursor cursor = getContentResolver().query(TestProviderInfo.TestEntry.CONTENT_URI,
null, null, null, null);
int count = cursor != null ? cursor.getCount() : 0;
if (count > 0) {
cursor.moveToFirst();
ContentValues contentValues = new ContentValues();
contentValues.put(TestProviderInfo.TestEntry.COLUMN_NAME, "name " + System.currentTimeMillis());
long id = cursor.getLong(cursor.getColumnIndex(TestProviderInfo.TestEntry._ID));
getContentResolver().update(TestProviderInfo.TestEntry.CONTENT_URI, contentValues,
TestProviderInfo.TestEntry._ID + " = ?",
new String[]{String.valueOf(id)});
}
if (cursor != null) {
cursor.close();
}
query(view);
}
public void delete(View view) {
Cursor cursor = getContentResolver().query(TestProviderInfo.TestEntry.CONTENT_URI,
null, null, null, null);
int count = cursor != null ? cursor.getCount() : 0;
if (count > 0) {
cursor.moveToFirst();
long id = cursor.getLong(cursor.getColumnIndex(TestProviderInfo.TestEntry._ID));
getContentResolver().delete(TestProviderInfo.TestEntry.CONTENT_URI,
TestProviderInfo.TestEntry._ID + " = ?",
new String[]{String.valueOf(id)});
}
if (cursor != null) {
cursor.close();
}
query(view);
}
public void bulkInsert(View view) {
ContentValues[] values = new ContentValues[3];
ContentValues contentValues = new ContentValues();
contentValues.put(TestProviderInfo.TestEntry.COLUMN_NAME, "test");
contentValues.put(TestProviderInfo.TestEntry._ID, System.currentTimeMillis());
values[0] = contentValues;
contentValues = new ContentValues();
contentValues.put(TestProviderInfo.TestEntry.COLUMN_NAME, "test");
contentValues.put(TestProviderInfo.TestEntry._ID, System.currentTimeMillis() + 5);
values[1] = contentValues;
contentValues = new ContentValues();
contentValues.put(TestProviderInfo.TestEntry.COLUMN_NAME, "test");
contentValues.put(TestProviderInfo.TestEntry._ID, System.currentTimeMillis() + 10);
values[2] = contentValues;
getContentResolver().bulkInsert(TestProviderInfo.TestEntry.CONTENT_URI, values);
query(view);
}
public void call(View view) {
Bundle beauty = getContentResolver().call(TestProviderInfo.TestEntry.CONTENT_URI, "getBeauty", "18", null);
if (beauty != null) {
Toast.makeText(this, "get beauty who name is " + beauty.getString("name"), Toast.LENGTH_LONG).show();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
getContentResolver().unregisterContentObserver(mObserver);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/provider/TestDBHelper.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.provider;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class TestDBHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "shadow.db";
public TestDBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
final String SQL_CREATE_CONTACT_TABLE = "CREATE TABLE " + TestProviderInfo.TestEntry.TABLE_NAME + "( "
+ TestProviderInfo.TestEntry._ID + " TEXT PRIMARY KEY, "
+ TestProviderInfo.TestEntry.COLUMN_NAME + " TEXT NOT NULL );";
db.execSQL(SQL_CREATE_CONTACT_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TestProviderInfo.TestEntry.TABLE_NAME);
onCreate(db);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/provider/TestFileProviderActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.provider;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v4.content.FileProvider;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import com.tencent.shadow.sample.plugin.app.lib.BuildConfig;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
import java.io.File;
public class TestFileProviderActivity extends Activity {
private static final String TAG = "TestFileProviderActivity";
private static final String KEY_FILE_PATH = "filePath";
public static class Case extends UseCase {
@Override
public String getName() {
return "FileProvider相关测试";
}
@Override
public String getSummary() {
return "通过使用系统相机拍照来测试FileProvider";
}
@Override
public Class getPageClass() {
return TestFileProviderActivity.class;
}
}
private static final int REQUEST_CODE = 1001;
private ImageView mImageView;
private File mFile;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate: ");
setContentView(R.layout.activity_test_file_provider);
mImageView = findViewById(R.id.photo);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.CAMERA}, 1001);
}
findViewById(R.id.go_take_photo).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String fileName = String.valueOf(System.currentTimeMillis());
String filePath = getFilesDir() + "/images/" + fileName + ".jpg";
mFile = new File(filePath);
if (!mFile.getParentFile().exists()) {
mFile.getParentFile().mkdir();
}
Uri contentUri;
if (targetSdkVersion() >= Build.VERSION_CODES.N) {
contentUri = FileProvider.getUriForFile(TestFileProviderActivity.this,
BuildConfig.APPLICATION_ID + ".general_cases.fileprovider", mFile);
// contentUri = Uri.parse("content://com.tencent.shadow.contentprovider.authority/com.tencent.shadow.test.plugin.general_cases.lib.gallery.fileprovider" +
// "/name/data/data/com.tencent.shadow.test.hostapp/files/images/1548417832706.jpg");
} else {
contentUri = Uri.fromFile(mFile);
}
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
startActivityForResult(intent, REQUEST_CODE);
}
});
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(KEY_FILE_PATH, mFile.getAbsolutePath());
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String filePath = savedInstanceState.getString(KEY_FILE_PATH);
if (!TextUtils.isEmpty(filePath)) {
mFile = new File(filePath);
}
}
private int targetSdkVersion() {
return getApplicationContext().getApplicationInfo().targetSdkVersion;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
setPic();
}
}
@SuppressLint("LongLogTag")
private void setPic() {
if (mFile == null || !mFile.exists()) {
Log.w(TAG, "setPic: file don't exist");
return;
}
mImageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
// Get the dimensions of the View
int targetW = mImageView.getWidth();
int targetH = mImageView.getHeight();
mImageView.getViewTreeObserver().removeOnPreDrawListener(this);
// Get the dimensions of the bitmap
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mFile.getAbsolutePath(), bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
// Determine how much to scale down the image
int scaleFactor = Math.min(photoW / targetW, photoH / targetH);
// Decode the image file into a Bitmap sized to fill the View
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
bmOptions.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeFile(mFile.getAbsolutePath(), bmOptions);
mImageView.setImageBitmap(bitmap);
return false;
}
});
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/provider/TestProvider.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.provider;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* TestProvider
* Created by 90Chris on 2016/5/1.
*/
public class TestProvider extends ContentProvider {
private TestDBHelper mOpenHelper;
@Override
public boolean onCreate() {
mOpenHelper = new TestDBHelper(getContext());
return true;
}
@Nullable
@Override
public String getType(Uri uri) {
return null;
}
@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
Cursor cursor = null;
switch (buildUriMatcher().match(uri)) {
case TEST:
cursor = db.query(TestProviderInfo.TestEntry.TABLE_NAME, projection, selection, selectionArgs, sortOrder, null, null);
break;
}
return cursor;
}
@SuppressWarnings("ConstantConditions")
@Nullable
@Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
Uri returnUri;
long _id;
switch (buildUriMatcher().match(uri)) {
case TEST:
_id = db.insert(TestProviderInfo.TestEntry.TABLE_NAME, null, values);
if (_id > 0) {
returnUri = TestProviderInfo.TestEntry.buildUri(_id);
getContext().getContentResolver().notifyChange(returnUri, null);
} else
throw new android.database.SQLException("Failed to insert row into " + uri);
break;
default:
throw new android.database.SQLException("Unknown uri: " + uri);
}
return returnUri;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int result = db.delete(TestProviderInfo.TestEntry.TABLE_NAME, selection, selectionArgs);
if (result > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return result;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int result = db.update(TestProviderInfo.TestEntry.TABLE_NAME, values, selection, selectionArgs);
if (result > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return result;
}
public Bundle call(@NonNull String method, String arg, @Nullable Bundle extras) {
switch (method) {
case "getBeauty":
Bundle bundle = new Bundle();
bundle.putString("name", "Anne Hathaway");
return bundle;
}
return null;
}
private final static int TEST = 100;
static UriMatcher buildUriMatcher() {
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
final String authority = TestProviderInfo.CONTENT_AUTHORITY;
matcher.addURI(authority, TestProviderInfo.PATH_TEST, TEST);
return matcher;
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/provider/TestProviderInfo.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.provider;
import android.content.ContentUris;
import android.net.Uri;
import android.provider.BaseColumns;
import com.tencent.shadow.sample.plugin.app.lib.BuildConfig;
public class TestProviderInfo {
protected static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider.test";
protected static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
protected static final String PATH_TEST = "test";
public static final class TestEntry implements BaseColumns {
public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon().appendPath(PATH_TEST).build();
protected static Uri buildUri(long id) {
return ContentUris.withAppendedId(CONTENT_URI, id);
}
protected static final String TABLE_NAME = "TestProviderInfo";
public static final String COLUMN_NAME = "name";
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/receiver/MyReceiver.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String msg = intent.getStringExtra("msg");
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/receiver/TestDynamicReceiverActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.Button;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
import com.tencent.shadow.sample.plugin.app.lib.gallery.util.ToastUtil;
public class TestDynamicReceiverActivity extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "动态广播测试";
}
@Override
public String getSummary() {
return "测试动态广播的发送和接收是否工作正常";
}
@Override
public Class getPageClass() {
return TestDynamicReceiverActivity.class;
}
}
private final static String INTENT_ACTION = "com.tencent.test.action.DYNAMIC";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_receiver);
Button button = findViewById(R.id.button);
button.setText("测试动态广播发送");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(INTENT_ACTION);
intent.putExtra("msg", "收到测试动态广播发送");
sendBroadcast(intent);
}
});
DynamicBroadcastReceiver dynamicBroadcastReceiver = new DynamicBroadcastReceiver();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
registerReceiver(dynamicBroadcastReceiver, new IntentFilter(INTENT_ACTION), Context.RECEIVER_EXPORTED);
} else {
registerReceiver(dynamicBroadcastReceiver, new IntentFilter(INTENT_ACTION));
}
}
private class DynamicBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String msg = intent.getStringExtra("msg");
ToastUtil.showToast(context, msg);
}
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/receiver/TestReceiverActivity.java
================================================
/*
* 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.sample.plugin.app.lib.usecases.receiver;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.Button;
import com.tencent.shadow.sample.plugin.app.lib.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class TestReceiverActivity extends BaseActivity {
public static class Case extends UseCase {
@Override
public String getName() {
return "静态广播测试";
}
@Override
public String getSummary() {
return "测试静态广播的发送和接收是否工作正常";
}
@Override
public Class getPageClass() {
return TestReceiverActivity.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_receiver);
Button button = findViewById(R.id.button);
button.setText("测试静态广播发送");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.tencent.test.action");
intent.putExtra("msg", "收到测试静态广播发送");
sendBroadcast(intent);
}
});
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/service/HostAddPluginViewService.java
================================================
package com.tencent.shadow.sample.plugin.app.lib.usecases.service;
import android.app.IntentService;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import com.tencent.shadow.sample.host.lib.HostAddPluginViewContainer;
import com.tencent.shadow.sample.host.lib.HostAddPluginViewContainerHolder;
import com.tencent.shadow.sample.plugin.app.lib.R;
public class HostAddPluginViewService extends IntentService {
private final Handler uiHandler = new Handler(Looper.getMainLooper());
public HostAddPluginViewService() {
super("HostAddPluginViewService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
int id = intent.getIntExtra("id", 0);
HostAddPluginViewContainer viewContainer
= HostAddPluginViewContainerHolder.instances.remove(id);
uiHandler.post(() -> {
View view = LayoutInflater.from(this).inflate(
R.layout.layout_host_add_plugin_view, null, false);
viewContainer.addView(view);
});
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/java/com/tencent/shadow/sample/plugin/app/lib/usecases/webview/WebViewActivity.java
================================================
package com.tencent.shadow.sample.plugin.app.lib.usecases.webview;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
public class WebViewActivity extends Activity {
public static class Case extends UseCase {
@Override
public String getName() {
return "WebView测试";
}
@Override
public String getSummary() {
return "测试WebView是否能正常工作";
}
@Override
public Class getPageClass() {
return WebViewActivity.class;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebView webView = new FooWebView(this);
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl("file:///android_asset/web/test.html?t=" + Math.random());
setContentView(webView);
}
}
/**
* 复现
* https://github.com/Tencent/Shadow/issues/1175
*/
class FooWebView extends WebView {
public FooWebView(@NonNull Context context) {
super(context);
}
@Override
public void setWebViewClient(@NonNull WebViewClient client) {
FooWebViewClient fooWebViewClient = (FooWebViewClient) client;
super.setWebViewClient(fooWebViewClient);
}
}
class FooWebViewClient extends WebViewClient {
}
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/anim/dialog_exit_fade_out.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/drawable/selector_group.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/drawable/selector_item.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/activity_test_file_provider.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/activity_test_re_create_by_system.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_activity_lifecycle.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_activity_settheme.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_common.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_dialog.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_dialog_activity.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_fragment_activity.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_fragment_test.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_fragment_xml_activity.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_host_add_plugin_view.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_orientation.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_packagemanager.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_provider_db.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_receiver.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_recreate.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_result.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_service.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_softmode.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/layout/layout_test_view_cons_cache.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/menu/case_test_activity_option_menu.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/values/strings.xml
================================================
Shadow主测试用例集合
这是插件中的string资源
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/values/styles.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/values/themes.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-app/src/main/res/values-v21/themes.xml
================================================
#9c27b0
#7b1fa2
#e040fb
================================================
FILE: projects/sample/source/sample-plugin/sample-base/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-plugin/sample-base/build.gradle
================================================
buildscript {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath 'com.tencent.shadow.core:runtime'
classpath 'com.tencent.shadow.core:activity-container'
classpath 'com.tencent.shadow.core:gradle-plugin'
classpath "org.javassist:javassist:$javassist_version"
}
}
apply plugin: 'com.android.application'
apply plugin: 'com.tencent.shadow.plugin'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
applicationId 'com.tencent.shadow.sample.plugin.lib.base'
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
// 将插件applicationId设置为和宿主相同
productFlavors {
plugin {
applicationId project.SAMPLE_HOST_APP_APPLICATION_ID
}
}
// 将插件的资源ID分区改为和宿主0x7F不同的值
aaptOptions {
additionalParameters "--package-id", "0x7E", "--allow-reserved-package-id"
}
lintOptions {
abortOnError false
}
}
dependencies {
implementation project(":sample-base-lib")
//Shadow Transform后业务代码会有一部分实际引用runtime中的类
//如果不以compileOnly方式依赖,会导致其他Transform或者Proguard找不到这些类
pluginCompileOnly 'com.tencent.shadow.core:runtime'
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/cubershi/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
#这是Shadow在编译期将AndroidManifest.xml中所需信息生成的Java类,没有被代码自然引用,所以需要手工keep住。
-keep class com.tencent.shadow.core.manifest_parser.PluginManifest{*;}
================================================
FILE: projects/sample/source/sample-plugin/sample-base/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/build.gradle
================================================
apply plugin: 'com.android.library'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
consumerProguardFiles 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
lintOptions {
abortOnError false
}
}
dependencies {
implementation project(":slidingmenu")
implementation project(":pinnedheaderexpandablelistview")
implementation "com.android.support:support-annotations:$android_support_annotations_version"
api "com.android.support:support-v4:$android_support_version"
api "com.android.support:appcompat-v7:$android_support_version"
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/cubershi/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# 这是供sample-app模块依赖的类
-keep class com.tencent.shadow.sample.plugin.app.lib.gallery.cases.UseCaseManager{*;}
-keep class com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.*{*;}
-keep class com.tencent.shadow.sample.plugin.app.lib.gallery.BaseActivity{*;}
-keep class com.tencent.shadow.sample.plugin.app.lib.gallery.util.*{*;}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/BaseActivity.java
================================================
/*
* 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.sample.plugin.app.lib.gallery;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
public class BaseActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/MainActivity.java
================================================
/*
* 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.sample.plugin.app.lib.gallery;
import android.app.Activity;
import android.app.FragmentTransaction;
import android.content.Context;
import android.os.Bundle;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView.LayoutParams;
import android.widget.BaseExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.TextView;
import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu;
import com.ryg.expandable.ui.PinnedHeaderExpandableListView;
import com.ryg.expandable.ui.PinnedHeaderExpandableListView.OnHeaderUpdateListener;
import com.ryg.expandable.ui.StickyLayout;
import com.ryg.expandable.ui.StickyLayout.OnGiveUpTouchEventListener;
import com.tencent.shadow.sample.plugin.app.lib.base.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.UseCaseManager;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.UseCaseSummaryFragment;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCaseCategory;
import java.util.List;
public class MainActivity extends Activity implements
ExpandableListView.OnChildClickListener,
ExpandableListView.OnGroupClickListener,
OnHeaderUpdateListener, OnGiveUpTouchEventListener {
private PinnedHeaderExpandableListView expandableListView;
private StickyLayout stickyLayout;
private List categoryList;
private SparseBooleanArray expandStatus;
private SlidingMenu slidingMenu;
private ExpandableListAdapter adapter;
private UseCaseSummaryFragment caseSummaryFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_main);
expandableListView = findViewById(R.id.expandablelist);
stickyLayout = findViewById(R.id.sticky_layout);
slidingMenu = findViewById(R.id.slidingmenu);
caseSummaryFragment = new UseCaseSummaryFragment();
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.fragment_container, caseSummaryFragment, "CaseSummaryFragment");
fragmentTransaction.commitAllowingStateLoss();
categoryList = UseCaseManager.useCases;
expandStatus = new SparseBooleanArray();
adapter = new ExpandableListAdapter(this);
expandableListView.setAdapter(adapter);
expandableListView.setOnHeaderUpdateListener(this);
expandableListView.setOnChildClickListener(this);
expandableListView.setOnGroupClickListener(this);
stickyLayout.setOnGiveUpTouchEventListener(this);
slidingMenu.showMenu();
}
class ExpandableListAdapter extends BaseExpandableListAdapter {
private LayoutInflater inflater;
public ExpandableListAdapter(Context context) {
inflater = LayoutInflater.from(context);
}
// 返回父列表个数
@Override
public int getGroupCount() {
return categoryList.size();
}
// 返回子列表个数
@Override
public int getChildrenCount(int groupPosition) {
return categoryList.get(groupPosition).caseList.length;
}
@Override
public Object getGroup(int groupPosition) {
if (groupPosition >= 0)
return categoryList.get(groupPosition);
else return null;
}
@Override
public Object getChild(int groupPosition, int childPosition) {
return categoryList.get(groupPosition).caseList[childPosition];
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded,
View convertView, ViewGroup parent) {
CaseCategoryHolder groupHolder = null;
if (convertView == null) {
groupHolder = new CaseCategoryHolder();
convertView = inflater.inflate(R.layout.layout_case_category_item, null);
groupHolder.textCategory = (TextView) convertView
.findViewById(R.id.tv_category);
convertView.setTag(groupHolder);
} else {
groupHolder = (CaseCategoryHolder) convertView.getTag();
}
expandStatus.put(groupPosition, isExpanded);
String title = ((UseCaseCategory) getGroup(groupPosition)).title;
groupHolder.textCategory.setText(isExpanded ? title + " - " : title + " + ");
return convertView;
}
@Override
public View getChildView(int groupPosition, int childPosition,
boolean isLastChild, View convertView, ViewGroup parent) {
CaseItemHolder childHolder = null;
if (convertView == null) {
childHolder = new CaseItemHolder();
convertView = inflater.inflate(R.layout.layout_case_item, null);
childHolder.textName = (TextView) convertView
.findViewById(R.id.tv_case);
convertView.setTag(childHolder);
} else {
childHolder = (CaseItemHolder) convertView.getTag();
}
childHolder.textName.setText(((UseCase) getChild(groupPosition,
childPosition)).getName());
return convertView;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
}
@Override
public boolean onGroupClick(final ExpandableListView parent, final View v,
int groupPosition, final long id) {
return false;
}
@Override
public boolean onChildClick(ExpandableListView parent, View v,
int groupPosition, int childPosition, long id) {
UseCase useCase = categoryList.get(groupPosition).caseList[childPosition];
caseSummaryFragment.setCase(useCase);
slidingMenu.showMenu();
return false;
}
class CaseCategoryHolder {
TextView textCategory;
}
class CaseItemHolder {
TextView textName;
}
@Override
public View getPinnedHeader() {
View headerView = (ViewGroup) getLayoutInflater().inflate(R.layout.layout_case_category_item, null);
headerView.setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
return headerView;
}
@Override
public void updatePinnedHeader(View headerView, int firstVisibleGroupPos) {
UseCaseCategory firstVisibleGroup = (UseCaseCategory) adapter.getGroup(firstVisibleGroupPos);
if (firstVisibleGroup == null) return;
TextView textView = headerView.findViewById(R.id.tv_category);
String title = firstVisibleGroup.title;
textView.setText(expandStatus.get(firstVisibleGroupPos) ? title + " - " : title + " + ");
}
@Override
public boolean giveUpTouchEvent(MotionEvent event) {
if (expandableListView.getFirstVisiblePosition() == 0) {
View view = expandableListView.getChildAt(0);
if (view != null && view.getTop() >= 0) {
return true;
}
}
return false;
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/TestApplication.java
================================================
/*
* 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.sample.plugin.app.lib.gallery;
import android.app.Application;
public class TestApplication extends Application {
private static TestApplication sInstence;
public boolean isOnCreate;
@Override
public void onCreate() {
sInstence = this;
isOnCreate = true;
super.onCreate();
}
public static TestApplication getInstance() {
return sInstence;
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/cases/UseCaseManager.java
================================================
/*
* 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.sample.plugin.app.lib.gallery.cases;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCaseCategory;
import java.util.ArrayList;
import java.util.List;
public class UseCaseManager {
public static List useCases = new ArrayList<>();
public static boolean sInit;
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/cases/UseCaseSummaryFragment.java
================================================
/*
* 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.sample.plugin.app.lib.gallery.cases;
import android.app.ActivityOptions;
import android.app.Fragment;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import com.tencent.shadow.sample.plugin.app.lib.base.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.cases.entity.UseCase;
import com.tencent.shadow.sample.plugin.app.lib.gallery.util.PluginChecker;
public class UseCaseSummaryFragment extends Fragment {
private TextView mCaseName;
private Button mStartCase;
private TextView mCaseSummary;
private TextView mEnvironment;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.layout_fragment_case_summary, container, false);
bindViews(view);
return view;
}
public void setCase(final UseCase useCase) {
mCaseName.setText(useCase.getName());
mCaseSummary.setText(useCase.getSummary());
mStartCase.setVisibility(View.VISIBLE);
mStartCase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getActivity(), useCase.getPageClass());
if (useCase.getPageParams() != null) {
intent.putExtras(useCase.getPageParams());
}
//只在API 21以上手工测试一下ActivityOptions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ActivityOptions activityOptions = ActivityOptions.makeCustomAnimation(
getActivity(),
android.R.anim.slide_in_left,
android.R.anim.slide_out_right
);
//测试调用makeSceneTransitionAnimation方法传入Activity
ActivityOptions.makeSceneTransitionAnimation(
getActivity(),
UseCaseSummaryFragment.this.mCaseName,
"mCaseName"
);
//测试调用makeSceneTransitionAnimation方法传入Activity
ActivityOptions.makeSceneTransitionAnimation(
getActivity(),
new Pair<>(UseCaseSummaryFragment.this.mCaseName, "mCaseName")
);
startActivity(intent,
activityOptions.toBundle()
);
} else {
startActivity(intent);
}
}
});
}
private void bindViews(View view) {
mCaseName = (TextView) view.findViewById(R.id.case_name);
mStartCase = (Button) view.findViewById(R.id.start_case);
mCaseSummary = (TextView) view.findViewById(R.id.case_summary);
mEnvironment = (TextView) view.findViewById(R.id.environment);
mEnvironment.setText(PluginChecker.isPluginMode() ? "当前环境:插件模式" : "当前环境:独立安装");
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/cases/entity/UseCase.java
================================================
/*
* 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.sample.plugin.app.lib.gallery.cases.entity;
import android.os.Bundle;
public abstract class UseCase {
public abstract String getName();
public abstract String getSummary();
public abstract Class getPageClass();
public Bundle getPageParams() {
return null;
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/cases/entity/UseCaseCategory.java
================================================
/*
* 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.sample.plugin.app.lib.gallery.cases.entity;
public class UseCaseCategory {
public String title;
public UseCase[] caseList;
public UseCaseCategory(String title, UseCase[] caseList) {
this.title = title;
this.caseList = caseList;
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/splash/ISplashAnimation.java
================================================
/*
* 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.sample.plugin.app.lib.gallery.splash;
public interface ISplashAnimation {
void start();
void stop();
void setAnimationListener(AnimationListener animationListener);
interface AnimationListener {
void onAnimationEnd();
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/splash/SplashActivity.java
================================================
/*
* 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.sample.plugin.app.lib.gallery.splash;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.tencent.shadow.sample.plugin.app.lib.base.R;
import com.tencent.shadow.sample.plugin.app.lib.gallery.MainActivity;
public class SplashActivity extends Activity {
private SplashAnimation mSplashAnimation;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_splash);
mSplashAnimation = new SplashAnimation(this);
mSplashAnimation.start();
mSplashAnimation.setAnimationListener(new ISplashAnimation.AnimationListener() {
@Override
public void onAnimationEnd() {
finish();
startActivity(new Intent(SplashActivity.this, MainActivity.class));
}
});
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/splash/SplashAnimation.java
================================================
/*
* 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.sample.plugin.app.lib.gallery.splash;
import android.content.Context;
import android.os.Handler;
import com.tencent.shadow.sample.plugin.app.lib.gallery.util.ToastUtil;
public class SplashAnimation implements ISplashAnimation {
private AnimationListener mAnimationListener;
private Context mContext;
public SplashAnimation(Context context) {
mContext = context;
}
@Override
public void start() {
ToastUtil.showToast(mContext, "animation start");
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (mAnimationListener != null) {
mAnimationListener.onAnimationEnd();
}
}
}, 2000);
}
@Override
public void stop() {
}
@Override
public void setAnimationListener(AnimationListener animationListener) {
mAnimationListener = animationListener;
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/util/PluginChecker.java
================================================
/*
* 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.sample.plugin.app.lib.gallery.util;
public class PluginChecker {
private static Boolean sPluginMode;
/**
* 检测当前是否处于插件状态下
* 这里先简单通过访问一个插件框架中的类是否成功来判断
*
* @return true 是插件模式
*/
public static boolean isPluginMode() {
if (sPluginMode == null) {
try {
PluginChecker.class.getClassLoader().loadClass("com.tencent.shadow.core.runtime.ShadowApplication");
sPluginMode = true;
} catch (ClassNotFoundException e) {
sPluginMode = false;
}
}
return sPluginMode;
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/util/ToastUtil.java
================================================
/*
* 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.sample.plugin.app.lib.gallery.util;
import android.content.Context;
import android.widget.Toast;
public class ToastUtil {
public static void showToast(Context context, String message) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/java/com/tencent/shadow/sample/plugin/app/lib/gallery/util/UiUtil.java
================================================
/*
* 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.sample.plugin.app.lib.gallery.util;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
final public class UiUtil {
@SuppressLint("SetTextI18n")
public static ViewGroup makeItemView(Context viewContext, String labelText, String viewTag) {
TextView label = new TextView(viewContext);
label.setText(labelText + ":");
label.setLayoutParams(new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
TextView value = new TextView(viewContext);
value.setLayoutParams(new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
value.setTag(viewTag);
LinearLayout linearLayout = new LinearLayout(viewContext);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setPadding(0, 10, 0, 10);
linearLayout.addView(label);
linearLayout.addView(value);
return linearLayout;
}
public static void setItemValue(ViewGroup viewGroupContainsItem, String viewTag, String value) {
TextView textView = viewGroupContainsItem.findViewWithTag(viewTag);
textView.setText(value);
}
public static ViewGroup makeItem(
Context viewContext,
String labelText,
final String viewTag,
String value
) {
final ViewGroup itemView = makeItemView(viewContext, labelText, viewTag);
setItemValue(itemView, viewTag, value);
return itemView;
}
public static ViewGroup makeItem(
Context viewContext,
String labelText,
final String viewTag,
AsyncGetValue asyncGetValue
) {
final ViewGroup itemView = makeItemView(viewContext, labelText, viewTag);
asyncGetValue.getValue(new AsyncGetValueCallback() {
@Override
public void onGotValue(String value) {
setItemValue(itemView, viewTag, value);
}
});
return itemView;
}
interface AsyncGetValue {
void getValue(AsyncGetValueCallback callback);
}
interface AsyncGetValueCallback {
void onGotValue(String value);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/drawable/child_bg.xml
================================================
-
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/layout/layout_case_category_item.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/layout/layout_case_item.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/layout/layout_fragment_case_summary.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/layout/layout_main.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/layout/layout_main_above.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/layout/layout_main_behind.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/layout/layout_splash.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/values/dimens.xml
================================================
40dp >
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/values/strings.xml
================================================
sample-base-lib
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/values/themes.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-base-lib/src/main/res/xml/filepaths.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-loader/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-plugin/sample-loader/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
applicationId project.SAMPLE_HOST_APP_APPLICATION_ID
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
}
dependencies {
implementation 'com.tencent.shadow.core:loader'
implementation 'com.tencent.shadow.dynamic:dynamic-loader'
implementation 'com.tencent.shadow.dynamic:dynamic-loader-impl'
implementation project(':sample-constant')
compileOnly 'com.tencent.shadow.core:runtime'
compileOnly 'com.tencent.shadow.core:activity-container'
compileOnly 'com.tencent.shadow.core:common'
//下面这行依赖是为了防止在proguard的时候找不到LoaderFactory接口
compileOnly 'com.tencent.shadow.dynamic:dynamic-host'
compileOnly files("${project(":sample-host-lib").getBuildDir()}/outputs/jar/sample-host-lib-debug.jar")
}
preBuild.dependsOn(":sample-host-lib:jarDebugPackage")
================================================
FILE: projects/sample/source/sample-plugin/sample-loader/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#kotlin一般性配置 START
-dontwarn kotlin.**
-keepclassmembers class **$WhenMappings {
;
}
-keepclassmembers class kotlin.Metadata {
public ;
}
#kotlin一般性配置 END
#kotlin优化性能 START
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
#kotlin优化性能 END
-keep class org.slf4j.**{*;}
-dontwarn org.slf4j.impl.**
-keep class com.tencent.shadow.dynamic.host.**{*;}
-keep class com.tencent.shadow.dynamic.impl.**{*;}
-keep class com.tencent.shadow.dynamic.loader.**{*;}
-keep class com.tencent.shadow.core.common.**{*;}
-keep class com.tencent.shadow.core.loader.**{*;}
-keep class com.tencent.shadow.core.runtime.**{*;}
-dontwarn com.tencent.shadow.dynamic.host.**
-dontwarn com.tencent.shadow.dynamic.impl.**
-dontwarn com.tencent.shadow.dynamic.loader.**
-dontwarn com.tencent.shadow.core.common.**
-dontwarn com.tencent.shadow.core.loader.**
================================================
FILE: projects/sample/source/sample-plugin/sample-loader/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-loader/src/main/java/com/tencent/shadow/dynamic/impl/WhiteList.java
================================================
/*
* 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.dynamic.impl;
/**
* 此类包名及类名固定
* classLoader的白名单
* PluginLoader可以加载宿主中位于白名单内的类
*/
public interface WhiteList {
String[] sWhiteList = new String[]
{
"com.tencent.shadow.sample.host.lib",
};
}
================================================
FILE: projects/sample/source/sample-plugin/sample-loader/src/main/java/com/tencent/shadow/dynamic/loader/impl/CoreLoaderFactoryImpl.java
================================================
/*
* 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.dynamic.loader.impl;
import android.content.Context;
import com.tencent.shadow.core.loader.ShadowPluginLoader;
import com.tencent.shadow.sample.plugin.loader.SamplePluginLoader;
/**
* 这个类的包名类名是固定的。
*
* 见com.tencent.shadow.dynamic.loader.impl.DynamicPluginLoader#CORE_LOADER_FACTORY_IMPL_NAME
*/
public class CoreLoaderFactoryImpl implements CoreLoaderFactory {
@Override
public ShadowPluginLoader build(Context hostAppContext) {
return new SamplePluginLoader(hostAppContext);
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-loader/src/main/java/com/tencent/shadow/sample/plugin/loader/SampleComponentManager.java
================================================
/*
* 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.sample.plugin.loader;
import android.content.ComponentName;
import android.content.Context;
import com.tencent.shadow.core.loader.infos.ContainerProviderInfo;
import com.tencent.shadow.core.loader.managers.ComponentManager;
public class SampleComponentManager extends ComponentManager {
/**
* dynamic-runtime-apk 模块中定义的壳子Activity,需要在宿主AndroidManifest.xml注册
*/
private static final String DEFAULT_ACTIVITY = "com.tencent.shadow.sample.plugin.runtime.PluginDefaultProxyActivity";
private static final String SINGLE_INSTANCE_ACTIVITY = "com.tencent.shadow.sample.plugin.runtime.PluginSingleInstance1ProxyActivity";
private static final String SINGLE_TASK_ACTIVITY = "com.tencent.shadow.sample.plugin.runtime.PluginSingleTask1ProxyActivity";
private Context context;
public SampleComponentManager(Context context) {
this.context = context;
}
/**
* 配置插件Activity 到 壳子Activity的对应关系
*
* @param pluginActivity 插件Activity
* @return 壳子Activity
*/
@Override
public ComponentName onBindContainerActivity(ComponentName pluginActivity) {
switch (pluginActivity.getClassName()) {
/**
* 这里配置对应的对应关系
*/
}
return new ComponentName(context, DEFAULT_ACTIVITY);
}
/**
* 配置对应宿主中预注册的壳子contentProvider的信息
*/
@Override
public ContainerProviderInfo onBindContainerContentProvider(ComponentName pluginContentProvider) {
return new ContainerProviderInfo(
"com.tencent.shadow.core.runtime.container.PluginContainerContentProvider",
context.getPackageName() + ".contentprovider.authority.dynamic");
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-loader/src/main/java/com/tencent/shadow/sample/plugin/loader/SamplePluginLoader.java
================================================
/*
* 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.sample.plugin.loader;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import com.tencent.shadow.core.common.InstalledApk;
import com.tencent.shadow.core.load_parameters.LoadParameters;
import com.tencent.shadow.core.loader.ShadowPluginLoader;
import com.tencent.shadow.core.loader.classloaders.PluginClassLoader;
import com.tencent.shadow.core.loader.exceptions.LoadPluginException;
import com.tencent.shadow.core.loader.infos.PluginParts;
import com.tencent.shadow.core.loader.managers.ComponentManager;
import com.tencent.shadow.sample.host.lib.LoadPluginCallback;
import java.util.concurrent.Future;
import static android.content.pm.PackageManager.GET_META_DATA;
public class SamplePluginLoader extends ShadowPluginLoader {
private final static String TAG = "shadow";
private ComponentManager componentManager;
public SamplePluginLoader(Context hostAppContext) {
super(hostAppContext);
componentManager = new SampleComponentManager(hostAppContext);
}
@Override
public ComponentManager getComponentManager() {
return componentManager;
}
@Override
public Future> loadPlugin(final InstalledApk installedApk) throws LoadPluginException {
LoadParameters loadParameters = getLoadParameters(installedApk);
final String partKey = loadParameters.partKey;
LoadPluginCallback.getCallback().beforeLoadPlugin(partKey);
final Future> future = super.loadPlugin(installedApk);
getMExecutorService().submit(new Runnable() {
@Override
public void run() {
try {
future.get();
PluginParts pluginParts = getPluginParts(partKey);
String packageName = pluginParts.getApplication().getPackageName();
ApplicationInfo applicationInfo = pluginParts.getPluginPackageManager().getApplicationInfo(packageName, GET_META_DATA);
PluginClassLoader classLoader = pluginParts.getClassLoader();
Resources resources = pluginParts.getResources();
LoadPluginCallback.getCallback().afterLoadPlugin(partKey, applicationInfo, classLoader, resources);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
return future;
}
@Override
public String getDelegateProviderKey() {
return "SAMPLE";
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-runtime/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-plugin/sample-runtime/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
applicationId project.SAMPLE_HOST_APP_APPLICATION_ID
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.create("release")
signingConfig.initWith(buildTypes.debug.signingConfig)
}
}
}
dependencies {
implementation 'com.tencent.shadow.core:activity-container'
}
================================================
FILE: projects/sample/source/sample-plugin/sample-runtime/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class org.slf4j.**{*;}
-dontwarn org.slf4j.impl.**
-keep class com.tencent.shadow.core.runtime.**{*;}
-keep class * extends com.tencent.shadow.core.runtime.container.PluginContainerActivity
================================================
FILE: projects/sample/source/sample-plugin/sample-runtime/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/sample-runtime/src/main/java/com/tencent/shadow/sample/plugin/runtime/PluginDefaultProxyActivity.java
================================================
/*
* 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.sample.plugin.runtime;
import android.annotation.SuppressLint;
import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
@SuppressLint("Registered")//无需注册在这个模块的Manifest中,要注册在宿主的Manifest中。
public class PluginDefaultProxyActivity extends PluginContainerActivity {
@Override
protected String getDelegateProviderKey() {
return "SAMPLE";
}
}
================================================
FILE: projects/sample/source/sample-plugin/sample-runtime/src/main/java/com/tencent/shadow/sample/plugin/runtime/PluginSingleInstance1ProxyActivity.java
================================================
/*
* 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.sample.plugin.runtime;
import android.annotation.SuppressLint;
import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
@SuppressLint("Registered")//无需注册在这个模块的Manifest中,要注册在宿主的Manifest中。
public class PluginSingleInstance1ProxyActivity extends PluginContainerActivity {
}
================================================
FILE: projects/sample/source/sample-plugin/sample-runtime/src/main/java/com/tencent/shadow/sample/plugin/runtime/PluginSingleTask1ProxyActivity.java
================================================
/*
* 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.sample.plugin.runtime;
import android.annotation.SuppressLint;
import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
@SuppressLint("Registered")//无需注册在这个模块的Manifest中,要注册在宿主的Manifest中。
public class PluginSingleTask1ProxyActivity extends PluginContainerActivity {
}
================================================
FILE: projects/sample/source/sample-plugin/third-party/pinnedheaderexpandablelistview/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-plugin/third-party/pinnedheaderexpandablelistview/LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2014 singwhatiwanna
https://github.com/singwhatiwanna
http://blog.csdn.net/singwhatiwanna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: projects/sample/source/sample-plugin/third-party/pinnedheaderexpandablelistview/README.md
================================================
https://github.com/singwhatiwanna/PinnedHeaderExpandableListView
================================================
FILE: projects/sample/source/sample-plugin/third-party/pinnedheaderexpandablelistview/build.gradle
================================================
apply plugin: 'com.android.library'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
}
dependencies {
}
================================================
FILE: projects/sample/source/sample-plugin/third-party/pinnedheaderexpandablelistview/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: projects/sample/source/sample-plugin/third-party/pinnedheaderexpandablelistview/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/third-party/pinnedheaderexpandablelistview/src/main/java/com/ryg/expandable/ui/PinnedHeaderExpandableListView.java
================================================
/**
* The MIT License (MIT)
*
* Copyright (c) 2014 singwhatiwanna
* https://github.com/singwhatiwanna
* http://blog.csdn.net/singwhatiwanna
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.ryg.expandable.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ExpandableListView;
public class PinnedHeaderExpandableListView extends ExpandableListView implements OnScrollListener {
private static final String TAG = "PinnedHeaderExpandableListView";
private static final boolean DEBUG = true;
public interface OnHeaderUpdateListener {
/**
* 返回一个view对象即可
* 注意:view必须要有LayoutParams
*/
public View getPinnedHeader();
public void updatePinnedHeader(View headerView, int firstVisibleGroupPos);
}
private View mHeaderView;
private int mHeaderWidth;
private int mHeaderHeight;
private View mTouchTarget;
private OnScrollListener mScrollListener;
private OnHeaderUpdateListener mHeaderUpdateListener;
private boolean mActionDownHappened = false;
protected boolean mIsHeaderGroupClickable = true;
public PinnedHeaderExpandableListView(Context context) {
super(context);
initView();
}
public PinnedHeaderExpandableListView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public PinnedHeaderExpandableListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
private void initView() {
setFadingEdgeLength(0);
setOnScrollListener(this);
}
@Override
public void setOnScrollListener(OnScrollListener l) {
if (l != this) {
mScrollListener = l;
} else {
mScrollListener = null;
}
super.setOnScrollListener(this);
}
/**
* 给group添加点击事件监听
*
* @param onGroupClickListener 监听
* @param isHeaderGroupClickable 表示header是否可点击
* note : 当不想group可点击的时候,需要在OnGroupClickListener#onGroupClick中返回true,
* 并将isHeaderGroupClickable设为false即可
*/
public void setOnGroupClickListener(OnGroupClickListener onGroupClickListener, boolean isHeaderGroupClickable) {
mIsHeaderGroupClickable = isHeaderGroupClickable;
super.setOnGroupClickListener(onGroupClickListener);
}
public void setOnHeaderUpdateListener(OnHeaderUpdateListener listener) {
mHeaderUpdateListener = listener;
if (listener == null) {
mHeaderView = null;
mHeaderWidth = mHeaderHeight = 0;
return;
}
mHeaderView = listener.getPinnedHeader();
int firstVisiblePos = getFirstVisiblePosition();
int firstVisibleGroupPos = getPackedPositionGroup(getExpandableListPosition(firstVisiblePos));
listener.updatePinnedHeader(mHeaderView, firstVisibleGroupPos);
requestLayout();
postInvalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mHeaderView == null) {
return;
}
measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
mHeaderWidth = mHeaderView.getMeasuredWidth();
mHeaderHeight = mHeaderView.getMeasuredHeight();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mHeaderView == null) {
return;
}
int delta = mHeaderView.getTop();
mHeaderView.layout(0, delta, mHeaderWidth, mHeaderHeight + delta);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mHeaderView != null) {
drawChild(canvas, mHeaderView, getDrawingTime());
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
int pos = pointToPosition(x, y);
if (mHeaderView != null && y >= mHeaderView.getTop() && y <= mHeaderView.getBottom()) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mTouchTarget = getTouchTarget(mHeaderView, x, y);
mActionDownHappened = true;
} else if (ev.getAction() == MotionEvent.ACTION_UP) {
View touchTarget = getTouchTarget(mHeaderView, x, y);
if (touchTarget == mTouchTarget && mTouchTarget.isClickable()) {
mTouchTarget.performClick();
invalidate(new Rect(0, 0, mHeaderWidth, mHeaderHeight));
} else if (mIsHeaderGroupClickable) {
int groupPosition = getPackedPositionGroup(getExpandableListPosition(pos));
if (groupPosition != INVALID_POSITION && mActionDownHappened) {
if (isGroupExpanded(groupPosition)) {
collapseGroup(groupPosition);
} else {
expandGroup(groupPosition);
}
}
}
mActionDownHappened = false;
}
return true;
}
return super.dispatchTouchEvent(ev);
}
private View getTouchTarget(View view, int x, int y) {
if (!(view instanceof ViewGroup)) {
return view;
}
ViewGroup parent = (ViewGroup) view;
int childrenCount = parent.getChildCount();
final boolean customOrder = isChildrenDrawingOrderEnabled();
View target = null;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = parent.getChildAt(childIndex);
if (isTouchPointInView(child, x, y)) {
target = child;
break;
}
}
if (target == null) {
target = parent;
}
return target;
}
private boolean isTouchPointInView(View view, int x, int y) {
if (view.isClickable() && y >= view.getTop() && y <= view.getBottom()
&& x >= view.getLeft() && x <= view.getRight()) {
return true;
}
return false;
}
public void requestRefreshHeader() {
refreshHeader();
invalidate(new Rect(0, 0, mHeaderWidth, mHeaderHeight));
}
protected void refreshHeader() {
if (mHeaderView == null) {
return;
}
int firstVisiblePos = getFirstVisiblePosition();
int pos = firstVisiblePos + 1;
int firstVisibleGroupPos = getPackedPositionGroup(getExpandableListPosition(firstVisiblePos));
int group = getPackedPositionGroup(getExpandableListPosition(pos));
if (DEBUG) {
Log.d(TAG, "refreshHeader firstVisibleGroupPos=" + firstVisibleGroupPos);
}
if (group == firstVisibleGroupPos + 1) {
View view = getChildAt(1);
if (view == null) {
Log.w(TAG, "Warning : refreshHeader getChildAt(1)=null");
return;
}
if (view.getTop() <= mHeaderHeight) {
int delta = mHeaderHeight - view.getTop();
mHeaderView.layout(0, -delta, mHeaderWidth, mHeaderHeight - delta);
} else {
mHeaderView.layout(0, 0, mHeaderWidth, mHeaderHeight);
}
} else {
mHeaderView.layout(0, 0, mHeaderWidth, mHeaderHeight);
}
if (mHeaderUpdateListener != null) {
mHeaderUpdateListener.updatePinnedHeader(mHeaderView, firstVisibleGroupPos);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (mScrollListener != null) {
mScrollListener.onScrollStateChanged(view, scrollState);
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (totalItemCount > 0) {
refreshHeader();
}
if (mScrollListener != null) {
mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
}
================================================
FILE: projects/sample/source/sample-plugin/third-party/pinnedheaderexpandablelistview/src/main/java/com/ryg/expandable/ui/StickyLayout.java
================================================
/**
* The MIT License (MIT)
*
* Copyright (c) 2014 singwhatiwanna
* https://github.com/singwhatiwanna
* http://blog.csdn.net/singwhatiwanna
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.ryg.expandable.ui;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.LinearLayout;
import java.util.NoSuchElementException;
public class StickyLayout extends LinearLayout {
private static final String TAG = "StickyLayout";
private static final boolean DEBUG = true;
public interface OnGiveUpTouchEventListener {
public boolean giveUpTouchEvent(MotionEvent event);
}
private View mHeader;
private View mContent;
private OnGiveUpTouchEventListener mGiveUpTouchEventListener;
// header的高度 单位:px
private int mOriginalHeaderHeight;
private int mHeaderHeight;
private int mStatus = STATUS_EXPANDED;
public static final int STATUS_EXPANDED = 1;
public static final int STATUS_COLLAPSED = 2;
private int mTouchSlop;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
// 分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
// 用来控制滑动角度,仅当角度a满足如下条件才进行滑动:tan a = deltaX / deltaY > 2
private static final int TAN = 2;
private boolean mIsSticky = true;
private boolean mInitDataSucceed = false;
private boolean mDisallowInterceptTouchEventOnHeader = true;
public StickyLayout(Context context) {
super(context);
}
public StickyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public StickyLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (hasWindowFocus && (mHeader == null || mContent == null)) {
initData();
}
}
private void initData() {
int headerId = getResources().getIdentifier("sticky_header", "id", getContext().getPackageName());
int contentId = getResources().getIdentifier("sticky_content", "id", getContext().getPackageName());
if (headerId != 0 && contentId != 0) {
mHeader = findViewById(headerId);
mContent = findViewById(contentId);
mOriginalHeaderHeight = mHeader.getMeasuredHeight();
mHeaderHeight = mOriginalHeaderHeight;
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
if (mHeaderHeight > 0) {
mInitDataSucceed = true;
}
if (DEBUG) {
Log.d(TAG, "mTouchSlop = " + mTouchSlop + "mHeaderHeight = " + mHeaderHeight);
}
} else {
throw new NoSuchElementException("Did your view with id \"sticky_header\" or \"sticky_content\" exists?");
}
}
public void setOnGiveUpTouchEventListener(OnGiveUpTouchEventListener l) {
mGiveUpTouchEventListener = l;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int intercepted = 0;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mLastXIntercept = x;
mLastYIntercept = y;
mLastX = x;
mLastY = y;
intercepted = 0;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (mDisallowInterceptTouchEventOnHeader && y <= getHeaderHeight()) {
intercepted = 0;
} else if (Math.abs(deltaY) <= Math.abs(deltaX)) {
intercepted = 0;
} else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {
intercepted = 1;
} else if (mGiveUpTouchEventListener != null) {
if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) {
intercepted = 1;
}
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = 0;
mLastXIntercept = mLastYIntercept = 0;
break;
}
default:
break;
}
if (DEBUG) {
Log.d(TAG, "intercepted=" + intercepted);
}
return intercepted != 0 && mIsSticky;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mIsSticky) {
return true;
}
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (DEBUG) {
Log.d(TAG, "mHeaderHeight=" + mHeaderHeight + " deltaY=" + deltaY + " mlastY=" + mLastY);
}
mHeaderHeight += deltaY;
setHeaderHeight(mHeaderHeight);
break;
}
case MotionEvent.ACTION_UP: {
// 这里做了下判断,当松开手的时候,会自动向两边滑动,具体向哪边滑,要看当前所处的位置
int destHeight = 0;
if (mHeaderHeight <= mOriginalHeaderHeight * 0.5) {
destHeight = 0;
mStatus = STATUS_COLLAPSED;
} else {
destHeight = mOriginalHeaderHeight;
mStatus = STATUS_EXPANDED;
}
// 慢慢滑向终点
this.smoothSetHeaderHeight(mHeaderHeight, destHeight, 500);
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
public void smoothSetHeaderHeight(final int from, final int to, long duration) {
smoothSetHeaderHeight(from, to, duration, false);
}
public void smoothSetHeaderHeight(final int from, final int to, long duration, final boolean modifyOriginalHeaderHeight) {
final int frameCount = (int) (duration / 1000f * 30) + 1;
final float partation = (to - from) / (float) frameCount;
new Thread("Thread#smoothSetHeaderHeight") {
@Override
public void run() {
for (int i = 0; i < frameCount; i++) {
final int height;
if (i == frameCount - 1) {
height = to;
} else {
height = (int) (from + partation * i);
}
post(new Runnable() {
public void run() {
setHeaderHeight(height);
}
});
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (modifyOriginalHeaderHeight) {
setOriginalHeaderHeight(to);
}
}
;
}.start();
}
public void setOriginalHeaderHeight(int originalHeaderHeight) {
mOriginalHeaderHeight = originalHeaderHeight;
}
public void setHeaderHeight(int height, boolean modifyOriginalHeaderHeight) {
if (modifyOriginalHeaderHeight) {
setOriginalHeaderHeight(height);
}
setHeaderHeight(height);
}
public void setHeaderHeight(int height) {
if (!mInitDataSucceed) {
initData();
}
if (DEBUG) {
Log.d(TAG, "setHeaderHeight height=" + height);
}
if (height <= 0) {
height = 0;
} else if (height > mOriginalHeaderHeight) {
height = mOriginalHeaderHeight;
}
if (height == 0) {
mStatus = STATUS_COLLAPSED;
} else {
mStatus = STATUS_EXPANDED;
}
if (mHeader != null && mHeader.getLayoutParams() != null) {
mHeader.getLayoutParams().height = height;
mHeader.requestLayout();
mHeaderHeight = height;
} else {
if (DEBUG) {
Log.e(TAG, "null LayoutParams when setHeaderHeight");
}
}
}
public int getHeaderHeight() {
return mHeaderHeight;
}
public void setSticky(boolean isSticky) {
mIsSticky = isSticky;
}
public void requestDisallowInterceptTouchEventOnHeader(boolean disallowIntercept) {
mDisallowInterceptTouchEventOnHeader = disallowIntercept;
}
}
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/.gitignore
================================================
/build
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/LICENSE.txt
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/README.md
================================================
https://github.com/jfeinstein10/SlidingMenu
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/build.gradle
================================================
apply plugin: 'com.android.library'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
defaultConfig {
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies{
implementation 'com.android.support:support-fragment:27.0.2'
}
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/src/main/java/com/jeremyfeinstein/slidingmenu/lib/CanvasTransformerBuilder.java
================================================
package com.jeremyfeinstein.slidingmenu.lib;
import android.graphics.Canvas;
import android.view.animation.Interpolator;
import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu.CanvasTransformer;
public class CanvasTransformerBuilder {
private CanvasTransformer mTrans;
private static Interpolator lin = new Interpolator() {
public float getInterpolation(float t) {
return t;
}
};
private void initTransformer() {
if (mTrans == null)
mTrans = new CanvasTransformer() {
public void transformCanvas(Canvas canvas, float percentOpen) {
}
};
}
public CanvasTransformer zoom(final int openedX, final int closedX,
final int openedY, final int closedY,
final int px, final int py) {
return zoom(openedX, closedX, openedY, closedY, px, py, lin);
}
public CanvasTransformer zoom(final int openedX, final int closedX,
final int openedY, final int closedY,
final int px, final int py, final Interpolator interp) {
initTransformer();
mTrans = new CanvasTransformer() {
public void transformCanvas(Canvas canvas, float percentOpen) {
mTrans.transformCanvas(canvas, percentOpen);
float f = interp.getInterpolation(percentOpen);
canvas.scale((openedX - closedX) * f + closedX,
(openedY - closedY) * f + closedY, px, py);
}
};
return mTrans;
}
public CanvasTransformer rotate(final int openedDeg, final int closedDeg,
final int px, final int py) {
return rotate(openedDeg, closedDeg, px, py, lin);
}
public CanvasTransformer rotate(final int openedDeg, final int closedDeg,
final int px, final int py, final Interpolator interp) {
initTransformer();
mTrans = new CanvasTransformer() {
public void transformCanvas(Canvas canvas, float percentOpen) {
mTrans.transformCanvas(canvas, percentOpen);
float f = interp.getInterpolation(percentOpen);
canvas.rotate((openedDeg - closedDeg) * f + closedDeg,
px, py);
}
};
return mTrans;
}
public CanvasTransformer translate(final int openedX, final int closedX,
final int openedY, final int closedY) {
return translate(openedX, closedX, openedY, closedY, lin);
}
public CanvasTransformer translate(final int openedX, final int closedX,
final int openedY, final int closedY, final Interpolator interp) {
initTransformer();
mTrans = new CanvasTransformer() {
public void transformCanvas(Canvas canvas, float percentOpen) {
mTrans.transformCanvas(canvas, percentOpen);
float f = interp.getInterpolation(percentOpen);
canvas.translate((openedX - closedX) * f + closedX,
(openedY - closedY) * f + closedY);
}
};
return mTrans;
}
public CanvasTransformer concatTransformer(final CanvasTransformer t) {
initTransformer();
mTrans = new CanvasTransformer() {
public void transformCanvas(Canvas canvas, float percentOpen) {
mTrans.transformCanvas(canvas, percentOpen);
t.transformCanvas(canvas, percentOpen);
}
};
return mTrans;
}
}
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/src/main/java/com/jeremyfeinstein/slidingmenu/lib/CustomViewAbove.java
================================================
package com.jeremyfeinstein.slidingmenu.lib;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewConfigurationCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.FocusFinder;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.Scroller;
import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu.OnClosedListener;
import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu.OnOpenedListener;
import java.util.ArrayList;
import java.util.List;
//import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu.OnCloseListener;
//import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu.OnOpenListener;
public class CustomViewAbove extends ViewGroup {
private static final String TAG = "CustomViewAbove";
private static final boolean DEBUG = false;
private static final boolean USE_CACHE = false;
private static final int MAX_SETTLE_DURATION = 600; // ms
private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
private static final Interpolator sInterpolator = new Interpolator() {
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
private View mContent;
private int mCurItem;
private Scroller mScroller;
private boolean mScrollingCacheEnabled;
private boolean mScrolling;
private boolean mIsBeingDragged;
private boolean mIsUnableToDrag;
private int mTouchSlop;
private float mInitialMotionX;
/**
* Position of the last motion event.
*/
private float mLastMotionX;
private float mLastMotionY;
/**
* ID of the active pointer. This is used to retain consistency during
* drags/flings if multiple pointers are used.
*/
protected int mActivePointerId = INVALID_POINTER;
/**
* Sentinel value for no current active pointer.
* Used by {@link #mActivePointerId}.
*/
private static final int INVALID_POINTER = -1;
/**
* Determines speed during touch scrolling
*/
protected VelocityTracker mVelocityTracker;
private int mMinimumVelocity;
protected int mMaximumVelocity;
private int mFlingDistance;
private CustomViewBehind mViewBehind;
// private int mMode;
private boolean mEnabled = true;
private OnPageChangeListener mOnPageChangeListener;
private OnPageChangeListener mInternalPageChangeListener;
// private OnCloseListener mCloseListener;
// private OnOpenListener mOpenListener;
private OnClosedListener mClosedListener;
private OnOpenedListener mOpenedListener;
private List mIgnoredViews = new ArrayList();
// private int mScrollState = SCROLL_STATE_IDLE;
/**
* Callback interface for responding to changing state of the selected page.
*/
public interface OnPageChangeListener {
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
*
* @param position Position index of the first page currently being displayed.
* Page position+1 will be visible if positionOffset is nonzero.
* @param positionOffset Value from [0, 1) indicating the offset from the page at position.
* @param positionOffsetPixels Value in pixels indicating the offset from position.
*/
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
/**
* This method will be invoked when a new page becomes selected. Animation is not
* necessarily complete.
*
* @param position Position index of the new selected page.
*/
public void onPageSelected(int position);
}
/**
* Simple implementation of the {@link OnPageChangeListener} interface with stub
* implementations of each method. Extend this if you do not intend to override
* every method of {@link OnPageChangeListener}.
*/
public static class SimpleOnPageChangeListener implements OnPageChangeListener {
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// This space for rent
}
public void onPageSelected(int position) {
// This space for rent
}
public void onPageScrollStateChanged(int state) {
// This space for rent
}
}
public CustomViewAbove(Context context) {
this(context, null);
}
public CustomViewAbove(Context context, AttributeSet attrs) {
super(context, attrs);
initCustomViewAbove();
}
void initCustomViewAbove() {
setWillNotDraw(false);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setFocusable(true);
final Context context = getContext();
mScroller = new Scroller(context, sInterpolator);
final ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
setInternalPageChangeListener(new SimpleOnPageChangeListener() {
public void onPageSelected(int position) {
if (mViewBehind != null) {
switch (position) {
case 0:
case 2:
mViewBehind.setChildrenEnabled(true);
break;
case 1:
mViewBehind.setChildrenEnabled(false);
break;
}
}
}
});
final float density = context.getResources().getDisplayMetrics().density;
mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
}
/**
* Set the currently selected page. If the CustomViewPager has already been through its first
* layout there will be a smooth animated transition between the current item and the
* specified item.
*
* @param item Item index to select
*/
public void setCurrentItem(int item) {
setCurrentItemInternal(item, true, false);
}
/**
* Set the currently selected page.
*
* @param item Item index to select
* @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
*/
public void setCurrentItem(int item, boolean smoothScroll) {
setCurrentItemInternal(item, smoothScroll, false);
}
public int getCurrentItem() {
return mCurItem;
}
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
setCurrentItemInternal(item, smoothScroll, always, 0);
}
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
if (!always && mCurItem == item) {
setScrollingCacheEnabled(false);
return;
}
item = mViewBehind.getMenuPage(item);
final boolean dispatchSelected = mCurItem != item;
mCurItem = item;
final int destX = getDestScrollX(mCurItem);
if (dispatchSelected && mOnPageChangeListener != null) {
mOnPageChangeListener.onPageSelected(item);
}
if (dispatchSelected && mInternalPageChangeListener != null) {
mInternalPageChangeListener.onPageSelected(item);
}
if (smoothScroll) {
smoothScrollTo(destX, 0, velocity);
} else {
completeScroll();
scrollTo(destX, 0);
}
}
/**
* Set a listener that will be invoked whenever the page changes or is incrementally
* scrolled. See {@link OnPageChangeListener}.
*
* @param listener Listener to set
*/
public void setOnPageChangeListener(OnPageChangeListener listener) {
mOnPageChangeListener = listener;
}
/*
public void setOnOpenListener(OnOpenListener l) {
mOpenListener = l;
}
public void setOnCloseListener(OnCloseListener l) {
mCloseListener = l;
}
*/
public void setOnOpenedListener(OnOpenedListener l) {
mOpenedListener = l;
}
public void setOnClosedListener(OnClosedListener l) {
mClosedListener = l;
}
/**
* Set a separate OnPageChangeListener for internal use by the support library.
*
* @param listener Listener to set
* @return The old listener that was set, if any.
*/
OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) {
OnPageChangeListener oldListener = mInternalPageChangeListener;
mInternalPageChangeListener = listener;
return oldListener;
}
public void addIgnoredView(View v) {
if (!mIgnoredViews.contains(v)) {
mIgnoredViews.add(v);
}
}
public void removeIgnoredView(View v) {
mIgnoredViews.remove(v);
}
public void clearIgnoredViews() {
mIgnoredViews.clear();
}
// We want the duration of the page snap animation to be influenced by the distance that
// the screen has to travel, however, we don't want this duration to be effected in a
// purely linear fashion. Instead, we use this method to moderate the effect that the distance
// of travel has on the overall snap duration.
float distanceInfluenceForSnapDuration(float f) {
f -= 0.5f; // center the values about 0.
f *= 0.3f * Math.PI / 2.0f;
return (float) Math.sin(f);
}
public int getDestScrollX(int page) {
switch (page) {
case 0:
case 2:
return mViewBehind.getMenuLeft(mContent, page);
case 1:
return mContent.getLeft();
}
return 0;
}
private int getLeftBound() {
return mViewBehind.getAbsLeftBound(mContent);
}
private int getRightBound() {
return mViewBehind.getAbsRightBound(mContent);
}
public int getContentLeft() {
return mContent.getLeft() + mContent.getPaddingLeft();
}
public boolean isMenuOpen() {
return mCurItem == 0 || mCurItem == 2;
}
private boolean isInIgnoredView(MotionEvent ev) {
Rect rect = new Rect();
for (View v : mIgnoredViews) {
v.getHitRect(rect);
if (rect.contains((int) ev.getX(), (int) ev.getY())) return true;
}
return false;
}
public int getBehindWidth() {
if (mViewBehind == null) {
return 0;
} else {
return mViewBehind.getBehindWidth();
}
}
public int getChildWidth(int i) {
switch (i) {
case 0:
return getBehindWidth();
case 1:
return mContent.getWidth();
default:
return 0;
}
}
public boolean isSlidingEnabled() {
return mEnabled;
}
public void setSlidingEnabled(boolean b) {
mEnabled = b;
}
/**
* Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
*
* @param x the number of pixels to scroll by on the X axis
* @param y the number of pixels to scroll by on the Y axis
*/
void smoothScrollTo(int x, int y) {
smoothScrollTo(x, y, 0);
}
/**
* Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
*
* @param x the number of pixels to scroll by on the X axis
* @param y the number of pixels to scroll by on the Y axis
* @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
*/
void smoothScrollTo(int x, int y, int velocity) {
if (getChildCount() == 0) {
// Nothing to do.
setScrollingCacheEnabled(false);
return;
}
int sx = getScrollX();
int sy = getScrollY();
int dx = x - sx;
int dy = y - sy;
if (dx == 0 && dy == 0) {
completeScroll();
if (isMenuOpen()) {
if (mOpenedListener != null)
mOpenedListener.onOpened();
} else {
if (mClosedListener != null)
mClosedListener.onClosed();
}
return;
}
setScrollingCacheEnabled(true);
mScrolling = true;
final int width = getBehindWidth();
final int halfWidth = width / 2;
final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
final float distance = halfWidth + halfWidth *
distanceInfluenceForSnapDuration(distanceRatio);
int duration = 0;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float pageDelta = (float) Math.abs(dx) / width;
duration = (int) ((pageDelta + 1) * 100);
duration = MAX_SETTLE_DURATION;
}
duration = Math.min(duration, MAX_SETTLE_DURATION);
mScroller.startScroll(sx, sy, dx, dy, duration);
invalidate();
}
public void setContent(View v) {
if (mContent != null)
this.removeView(mContent);
mContent = v;
addView(mContent);
}
public View getContent() {
return mContent;
}
public void setCustomViewBehind(CustomViewBehind cvb) {
mViewBehind = cvb;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getDefaultSize(0, widthMeasureSpec);
int height = getDefaultSize(0, heightMeasureSpec);
setMeasuredDimension(width, height);
final int contentWidth = getChildMeasureSpec(widthMeasureSpec, 0, width);
final int contentHeight = getChildMeasureSpec(heightMeasureSpec, 0, height);
mContent.measure(contentWidth, contentHeight);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Make sure scroll position is set correctly.
if (w != oldw) {
// [ChrisJ] - This fixes the onConfiguration change for orientation issue..
// maybe worth having a look why the recomputeScroll pos is screwing
// up?
completeScroll();
scrollTo(getDestScrollX(mCurItem), getScrollY());
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = r - l;
final int height = b - t;
mContent.layout(0, 0, width, height);
}
public void setAboveOffset(int i) {
// RelativeLayout.LayoutParams params = ((RelativeLayout.LayoutParams)mContent.getLayoutParams());
// params.setMargins(i, params.topMargin, params.rightMargin, params.bottomMargin);
mContent.setPadding(i, mContent.getPaddingTop(),
mContent.getPaddingRight(), mContent.getPaddingBottom());
}
@Override
public void computeScroll() {
if (!mScroller.isFinished()) {
if (mScroller.computeScrollOffset()) {
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
scrollTo(x, y);
pageScrolled(x);
}
// Keep on drawing until the animation has finished.
invalidate();
return;
}
}
// Done with scroll, clean up state.
completeScroll();
}
private void pageScrolled(int xpos) {
final int widthWithMargin = getWidth();
final int position = xpos / widthWithMargin;
final int offsetPixels = xpos % widthWithMargin;
final float offset = (float) offsetPixels / widthWithMargin;
onPageScrolled(position, offset, offsetPixels);
}
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
* If you override this method you must call through to the superclass implementation
* (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
* returns.
*
* @param position Position index of the first page currently being displayed.
* Page position+1 will be visible if positionOffset is nonzero.
* @param offset Value from [0, 1) indicating the offset from the page at position.
* @param offsetPixels Value in pixels indicating the offset from position.
*/
protected void onPageScrolled(int position, float offset, int offsetPixels) {
if (mOnPageChangeListener != null) {
mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
}
if (mInternalPageChangeListener != null) {
mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
}
}
private void completeScroll() {
boolean needPopulate = mScrolling;
if (needPopulate) {
// Done with scroll, no longer want to cache view drawing.
setScrollingCacheEnabled(false);
mScroller.abortAnimation();
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
scrollTo(x, y);
}
if (isMenuOpen()) {
if (mOpenedListener != null)
mOpenedListener.onOpened();
} else {
if (mClosedListener != null)
mClosedListener.onClosed();
}
}
mScrolling = false;
}
protected int mTouchMode = SlidingMenu.TOUCHMODE_MARGIN;
public void setTouchMode(int i) {
mTouchMode = i;
}
public int getTouchMode() {
return mTouchMode;
}
private boolean thisTouchAllowed(MotionEvent ev) {
int x = (int) (ev.getX() + mScrollX);
if (isMenuOpen()) {
return mViewBehind.menuOpenTouchAllowed(mContent, mCurItem, x);
} else {
switch (mTouchMode) {
case SlidingMenu.TOUCHMODE_FULLSCREEN:
return !isInIgnoredView(ev);
case SlidingMenu.TOUCHMODE_NONE:
return false;
case SlidingMenu.TOUCHMODE_MARGIN:
return mViewBehind.marginTouchAllowed(mContent, x);
}
}
return false;
}
private boolean thisSlideAllowed(float dx) {
boolean allowed = false;
if (isMenuOpen()) {
allowed = mViewBehind.menuOpenSlideAllowed(dx);
} else {
allowed = mViewBehind.menuClosedSlideAllowed(dx);
}
if (DEBUG)
Log.v(TAG, "this slide allowed " + allowed + " dx: " + dx);
return allowed;
}
private int getPointerIndex(MotionEvent ev, int id) {
int activePointerIndex = MotionEventCompat.findPointerIndex(ev, id);
if (activePointerIndex == -1)
mActivePointerId = INVALID_POINTER;
return activePointerIndex;
}
private boolean mQuickReturn = false;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!mEnabled)
return false;
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
if (DEBUG)
if (action == MotionEvent.ACTION_DOWN)
Log.v(TAG, "Received ACTION_DOWN");
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP
|| (action != MotionEvent.ACTION_DOWN && mIsUnableToDrag)) {
endDrag();
return false;
}
switch (action) {
case MotionEvent.ACTION_MOVE:
determineDrag(ev);
break;
case MotionEvent.ACTION_DOWN:
int index = MotionEventCompat.getActionIndex(ev);
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
if (mActivePointerId == INVALID_POINTER)
break;
mLastMotionX = mInitialMotionX = MotionEventCompat.getX(ev, index);
mLastMotionY = MotionEventCompat.getY(ev, index);
if (thisTouchAllowed(ev)) {
mIsBeingDragged = false;
mIsUnableToDrag = false;
if (isMenuOpen() && mViewBehind.menuTouchInQuickReturn(mContent, mCurItem, ev.getX() + mScrollX)) {
mQuickReturn = true;
}
} else {
mIsUnableToDrag = true;
}
break;
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
if (!mIsBeingDragged) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
}
return mIsBeingDragged || mQuickReturn;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mEnabled)
return false;
if (!mIsBeingDragged && !thisTouchAllowed(ev))
return false;
// if (!mIsBeingDragged && !mQuickReturn)
// return false;
final int action = ev.getAction();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
completeScroll();
// Remember where the motion event started
int index = MotionEventCompat.getActionIndex(ev);
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
mLastMotionX = mInitialMotionX = ev.getX();
break;
case MotionEvent.ACTION_MOVE:
if (!mIsBeingDragged) {
determineDrag(ev);
if (mIsUnableToDrag)
return false;
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
final int activePointerIndex = getPointerIndex(ev, mActivePointerId);
if (mActivePointerId == INVALID_POINTER)
break;
final float x = MotionEventCompat.getX(ev, activePointerIndex);
final float deltaX = mLastMotionX - x;
mLastMotionX = x;
float oldScrollX = getScrollX();
float scrollX = oldScrollX + deltaX;
final float leftBound = getLeftBound();
final float rightBound = getRightBound();
if (scrollX < leftBound) {
scrollX = leftBound;
} else if (scrollX > rightBound) {
scrollX = rightBound;
}
// Don't lose the rounded component
mLastMotionX += scrollX - (int) scrollX;
scrollTo((int) scrollX, getScrollY());
pageScrolled((int) scrollX);
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
velocityTracker, mActivePointerId);
final int scrollX = getScrollX();
final float pageOffset = (float) (scrollX - getDestScrollX(mCurItem)) / getBehindWidth();
final int activePointerIndex = getPointerIndex(ev, mActivePointerId);
if (mActivePointerId != INVALID_POINTER) {
final float x = MotionEventCompat.getX(ev, activePointerIndex);
final int totalDelta = (int) (x - mInitialMotionX);
int nextPage = determineTargetPage(pageOffset, initialVelocity, totalDelta);
setCurrentItemInternal(nextPage, true, true, initialVelocity);
} else {
setCurrentItemInternal(mCurItem, true, true, initialVelocity);
}
mActivePointerId = INVALID_POINTER;
endDrag();
} else if (mQuickReturn && mViewBehind.menuTouchInQuickReturn(mContent, mCurItem, ev.getX() + mScrollX)) {
// close the menu
setCurrentItem(1);
endDrag();
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged) {
setCurrentItemInternal(mCurItem, true, true);
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int indexx = MotionEventCompat.getActionIndex(ev);
mLastMotionX = MotionEventCompat.getX(ev, indexx);
mActivePointerId = MotionEventCompat.getPointerId(ev, indexx);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
int pointerIndex = getPointerIndex(ev, mActivePointerId);
if (mActivePointerId == INVALID_POINTER)
break;
mLastMotionX = MotionEventCompat.getX(ev, pointerIndex);
break;
}
return true;
}
private void determineDrag(MotionEvent ev) {
final int activePointerId = mActivePointerId;
final int pointerIndex = getPointerIndex(ev, activePointerId);
if (activePointerId == INVALID_POINTER || pointerIndex == INVALID_POINTER)
return;
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float dx = x - mLastMotionX;
final float xDiff = Math.abs(dx);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float dy = y - mLastMotionY;
final float yDiff = Math.abs(dy);
if (xDiff > (isMenuOpen() ? mTouchSlop / 2 : mTouchSlop) && xDiff > yDiff && thisSlideAllowed(dx)) {
startDrag();
mLastMotionX = x;
mLastMotionY = y;
setScrollingCacheEnabled(true);
} else if (xDiff > mTouchSlop) {
mIsUnableToDrag = true;
}
}
@Override
public void scrollTo(int x, int y) {
super.scrollTo(x, y);
mScrollX = x;
mViewBehind.scrollBehindTo(mContent, x, y);
((SlidingMenu) getParent()).manageLayers(getPercentOpen());
}
private int determineTargetPage(float pageOffset, int velocity, int deltaX) {
int targetPage = mCurItem;
if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
if (velocity > 0 && deltaX > 0) {
targetPage -= 1;
} else if (velocity < 0 && deltaX < 0) {
targetPage += 1;
}
} else {
targetPage = (int) Math.round(mCurItem + pageOffset);
}
return targetPage;
}
protected float getPercentOpen() {
return Math.abs(mScrollX - mContent.getLeft()) / getBehindWidth();
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// Draw the margin drawable if needed.
mViewBehind.drawShadow(mContent, canvas);
mViewBehind.drawFade(mContent, canvas, getPercentOpen());
mViewBehind.drawSelector(mContent, canvas, getPercentOpen());
}
// variables for drawing
private float mScrollX = 0.0f;
private void onSecondaryPointerUp(MotionEvent ev) {
if (DEBUG) Log.v(TAG, "onSecondaryPointerUp called");
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
private void startDrag() {
mIsBeingDragged = true;
mQuickReturn = false;
}
private void endDrag() {
mQuickReturn = false;
mIsBeingDragged = false;
mIsUnableToDrag = false;
mActivePointerId = INVALID_POINTER;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void setScrollingCacheEnabled(boolean enabled) {
if (mScrollingCacheEnabled != enabled) {
mScrollingCacheEnabled = enabled;
if (USE_CACHE) {
final int size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
child.setDrawingCacheEnabled(enabled);
}
}
}
}
}
/**
* Tests scrollability within child views of v given a delta of dx.
*
* @param v View to test for horizontal scrollability
* @param checkV Whether the view v passed should itself be checked for scrollability (true),
* or just its children (false).
* @param dx Delta scrolled in pixels
* @param x X coordinate of the active touch point
* @param y Y coordinate of the active touch point
* @return true if child views of v can be scrolled by delta of dx.
*/
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
canScroll(child, true, dx, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
return checkV && ViewCompat.canScrollHorizontally(v, -dx);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Let the focused view and/or our descendants get the key first
return super.dispatchKeyEvent(event) || executeKeyEvent(event);
}
/**
* You can call this function yourself to have the scroll view perform
* scrolling from a key event, just as if the event had been dispatched to
* it by the view hierarchy.
*
* @param event The key event to execute.
* @return Return true if the event was handled, else false.
*/
public boolean executeKeyEvent(KeyEvent event) {
boolean handled = false;
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
handled = arrowScroll(FOCUS_LEFT);
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
handled = arrowScroll(FOCUS_RIGHT);
break;
case KeyEvent.KEYCODE_TAB:
if (Build.VERSION.SDK_INT >= 11) {
// The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
// before Android 3.0. Ignore the tab key on those devices.
if (event.hasNoModifiers()) {
handled = arrowScroll(FOCUS_FORWARD);
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
handled = arrowScroll(FOCUS_BACKWARD);
}
}
break;
}
}
return handled;
}
public boolean arrowScroll(int direction) {
View currentFocused = findFocus();
if (currentFocused == this) currentFocused = null;
boolean handled = false;
View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
direction);
if (nextFocused != null && nextFocused != currentFocused) {
if (direction == View.FOCUS_LEFT) {
handled = nextFocused.requestFocus();
} else if (direction == View.FOCUS_RIGHT) {
// If there is nothing to the right, or this is causing us to
// jump to the left, then what we really want to do is page right.
if (currentFocused != null && nextFocused.getLeft() <= currentFocused.getLeft()) {
handled = pageRight();
} else {
handled = nextFocused.requestFocus();
}
}
} else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
// Trying to move left and nothing there; try to page.
handled = pageLeft();
} else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
// Trying to move right and nothing there; try to page.
handled = pageRight();
}
if (handled) {
playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
}
return handled;
}
boolean pageLeft() {
if (mCurItem > 0) {
setCurrentItem(mCurItem - 1, true);
return true;
}
return false;
}
boolean pageRight() {
if (mCurItem < 1) {
setCurrentItem(mCurItem + 1, true);
return true;
}
return false;
}
}
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/src/main/java/com/jeremyfeinstein/slidingmenu/lib/CustomViewBehind.java
================================================
package com.jeremyfeinstein.slidingmenu.lib;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu.CanvasTransformer;
public class CustomViewBehind extends ViewGroup {
private static final String TAG = "CustomViewBehind";
private static final int MARGIN_THRESHOLD = 48; // dips
private int mTouchMode = SlidingMenu.TOUCHMODE_MARGIN;
private CustomViewAbove mViewAbove;
private View mContent;
private View mSecondaryContent;
private int mMarginThreshold;
private int mWidthOffset;
private CanvasTransformer mTransformer;
private boolean mChildrenEnabled;
public CustomViewBehind(Context context) {
this(context, null);
}
public CustomViewBehind(Context context, AttributeSet attrs) {
super(context, attrs);
mMarginThreshold = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
MARGIN_THRESHOLD, getResources().getDisplayMetrics());
}
public void setCustomViewAbove(CustomViewAbove customViewAbove) {
mViewAbove = customViewAbove;
}
public void setCanvasTransformer(CanvasTransformer t) {
mTransformer = t;
}
public void setWidthOffset(int i) {
mWidthOffset = i;
requestLayout();
}
public void setMarginThreshold(int marginThreshold) {
mMarginThreshold = marginThreshold;
}
public int getMarginThreshold() {
return mMarginThreshold;
}
public int getBehindWidth() {
return mContent.getWidth();
}
public void setContent(View v) {
if (mContent != null)
removeView(mContent);
mContent = v;
addView(mContent);
}
public View getContent() {
return mContent;
}
/**
* Sets the secondary (right) menu for use when setMode is called with SlidingMenu.LEFT_RIGHT.
*
* @param v the right menu
*/
public void setSecondaryContent(View v) {
if (mSecondaryContent != null)
removeView(mSecondaryContent);
mSecondaryContent = v;
addView(mSecondaryContent);
}
public View getSecondaryContent() {
return mSecondaryContent;
}
public void setChildrenEnabled(boolean enabled) {
mChildrenEnabled = enabled;
}
@Override
public void scrollTo(int x, int y) {
super.scrollTo(x, y);
if (mTransformer != null)
invalidate();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
return !mChildrenEnabled;
}
@Override
public boolean onTouchEvent(MotionEvent e) {
return !mChildrenEnabled;
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (mTransformer != null) {
canvas.save();
mTransformer.transformCanvas(canvas, mViewAbove.getPercentOpen());
super.dispatchDraw(canvas);
canvas.restore();
} else
super.dispatchDraw(canvas);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = r - l;
final int height = b - t;
mContent.layout(0, 0, width - mWidthOffset, height);
if (mSecondaryContent != null)
mSecondaryContent.layout(0, 0, width - mWidthOffset, height);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getDefaultSize(0, widthMeasureSpec);
int height = getDefaultSize(0, heightMeasureSpec);
setMeasuredDimension(width, height);
final int contentWidth = getChildMeasureSpec(widthMeasureSpec, 0, width - mWidthOffset);
final int contentHeight = getChildMeasureSpec(heightMeasureSpec, 0, height);
mContent.measure(contentWidth, contentHeight);
if (mSecondaryContent != null)
mSecondaryContent.measure(contentWidth, contentHeight);
}
private int mMode;
private boolean mFadeEnabled;
private final Paint mFadePaint = new Paint();
private float mScrollScale;
private Drawable mShadowDrawable;
private Drawable mSecondaryShadowDrawable;
private int mShadowWidth;
private float mFadeDegree;
public void setMode(int mode) {
if (mode == SlidingMenu.LEFT || mode == SlidingMenu.RIGHT) {
if (mContent != null)
mContent.setVisibility(View.VISIBLE);
if (mSecondaryContent != null)
mSecondaryContent.setVisibility(View.INVISIBLE);
}
mMode = mode;
}
public int getMode() {
return mMode;
}
public void setScrollScale(float scrollScale) {
mScrollScale = scrollScale;
}
public float getScrollScale() {
return mScrollScale;
}
public void setShadowDrawable(Drawable shadow) {
mShadowDrawable = shadow;
invalidate();
}
public void setSecondaryShadowDrawable(Drawable shadow) {
mSecondaryShadowDrawable = shadow;
invalidate();
}
public void setShadowWidth(int width) {
mShadowWidth = width;
invalidate();
}
public void setFadeEnabled(boolean b) {
mFadeEnabled = b;
}
public void setFadeDegree(float degree) {
if (degree > 1.0f || degree < 0.0f)
throw new IllegalStateException("The BehindFadeDegree must be between 0.0f and 1.0f");
mFadeDegree = degree;
}
public int getMenuPage(int page) {
page = (page > 1) ? 2 : ((page < 1) ? 0 : page);
if (mMode == SlidingMenu.LEFT && page > 1) {
return 0;
} else if (mMode == SlidingMenu.RIGHT && page < 1) {
return 2;
} else {
return page;
}
}
public void scrollBehindTo(View content, int x, int y) {
int vis = View.VISIBLE;
if (mMode == SlidingMenu.LEFT) {
if (x >= content.getLeft()) vis = View.INVISIBLE;
scrollTo((int) ((x + getBehindWidth()) * mScrollScale), y);
} else if (mMode == SlidingMenu.RIGHT) {
if (x <= content.getLeft()) vis = View.INVISIBLE;
scrollTo((int) (getBehindWidth() - getWidth() +
(x - getBehindWidth()) * mScrollScale), y);
} else if (mMode == SlidingMenu.LEFT_RIGHT) {
mContent.setVisibility(x >= content.getLeft() ? View.INVISIBLE : View.VISIBLE);
mSecondaryContent.setVisibility(x <= content.getLeft() ? View.INVISIBLE : View.VISIBLE);
vis = x == 0 ? View.INVISIBLE : View.VISIBLE;
if (x <= content.getLeft()) {
scrollTo((int) ((x + getBehindWidth()) * mScrollScale), y);
} else {
scrollTo((int) (getBehindWidth() - getWidth() +
(x - getBehindWidth()) * mScrollScale), y);
}
}
if (vis == View.INVISIBLE)
Log.v(TAG, "behind INVISIBLE");
setVisibility(vis);
}
public int getMenuLeft(View content, int page) {
if (mMode == SlidingMenu.LEFT) {
switch (page) {
case 0:
return content.getLeft() - getBehindWidth();
case 2:
return content.getLeft();
}
} else if (mMode == SlidingMenu.RIGHT) {
switch (page) {
case 0:
return content.getLeft();
case 2:
return content.getLeft() + getBehindWidth();
}
} else if (mMode == SlidingMenu.LEFT_RIGHT) {
switch (page) {
case 0:
return content.getLeft() - getBehindWidth();
case 2:
return content.getLeft() + getBehindWidth();
}
}
return content.getLeft();
}
public int getAbsLeftBound(View content) {
if (mMode == SlidingMenu.LEFT || mMode == SlidingMenu.LEFT_RIGHT) {
return content.getLeft() - getBehindWidth();
} else if (mMode == SlidingMenu.RIGHT) {
return content.getLeft();
}
return 0;
}
public int getAbsRightBound(View content) {
if (mMode == SlidingMenu.LEFT) {
return content.getLeft();
} else if (mMode == SlidingMenu.RIGHT || mMode == SlidingMenu.LEFT_RIGHT) {
return content.getLeft() + getBehindWidth();
}
return 0;
}
public boolean marginTouchAllowed(View content, int x) {
int left = content.getLeft();
int right = content.getRight();
if (mMode == SlidingMenu.LEFT) {
return (x >= left && x <= mMarginThreshold + left);
} else if (mMode == SlidingMenu.RIGHT) {
return (x <= right && x >= right - mMarginThreshold);
} else if (mMode == SlidingMenu.LEFT_RIGHT) {
return (x >= left && x <= mMarginThreshold + left) ||
(x <= right && x >= right - mMarginThreshold);
}
return false;
}
public void setTouchMode(int i) {
mTouchMode = i;
}
public boolean menuOpenTouchAllowed(View content, int currPage, float x) {
switch (mTouchMode) {
case SlidingMenu.TOUCHMODE_FULLSCREEN:
return true;
case SlidingMenu.TOUCHMODE_MARGIN:
return menuTouchInQuickReturn(content, currPage, x);
}
return false;
}
public boolean menuTouchInQuickReturn(View content, int currPage, float x) {
if (mMode == SlidingMenu.LEFT || (mMode == SlidingMenu.LEFT_RIGHT && currPage == 0)) {
return x >= content.getLeft();
} else if (mMode == SlidingMenu.RIGHT || (mMode == SlidingMenu.LEFT_RIGHT && currPage == 2)) {
return x <= content.getRight();
}
return false;
}
public boolean menuClosedSlideAllowed(float dx) {
if (mMode == SlidingMenu.LEFT) {
return dx > 0;
} else if (mMode == SlidingMenu.RIGHT) {
return dx < 0;
} else if (mMode == SlidingMenu.LEFT_RIGHT) {
return true;
}
return false;
}
public boolean menuOpenSlideAllowed(float dx) {
if (mMode == SlidingMenu.LEFT) {
return dx < 0;
} else if (mMode == SlidingMenu.RIGHT) {
return dx > 0;
} else if (mMode == SlidingMenu.LEFT_RIGHT) {
return true;
}
return false;
}
public void drawShadow(View content, Canvas canvas) {
if (mShadowDrawable == null || mShadowWidth <= 0) return;
int left = 0;
if (mMode == SlidingMenu.LEFT) {
left = content.getLeft() - mShadowWidth;
} else if (mMode == SlidingMenu.RIGHT) {
left = content.getRight();
} else if (mMode == SlidingMenu.LEFT_RIGHT) {
if (mSecondaryShadowDrawable != null) {
left = content.getRight();
mSecondaryShadowDrawable.setBounds(left, 0, left + mShadowWidth, getHeight());
mSecondaryShadowDrawable.draw(canvas);
}
left = content.getLeft() - mShadowWidth;
}
mShadowDrawable.setBounds(left, 0, left + mShadowWidth, getHeight());
mShadowDrawable.draw(canvas);
}
public void drawFade(View content, Canvas canvas, float openPercent) {
if (!mFadeEnabled) return;
final int alpha = (int) (mFadeDegree * 255 * Math.abs(1 - openPercent));
mFadePaint.setColor(Color.argb(alpha, 0, 0, 0));
int left = 0;
int right = 0;
if (mMode == SlidingMenu.LEFT) {
left = content.getLeft() - getBehindWidth();
right = content.getLeft();
} else if (mMode == SlidingMenu.RIGHT) {
left = content.getRight();
right = content.getRight() + getBehindWidth();
} else if (mMode == SlidingMenu.LEFT_RIGHT) {
left = content.getLeft() - getBehindWidth();
right = content.getLeft();
canvas.drawRect(left, 0, right, getHeight(), mFadePaint);
left = content.getRight();
right = content.getRight() + getBehindWidth();
}
canvas.drawRect(left, 0, right, getHeight(), mFadePaint);
}
private boolean mSelectorEnabled = true;
private Bitmap mSelectorDrawable;
private View mSelectedView;
public void drawSelector(View content, Canvas canvas, float openPercent) {
if (!mSelectorEnabled) return;
if (mSelectorDrawable != null && mSelectedView != null) {
String tag = (String) mSelectedView.getTag(R.id.selected_view);
if (tag.equals(TAG + "SelectedView")) {
canvas.save();
int left, right, offset;
offset = (int) (mSelectorDrawable.getWidth() * openPercent);
if (mMode == SlidingMenu.LEFT) {
right = content.getLeft();
left = right - offset;
canvas.clipRect(left, 0, right, getHeight());
canvas.drawBitmap(mSelectorDrawable, left, getSelectorTop(), null);
} else if (mMode == SlidingMenu.RIGHT) {
left = content.getRight();
right = left + offset;
canvas.clipRect(left, 0, right, getHeight());
canvas.drawBitmap(mSelectorDrawable, right - mSelectorDrawable.getWidth(), getSelectorTop(), null);
}
canvas.restore();
}
}
}
public void setSelectorEnabled(boolean b) {
mSelectorEnabled = b;
}
public void setSelectedView(View v) {
if (mSelectedView != null) {
mSelectedView.setTag(R.id.selected_view, null);
mSelectedView = null;
}
if (v != null && v.getParent() != null) {
mSelectedView = v;
mSelectedView.setTag(R.id.selected_view, TAG + "SelectedView");
invalidate();
}
}
private int getSelectorTop() {
int y = mSelectedView.getTop();
y += (mSelectedView.getHeight() - mSelectorDrawable.getHeight()) / 2;
return y;
}
public void setSelectorBitmap(Bitmap b) {
mSelectorDrawable = b;
refreshDrawableState();
}
}
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/src/main/java/com/jeremyfeinstein/slidingmenu/lib/SlidingMenu.java
================================================
package com.jeremyfeinstein.slidingmenu.lib;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import com.jeremyfeinstein.slidingmenu.lib.CustomViewAbove.OnPageChangeListener;
import java.lang.reflect.Method;
public class SlidingMenu extends RelativeLayout {
private static final String TAG = SlidingMenu.class.getSimpleName();
public static final int SLIDING_WINDOW = 0;
public static final int SLIDING_CONTENT = 1;
private boolean mActionbarOverlay = false;
/**
* Constant value for use with setTouchModeAbove(). Allows the SlidingMenu to be opened with a swipe
* gesture on the screen's margin
*/
public static final int TOUCHMODE_MARGIN = 0;
/**
* Constant value for use with setTouchModeAbove(). Allows the SlidingMenu to be opened with a swipe
* gesture anywhere on the screen
*/
public static final int TOUCHMODE_FULLSCREEN = 1;
/**
* Constant value for use with setTouchModeAbove(). Denies the SlidingMenu to be opened with a swipe
* gesture
*/
public static final int TOUCHMODE_NONE = 2;
/**
* Constant value for use with setMode(). Puts the menu to the left of the content.
*/
public static final int LEFT = 0;
/**
* Constant value for use with setMode(). Puts the menu to the right of the content.
*/
public static final int RIGHT = 1;
/**
* Constant value for use with setMode(). Puts menus to the left and right of the content.
*/
public static final int LEFT_RIGHT = 2;
private CustomViewAbove mViewAbove;
private CustomViewBehind mViewBehind;
private OnOpenListener mOpenListener;
private OnOpenListener mSecondaryOpenListner;
private OnCloseListener mCloseListener;
/**
* The listener interface for receiving onOpen events.
* The class that is interested in processing a onOpen
* event implements this interface, and the object created
* with that class is registered with a component using the
* component's addOnOpenListener method. When
* the onOpen event occurs, that object's appropriate
* method is invoked
*/
public interface OnOpenListener {
/**
* On open.
*/
public void onOpen();
}
/**
* The listener interface for receiving onOpened events.
* The class that is interested in processing a onOpened
* event implements this interface, and the object created
* with that class is registered with a component using the
* component's addOnOpenedListener method. When
* the onOpened event occurs, that object's appropriate
* method is invoked.
*
* @see OnOpenedEvent
*/
public interface OnOpenedListener {
/**
* On opened.
*/
public void onOpened();
}
/**
* The listener interface for receiving onClose events.
* The class that is interested in processing a onClose
* event implements this interface, and the object created
* with that class is registered with a component using the
* component's addOnCloseListener method. When
* the onClose event occurs, that object's appropriate
* method is invoked.
*
* @see OnCloseEvent
*/
public interface OnCloseListener {
/**
* On close.
*/
public void onClose();
}
/**
* The listener interface for receiving onClosed events.
* The class that is interested in processing a onClosed
* event implements this interface, and the object created
* with that class is registered with a component using the
* component's addOnClosedListener method. When
* the onClosed event occurs, that object's appropriate
* method is invoked.
*
* @see OnClosedEvent
*/
public interface OnClosedListener {
/**
* On closed.
*/
public void onClosed();
}
/**
* The Interface CanvasTransformer.
*/
public interface CanvasTransformer {
/**
* Transform canvas.
*
* @param canvas the canvas
* @param percentOpen the percent open
*/
public void transformCanvas(Canvas canvas, float percentOpen);
}
/**
* Instantiates a new SlidingMenu.
*
* @param context the associated Context
*/
public SlidingMenu(Context context) {
this(context, null);
}
/**
* Instantiates a new SlidingMenu and attach to Activity.
*
* @param activity the activity to attach slidingmenu
* @param slideStyle the slidingmenu style
*/
public SlidingMenu(Activity activity, int slideStyle) {
this(activity, null);
this.attachToActivity(activity, slideStyle);
}
/**
* Instantiates a new SlidingMenu.
*
* @param context the associated Context
* @param attrs the attrs
*/
public SlidingMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* Instantiates a new SlidingMenu.
*
* @param context the associated Context
* @param attrs the attrs
* @param defStyle the def style
*/
public SlidingMenu(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
LayoutParams behindParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
mViewBehind = new CustomViewBehind(context);
addView(mViewBehind, behindParams);
LayoutParams aboveParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
mViewAbove = new CustomViewAbove(context);
addView(mViewAbove, aboveParams);
// register the CustomViewBehind with the CustomViewAbove
mViewAbove.setCustomViewBehind(mViewBehind);
mViewBehind.setCustomViewAbove(mViewAbove);
mViewAbove.setOnPageChangeListener(new OnPageChangeListener() {
public static final int POSITION_OPEN = 0;
public static final int POSITION_CLOSE = 1;
public static final int POSITION_SECONDARY_OPEN = 2;
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
}
public void onPageSelected(int position) {
if (position == POSITION_OPEN && mOpenListener != null) {
mOpenListener.onOpen();
} else if (position == POSITION_CLOSE && mCloseListener != null) {
mCloseListener.onClose();
} else if (position == POSITION_SECONDARY_OPEN && mSecondaryOpenListner != null) {
mSecondaryOpenListner.onOpen();
}
}
});
// now style everything!
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingMenu);
// set the above and behind views if defined in xml
int mode = ta.getInt(R.styleable.SlidingMenu_mode, LEFT);
setMode(mode);
int viewAbove = ta.getResourceId(R.styleable.SlidingMenu_viewAbove, -1);
if (viewAbove != -1) {
setContent(viewAbove);
} else {
setContent(new FrameLayout(context));
}
int viewBehind = ta.getResourceId(R.styleable.SlidingMenu_viewBehind, -1);
if (viewBehind != -1) {
setMenu(viewBehind);
} else {
setMenu(new FrameLayout(context));
}
int touchModeAbove = ta.getInt(R.styleable.SlidingMenu_touchModeAbove, TOUCHMODE_MARGIN);
setTouchModeAbove(touchModeAbove);
int touchModeBehind = ta.getInt(R.styleable.SlidingMenu_touchModeBehind, TOUCHMODE_MARGIN);
setTouchModeBehind(touchModeBehind);
int offsetBehind = (int) ta.getDimension(R.styleable.SlidingMenu_behindOffset, -1);
int widthBehind = (int) ta.getDimension(R.styleable.SlidingMenu_behindWidth, -1);
if (offsetBehind != -1 && widthBehind != -1)
throw new IllegalStateException("Cannot set both behindOffset and behindWidth for a SlidingMenu");
else if (offsetBehind != -1)
setBehindOffset(offsetBehind);
else if (widthBehind != -1)
setBehindWidth(widthBehind);
else
setBehindOffset(0);
float scrollOffsetBehind = ta.getFloat(R.styleable.SlidingMenu_behindScrollScale, 0.33f);
setBehindScrollScale(scrollOffsetBehind);
int shadowRes = ta.getResourceId(R.styleable.SlidingMenu_shadowDrawable, -1);
if (shadowRes != -1) {
setShadowDrawable(shadowRes);
}
int shadowWidth = (int) ta.getDimension(R.styleable.SlidingMenu_shadowWidth, 0);
setShadowWidth(shadowWidth);
boolean fadeEnabled = ta.getBoolean(R.styleable.SlidingMenu_fadeEnabled, true);
setFadeEnabled(fadeEnabled);
float fadeDeg = ta.getFloat(R.styleable.SlidingMenu_fadeDegree, 0.33f);
setFadeDegree(fadeDeg);
boolean selectorEnabled = ta.getBoolean(R.styleable.SlidingMenu_selectorEnabled, false);
setSelectorEnabled(selectorEnabled);
int selectorRes = ta.getResourceId(R.styleable.SlidingMenu_selectorDrawable, -1);
if (selectorRes != -1)
setSelectorDrawable(selectorRes);
ta.recycle();
}
/**
* Attaches the SlidingMenu to an entire Activity
*
* @param activity the Activity
* @param slideStyle either SLIDING_CONTENT or SLIDING_WINDOW
*/
public void attachToActivity(Activity activity, int slideStyle) {
attachToActivity(activity, slideStyle, false);
}
/**
* Attaches the SlidingMenu to an entire Activity
*
* @param activity the Activity
* @param slideStyle either SLIDING_CONTENT or SLIDING_WINDOW
* @param actionbarOverlay whether or not the ActionBar is overlaid
*/
public void attachToActivity(Activity activity, int slideStyle, boolean actionbarOverlay) {
if (slideStyle != SLIDING_WINDOW && slideStyle != SLIDING_CONTENT)
throw new IllegalArgumentException("slideStyle must be either SLIDING_WINDOW or SLIDING_CONTENT");
if (getParent() != null)
throw new IllegalStateException("This SlidingMenu appears to already be attached");
// get the window background
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{android.R.attr.windowBackground});
int background = a.getResourceId(0, 0);
a.recycle();
switch (slideStyle) {
case SLIDING_WINDOW:
mActionbarOverlay = false;
ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
// save ActionBar themes that have transparent assets
decorChild.setBackgroundResource(background);
decor.removeView(decorChild);
decor.addView(this);
setContent(decorChild);
break;
case SLIDING_CONTENT:
mActionbarOverlay = actionbarOverlay;
// take the above view out of
ViewGroup contentParent = (ViewGroup) activity.findViewById(android.R.id.content);
View content = contentParent.getChildAt(0);
contentParent.removeView(content);
contentParent.addView(this);
setContent(content);
// save people from having transparent backgrounds
if (content.getBackground() == null)
content.setBackgroundResource(background);
break;
}
}
/**
* Set the above view content from a layout resource. The resource will be inflated, adding all top-level views
* to the above view.
*
* @param res the new content
*/
public void setContent(int res) {
setContent(LayoutInflater.from(getContext()).inflate(res, null));
}
/**
* Set the above view content to the given View.
*
* @param view The desired content to display.
*/
public void setContent(View view) {
mViewAbove.setContent(view);
showContent();
}
/**
* Retrieves the current content.
*
* @return the current content
*/
public View getContent() {
return mViewAbove.getContent();
}
/**
* Set the behind view (menu) content from a layout resource. The resource will be inflated, adding all top-level views
* to the behind view.
*
* @param res the new content
*/
public void setMenu(int res) {
setMenu(LayoutInflater.from(getContext()).inflate(res, null));
}
/**
* Set the behind view (menu) content to the given View.
*
* @param view The desired content to display.
*/
public void setMenu(View v) {
mViewBehind.setContent(v);
}
/**
* Retrieves the layout_main_above menu.
*
* @return the layout_main_above menu
*/
public View getMenu() {
return mViewBehind.getContent();
}
/**
* Set the secondary behind view (right menu) content from a layout resource. The resource will be inflated, adding all top-level views
* to the behind view.
*
* @param res the new content
*/
public void setSecondaryMenu(int res) {
setSecondaryMenu(LayoutInflater.from(getContext()).inflate(res, null));
}
/**
* Set the secondary behind view (right menu) content to the given View.
*
* @param view The desired content to display.
*/
public void setSecondaryMenu(View v) {
mViewBehind.setSecondaryContent(v);
// mViewBehind.invalidate();
}
/**
* Retrieves the current secondary menu (right).
*
* @return the current menu
*/
public View getSecondaryMenu() {
return mViewBehind.getSecondaryContent();
}
/**
* Sets the sliding enabled.
*
* @param b true to enable sliding, false to disable it.
*/
public void setSlidingEnabled(boolean b) {
mViewAbove.setSlidingEnabled(b);
}
/**
* Checks if is sliding enabled.
*
* @return true, if is sliding enabled
*/
public boolean isSlidingEnabled() {
return mViewAbove.isSlidingEnabled();
}
/**
* Sets which side the SlidingMenu should appear on.
*
* @param mode must be either SlidingMenu.LEFT or SlidingMenu.RIGHT
*/
public void setMode(int mode) {
if (mode != LEFT && mode != RIGHT && mode != LEFT_RIGHT) {
throw new IllegalStateException("SlidingMenu mode must be LEFT, RIGHT, or LEFT_RIGHT");
}
mViewBehind.setMode(mode);
}
/**
* Returns the current side that the SlidingMenu is on.
*
* @return the current mode, either SlidingMenu.LEFT or SlidingMenu.RIGHT
*/
public int getMode() {
return mViewBehind.getMode();
}
/**
* Sets whether or not the SlidingMenu is in static mode (i.e. nothing is moving and everything is showing)
*
* @param b true to set static mode, false to disable static mode.
*/
public void setStatic(boolean b) {
if (b) {
setSlidingEnabled(false);
mViewAbove.setCustomViewBehind(null);
mViewAbove.setCurrentItem(1);
// mViewBehind.setCurrentItem(0);
} else {
mViewAbove.setCurrentItem(1);
// mViewBehind.setCurrentItem(1);
mViewAbove.setCustomViewBehind(mViewBehind);
setSlidingEnabled(true);
}
}
/**
* Opens the menu and shows the menu view.
*/
public void showMenu() {
showMenu(true);
}
/**
* Opens the menu and shows the menu view.
*
* @param animate true to animate the transition, false to ignore animation
*/
public void showMenu(boolean animate) {
mViewAbove.setCurrentItem(0, animate);
}
/**
* Opens the menu and shows the secondary menu view. Will default to the regular menu
* if there is only one.
*/
public void showSecondaryMenu() {
showSecondaryMenu(true);
}
/**
* Opens the menu and shows the secondary (right) menu view. Will default to the regular menu
* if there is only one.
*
* @param animate true to animate the transition, false to ignore animation
*/
public void showSecondaryMenu(boolean animate) {
mViewAbove.setCurrentItem(2, animate);
}
/**
* Closes the menu and shows the above view.
*/
public void showContent() {
showContent(true);
}
/**
* Closes the menu and shows the above view.
*
* @param animate true to animate the transition, false to ignore animation
*/
public void showContent(boolean animate) {
mViewAbove.setCurrentItem(1, animate);
}
/**
* Toggle the SlidingMenu. If it is open, it will be closed, and vice versa.
*/
public void toggle() {
toggle(true);
}
/**
* Toggle the SlidingMenu. If it is open, it will be closed, and vice versa.
*
* @param animate true to animate the transition, false to ignore animation
*/
public void toggle(boolean animate) {
if (isMenuShowing()) {
showContent(animate);
} else {
showMenu(animate);
}
}
/**
* Checks if is the behind view showing.
*
* @return Whether or not the behind view is showing
*/
public boolean isMenuShowing() {
return mViewAbove.getCurrentItem() == 0 || mViewAbove.getCurrentItem() == 2;
}
/**
* Checks if is the behind view showing.
*
* @return Whether or not the behind view is showing
*/
public boolean isSecondaryMenuShowing() {
return mViewAbove.getCurrentItem() == 2;
}
/**
* Gets the behind offset.
*
* @return The margin on the right of the screen that the behind view scrolls to
*/
public int getBehindOffset() {
return ((LayoutParams) mViewBehind.getLayoutParams()).rightMargin;
}
/**
* Sets the behind offset.
*
* @param i The margin, in pixels, on the right of the screen that the behind view scrolls to.
*/
public void setBehindOffset(int i) {
// RelativeLayout.LayoutParams params = ((RelativeLayout.LayoutParams)mViewBehind.getLayoutParams());
// int bottom = params.bottomMargin;
// int top = params.topMargin;
// int left = params.leftMargin;
// params.setMargins(left, top, i, bottom);
mViewBehind.setWidthOffset(i);
}
/**
* Sets the behind offset.
*
* @param resID The dimension resource id to be set as the behind offset.
* The menu, when open, will leave this width margin on the right of the screen.
*/
public void setBehindOffsetRes(int resID) {
int i = (int) getContext().getResources().getDimension(resID);
setBehindOffset(i);
}
/**
* Sets the above offset.
*
* @param i the new above offset, in pixels
*/
public void setAboveOffset(int i) {
mViewAbove.setAboveOffset(i);
}
/**
* Sets the above offset.
*
* @param resID The dimension resource id to be set as the above offset.
*/
public void setAboveOffsetRes(int resID) {
int i = (int) getContext().getResources().getDimension(resID);
setAboveOffset(i);
}
/**
* Sets the behind width.
*
* @param i The width the Sliding Menu will open to, in pixels
*/
@SuppressWarnings("deprecation")
public void setBehindWidth(int i) {
int width;
Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
try {
Class> cls = Display.class;
Class>[] parameterTypes = {Point.class};
Point parameter = new Point();
Method method = cls.getMethod("getSize", parameterTypes);
method.invoke(display, parameter);
width = parameter.x;
} catch (Exception e) {
width = display.getWidth();
}
setBehindOffset(width - i);
}
/**
* Sets the behind width.
*
* @param res The dimension resource id to be set as the behind width offset.
* The menu, when open, will open this wide.
*/
public void setBehindWidthRes(int res) {
int i = (int) getContext().getResources().getDimension(res);
setBehindWidth(i);
}
/**
* Gets the behind scroll scale.
*
* @return The scale of the parallax scroll
*/
public float getBehindScrollScale() {
return mViewBehind.getScrollScale();
}
/**
* Gets the touch mode margin threshold
*
* @return the touch mode margin threshold
*/
public int getTouchmodeMarginThreshold() {
return mViewBehind.getMarginThreshold();
}
/**
* Set the touch mode margin threshold
*
* @param touchmodeMarginThreshold
*/
public void setTouchmodeMarginThreshold(int touchmodeMarginThreshold) {
mViewBehind.setMarginThreshold(touchmodeMarginThreshold);
}
/**
* Sets the behind scroll scale.
*
* @param f The scale of the parallax scroll (i.e. 1.0f scrolls 1 pixel for every
* 1 pixel that the above view scrolls and 0.0f scrolls 0 pixels)
*/
public void setBehindScrollScale(float f) {
if (f < 0 && f > 1)
throw new IllegalStateException("ScrollScale must be between 0 and 1");
mViewBehind.setScrollScale(f);
}
/**
* Sets the behind canvas transformer.
*
* @param t the new behind canvas transformer
*/
public void setBehindCanvasTransformer(CanvasTransformer t) {
mViewBehind.setCanvasTransformer(t);
}
/**
* Gets the touch mode above.
*
* @return the touch mode above
*/
public int getTouchModeAbove() {
return mViewAbove.getTouchMode();
}
/**
* Controls whether the SlidingMenu can be opened with a swipe gesture.
* Options are {@link #TOUCHMODE_MARGIN TOUCHMODE_MARGIN}, {@link #TOUCHMODE_FULLSCREEN TOUCHMODE_FULLSCREEN},
* or {@link #TOUCHMODE_NONE TOUCHMODE_NONE}
*
* @param i the new touch mode
*/
public void setTouchModeAbove(int i) {
if (i != TOUCHMODE_FULLSCREEN && i != TOUCHMODE_MARGIN
&& i != TOUCHMODE_NONE) {
throw new IllegalStateException("TouchMode must be set to either" +
"TOUCHMODE_FULLSCREEN or TOUCHMODE_MARGIN or TOUCHMODE_NONE.");
}
mViewAbove.setTouchMode(i);
}
/**
* Controls whether the SlidingMenu can be opened with a swipe gesture.
* Options are {@link #TOUCHMODE_MARGIN TOUCHMODE_MARGIN}, {@link #TOUCHMODE_FULLSCREEN TOUCHMODE_FULLSCREEN},
* or {@link #TOUCHMODE_NONE TOUCHMODE_NONE}
*
* @param i the new touch mode
*/
public void setTouchModeBehind(int i) {
if (i != TOUCHMODE_FULLSCREEN && i != TOUCHMODE_MARGIN
&& i != TOUCHMODE_NONE) {
throw new IllegalStateException("TouchMode must be set to either" +
"TOUCHMODE_FULLSCREEN or TOUCHMODE_MARGIN or TOUCHMODE_NONE.");
}
mViewBehind.setTouchMode(i);
}
/**
* Sets the shadow drawable.
*
* @param resId the resource ID of the new shadow drawable
*/
public void setShadowDrawable(int resId) {
setShadowDrawable(getContext().getResources().getDrawable(resId));
}
/**
* Sets the shadow drawable.
*
* @param d the new shadow drawable
*/
public void setShadowDrawable(Drawable d) {
mViewBehind.setShadowDrawable(d);
}
/**
* Sets the secondary (right) shadow drawable.
*
* @param resId the resource ID of the new shadow drawable
*/
public void setSecondaryShadowDrawable(int resId) {
setSecondaryShadowDrawable(getContext().getResources().getDrawable(resId));
}
/**
* Sets the secondary (right) shadow drawable.
*
* @param d the new shadow drawable
*/
public void setSecondaryShadowDrawable(Drawable d) {
mViewBehind.setSecondaryShadowDrawable(d);
}
/**
* Sets the shadow width.
*
* @param resId The dimension resource id to be set as the shadow width.
*/
public void setShadowWidthRes(int resId) {
setShadowWidth((int) getResources().getDimension(resId));
}
/**
* Sets the shadow width.
*
* @param pixels the new shadow width, in pixels
*/
public void setShadowWidth(int pixels) {
mViewBehind.setShadowWidth(pixels);
}
/**
* Enables or disables the SlidingMenu's fade in and out
*
* @param b true to enable fade, false to disable it
*/
public void setFadeEnabled(boolean b) {
mViewBehind.setFadeEnabled(b);
}
/**
* Sets how much the SlidingMenu fades in and out. Fade must be enabled, see
* {@link #setFadeEnabled(boolean) setFadeEnabled(boolean)}
*
* @param f the new fade degree, between 0.0f and 1.0f
*/
public void setFadeDegree(float f) {
mViewBehind.setFadeDegree(f);
}
/**
* Enables or disables whether the selector is drawn
*
* @param b true to draw the selector, false to not draw the selector
*/
public void setSelectorEnabled(boolean b) {
mViewBehind.setSelectorEnabled(true);
}
/**
* Sets the selected view. The selector will be drawn here
*
* @param v the new selected view
*/
public void setSelectedView(View v) {
mViewBehind.setSelectedView(v);
}
/**
* Sets the selector drawable.
*
* @param res a resource ID for the selector drawable
*/
public void setSelectorDrawable(int res) {
mViewBehind.setSelectorBitmap(BitmapFactory.decodeResource(getResources(), res));
}
/**
* Sets the selector drawable.
*
* @param b the new selector bitmap
*/
public void setSelectorBitmap(Bitmap b) {
mViewBehind.setSelectorBitmap(b);
}
/**
* Add a View ignored by the Touch Down event when mode is Fullscreen
*
* @param v a view to be ignored
*/
public void addIgnoredView(View v) {
mViewAbove.addIgnoredView(v);
}
/**
* Remove a View ignored by the Touch Down event when mode is Fullscreen
*
* @param v a view not wanted to be ignored anymore
*/
public void removeIgnoredView(View v) {
mViewAbove.removeIgnoredView(v);
}
/**
* Clear the list of Views ignored by the Touch Down event when mode is Fullscreen
*/
public void clearIgnoredViews() {
mViewAbove.clearIgnoredViews();
}
/**
* Sets the OnOpenListener. {@link OnOpenListener#onOpen() OnOpenListener.onOpen()} will be called when the SlidingMenu is opened
*
* @param listener the new OnOpenListener
*/
public void setOnOpenListener(OnOpenListener listener) {
//mViewAbove.setOnOpenListener(listener);
mOpenListener = listener;
}
/**
* Sets the OnOpenListner for secondary menu {@link OnOpenListener#onOpen() OnOpenListener.onOpen()} will be called when the secondary SlidingMenu is opened
*
* @param listener the new OnOpenListener
*/
public void setSecondaryOnOpenListner(OnOpenListener listener) {
mSecondaryOpenListner = listener;
}
/**
* Sets the OnCloseListener. {@link OnCloseListener#onClose() OnCloseListener.onClose()} will be called when any one of the SlidingMenu is closed
*
* @param listener the new setOnCloseListener
*/
public void setOnCloseListener(OnCloseListener listener) {
//mViewAbove.setOnCloseListener(listener);
mCloseListener = listener;
}
/**
* Sets the OnOpenedListener. {@link OnOpenedListener#onOpened() OnOpenedListener.onOpened()} will be called after the SlidingMenu is opened
*
* @param listener the new OnOpenedListener
*/
public void setOnOpenedListener(OnOpenedListener listener) {
mViewAbove.setOnOpenedListener(listener);
}
/**
* Sets the OnClosedListener. {@link OnClosedListener#onClosed() OnClosedListener.onClosed()} will be called after the SlidingMenu is closed
*
* @param listener the new OnClosedListener
*/
public void setOnClosedListener(OnClosedListener listener) {
mViewAbove.setOnClosedListener(listener);
}
public static class SavedState extends BaseSavedState {
private final int mItem;
public SavedState(Parcelable superState, int item) {
super(superState);
mItem = item;
}
private SavedState(Parcel in) {
super(in);
mItem = in.readInt();
}
public int getItem() {
return mItem;
}
/* (non-Javadoc)
* @see android.view.AbsSavedState#writeToParcel(android.os.Parcel, int)
*/
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(mItem);
}
public static final Creator CREATOR =
new Creator() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
/* (non-Javadoc)
* @see android.view.View#onSaveInstanceState()
*/
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState, mViewAbove.getCurrentItem());
return ss;
}
/* (non-Javadoc)
* @see android.view.View#onRestoreInstanceState(android.os.Parcelable)
*/
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mViewAbove.setCurrentItem(ss.getItem());
}
/* (non-Javadoc)
* @see android.view.ViewGroup#fitSystemWindows(android.graphics.Rect)
*/
@SuppressLint("NewApi")
@Override
protected boolean fitSystemWindows(Rect insets) {
int leftPadding = insets.left;
int rightPadding = insets.right;
int topPadding = insets.top;
int bottomPadding = insets.bottom;
if (!mActionbarOverlay) {
Log.v(TAG, "setting padding!");
setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
}
return true;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void manageLayers(float percentOpen) {
if (Build.VERSION.SDK_INT < 11) return;
boolean layer = percentOpen > 0.0f && percentOpen < 1.0f;
final int layerType = layer ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE;
if (layerType != getContent().getLayerType()) {
getHandler().post(new Runnable() {
public void run() {
Log.v(TAG, "changing layerType. hardware? " + (layerType == View.LAYER_TYPE_HARDWARE));
getContent().setLayerType(layerType, null);
getMenu().setLayerType(layerType, null);
if (getSecondaryMenu() != null) {
getSecondaryMenu().setLayerType(layerType, null);
}
}
});
}
}
}
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/src/main/res/values/attrs.xml
================================================
================================================
FILE: projects/sample/source/sample-plugin/third-party/slidingmenu/src/main/res/values/ids.xml
================================================
================================================
FILE: projects/sdk/coding/.gitignore
================================================
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
.externalNativeBuild
.gradletasknamecache
================================================
FILE: projects/sdk/coding/aar-to-jar-plugin/.gitignore
================================================
/build
================================================
FILE: projects/sdk/coding/aar-to-jar-plugin/build.gradle
================================================
apply plugin: 'java-gradle-plugin'
apply plugin: 'kotlin'
gradlePlugin {
plugins {
shadow {
id = "com.tencent.shadow.internal.aar-to-jar"
implementationClass = "com.tencent.shadow.coding.aar_to_jar_plugin.AarToJarPlugin"
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.tools.build:gradle:$build_gradle_version"
testImplementation "junit:junit:$junit_version"
testImplementation gradleTestKit()
}
================================================
FILE: projects/sdk/coding/aar-to-jar-plugin/src/main/kotlin/com/tencent/shadow/coding/aar_to_jar_plugin/AarToJarPlugin.kt
================================================
/*
* 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.coding.aar_to_jar_plugin
import com.android.build.gradle.BaseExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.Copy
import java.util.*
class AarToJarPlugin : Plugin {
override fun apply(project: Project) {
project.afterEvaluate {
val android = it.extensions.getByName("android") as BaseExtension
android.buildTypes.forEach { buildType ->
val createJarPackageTask = createJarPackageTask(project, buildType.name)
addJarConfiguration(project, buildType.name, createJarPackageTask)
}
}
}
private fun createJarPackageTask(project: Project, buildType: String): Task {
val taskName = "jar${buildType.capitalize(Locale.getDefault())}Package"
return project.tasks.create(taskName, Copy::class.java) {
fun buildDirFile(relativePath: String) =
project.file(project.buildDir.path + relativePath)
val aarFileName = "${project.name}-${buildType}"
val aarFile = buildDirFile("/outputs/aar/${aarFileName}.aar")
val outputDir = buildDirFile("/outputs/jar")
it.from(project.zipTree(aarFile))
it.into(outputDir)
it.include("classes.jar")
it.rename("classes.jar", "${aarFileName}.jar")
it.group = "build"
it.description = "生成jar包"
}.dependsOn(
project.getTasksByName(
"assemble${buildType.capitalize(Locale.getDefault())}",
false
).first()
)
}
/**
* 添加一个额外的Configuration,用于buildScript中以classpath方式依赖
*/
private fun addJarConfiguration(
project: Project,
buildType: String,
createJarPackageTask: Task
) {
val configurationName = "jar-${buildType}"
val jarFile =
project.file(project.buildDir.path + "/outputs/jar/${project.name}-${buildType}.jar")
project.configurations.create(configurationName)
project.artifacts.add(configurationName, jarFile) {
it.builtBy(createJarPackageTask)
}
}
}
================================================
FILE: projects/sdk/coding/android-jar/.gitignore
================================================
/build
================================================
FILE: projects/sdk/coding/android-jar/build.gradle
================================================
apply plugin: 'java-library'
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
evaluationDependsOn(':get-android-jar')
dependencies {
def androidJarPath = project(':get-android-jar').androidJarPath
api files(androidJarPath)
}
================================================
FILE: projects/sdk/coding/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
//buildscript不能从其他gradle文件中apply,所以这段buildscript脚本存在于多个子构建中。
//请更新buildscript时同步更新。
buildscript {
loadVersions:
{// 读取versions.properties到ext中,供项目中直接用变量引用版本号
def versions_properties_path = '../../../buildScripts/gradle/versions.properties'
def versions = new Properties()
versions.load(file(versions_properties_path).newReader())
versions.forEach { key, stringValue ->
def value = stringValue?.isInteger() ? stringValue as Integer : stringValue
ext.set(key, value)
}
}
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath "com.android.tools.build:gradle:$build_gradle_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply from: '../../../buildScripts/gradle/common.gradle'
allprojects {
group 'com.tencent.shadow.coding'
}
tasks.create('test').dependsOn subprojects.collect { it.getTasksByName('test', false) }
================================================
FILE: projects/sdk/coding/code-generator/.gitignore
================================================
/build
================================================
FILE: projects/sdk/coding/code-generator/build.gradle
================================================
apply plugin: 'kotlin'
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.squareup:javapoet:$javapoet_version"
implementation "org.javassist:javassist:$javassist_version"
implementation "junit:junit:$junit_version"
compileOnly project(':android-jar')
testImplementation project(':android-jar')
}
compileKotlin {
kotlinOptions {
jvmTarget = "1.6"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "1.6"
}
}
================================================
FILE: projects/sdk/coding/code-generator/src/main/kotlin/com/tencent/shadow/coding/code_generator/ActivityCodeGenerator.kt
================================================
@file:Suppress("DEPRECATION")
package com.tencent.shadow.coding.code_generator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.app.NativeActivity
import android.content.ComponentCallbacks2
import android.view.ContextThemeWrapper
import android.view.KeyEvent
import android.view.Window
import com.squareup.javapoet.AnnotationSpec
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.CodeBlock
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.TypeSpec
import com.squareup.javapoet.TypeVariableName
import com.tencent.shadow.core.runtime.NeighborClass
import javassist.ClassMap
import javassist.ClassPool
import javassist.LoaderClassPath
import javassist.bytecode.Descriptor
import java.io.File
import java.lang.reflect.Method
import java.lang.reflect.Type
import javax.lang.model.element.Modifier
/**
* Activity相关代码生成逻辑
*
* 这个逻辑应该可以进一步抽象成任意组件的相关代码生成逻辑,不局限于Activity
*
* 下面解释相关代码指的是什么。
*
* 假设业务有Activity名为BizActivity。那么原本BizActivity extends Activity。
* 在Shadow的方案中,也就是代理的方案中,需要宿主中有一个PluginContainerActivity,
* PluginContainerActivity extends Activity。然后将BizActivity改为
* BizActivity extends ShadowActivity。其中ShadowActivity extends PluginActivity。
* 然后让PluginContainerActivity implements HostActivityDelegator接口,
* 让Loader中的ShadowActivityDelegate implements HostActivityDelegate接口。
*
* 然后我们要将Activity上的方法分成3类:
* 1.私有方法,BizActivity原本也不会访问到这些方法,这些方法我们不用管。
*
* 2.系统会调用的方法,如onCreate等生命周期回调,这些方法需要由PluginContainerActivity
* 转调给HostActivityDelegate,HostActivityDelegate再转调给ShadowActivity,
* ShadowActivity再转调给HostActivityDelegator接口上定义的super前缀方法,
* super前缀方法实现为调用原本的super相应方法。这样一来,系统调用到PluginContainerActivity
* 上的方法,就可以被BizActivity响应,并且控制何时调用super方法。
*
* 3.插件会调用的方法,如setContentView,这些方法需要在ShadowActivity中
* 声明出一样的方法,并且实现为调用HostActivityDelegator接口上的同名方法。以便
* BizActivity在Override后通过super调用还能够调用到原本的Activity父类实现上。
* 与"系统会调用的方法"相比,省去了super前缀方法转调,因为PluginContainerActivity
* 不需要Override这些方法。
*
* 其中涉及被Shadow Transform修改了类型的方法,需要将相关代码的类型也修改掉。比如,
* getParent方法的返回类型需要改为ShadowActivity。
*
*/
class ActivityCodeGenerator {
companion object {
const val ACTIVITY_CONTAINER_PACKAGE = "com.tencent.shadow.core.runtime.container"
const val RUNTIME_PACKAGE = "com.tencent.shadow.core.runtime"
const val DELEGATE_PACKAGE = "com.tencent.shadow.core.loader.delegates"
const val PREFIX = "Generated"
//CS:const string
const val CS_HostActivityDelegate = "${PREFIX}HostActivityDelegate"
const val CS_HostActivityDelegator = "${PREFIX}HostActivityDelegator"
const val CS_PluginContainerActivity = "${PREFIX}PluginContainerActivity"
const val CS_NativePluginContainerActivity = "${PREFIX}NativePluginContainerActivity"
const val CS_PluginActivity = "${PREFIX}PluginActivity"
const val CS_ShadowActivityDelegate = "${PREFIX}ShadowActivityDelegate"
const val CS_delegate_field = "hostActivityDelegate"
const val CS_delegator_field = "hostActivityDelegator"
const val CS_pluginActivity_field = "pluginActivity"
val classPool = ClassPool.getDefault()
init {
// 兼容javassist升级后的ClassPool#appendSystemPath()改动:
// https://github.com/jboss-javassist/javassist/commit/e41e0790c0cb073e9e2e30071afecfcdc4621d42
if (javassist.bytecode.ClassFile.MAJOR_VERSION < javassist.bytecode.ClassFile.JAVA_9) {
val cl = Thread.currentThread().contextClassLoader
classPool.appendClassPath(LoaderClassPath(cl))
}
classPool.makeClass("$RUNTIME_PACKAGE.ShadowApplication")
.toClass(NeighborClass::class.java)
}
val ActivityClass = Activity::class.java
val NativeActivityClass = NativeActivity::class.java
val ModifiedActivityClass = modifySdkClass(Activity::class.java)
val activityCallbackMethods = getActivityCallbackMethods(ActivityClass)
val otherMethods = getOtherMethods(ActivityClass)
val activityCallbackMethodsModified = getActivityCallbackMethods(ModifiedActivityClass)
val otherMethodsModified = getOtherMethods(ModifiedActivityClass)
/**
* 统一在这里修改SDK中的Class对象,替换其中的类型为Shadow Runtime的类型
*/
fun modifySdkClass(clazz: Class<*>): Class<*> {
val name = clazz.name
val renameMap = ClassMap()
val ctClass = classPool.get(name)
val newClassNames = mutableListOf()
ctClass.name = name
mapOf(
Activity::class to "ShadowActivity",
Application::class to "ShadowApplication"
).forEach {
val newClassName = "$RUNTIME_PACKAGE.${it.value}"
renameMap[Descriptor.toJvmName(it.key.java.name)] =
Descriptor.toJvmName(newClassName)
newClassNames.add(newClassName)
}
ctClass.replaceClassName(renameMap)
return ctClass.toClass(NeighborClass::class.java)
}
fun getActivityMethods(clazz: Class<*>): List {
val allMethods = clazz.methods.toMutableSet()
allMethods.addAll(clazz.declaredMethods)
return allMethods
.filter {
it.declaringClass != Object::class.java
}
}
/**
* 有一部分方法系统会调用,插件也会调用。
* 这部分方法是否真的是这样,以后还是需要再仔细看一下。
* 先这样定义出来,叫做Custom也是因为暂时不知道叫什么好。
*/
fun getCustomMethods(clazz: Class<*>): Set {
val set = mutableSetOf()
fun addMethod(name: String, vararg args: Class<*>) {
val method =
try {
clazz.getDeclaredMethod(name, * args)
} catch (e: NoSuchMethodException) {
clazz.getMethod(name, * args)
}
set.add(method)
}
addMethod("isChangingConfigurations")
addMethod("finish")
addMethod("getClassLoader")
addMethod("getLayoutInflater")
addMethod("getResources")
addMethod("recreate")
addMethod("getCallingActivity")
return set
}
fun getActivityCallbackMethods(clazz: Class<*>): Set {
val callbacks = mutableSetOf()
callbacks.addAll(getCustomMethods(clazz))
val startWithOnMethods = getActivityMethods(clazz)
.filter {
java.lang.reflect.Modifier.isPublic(it.modifiers) or
java.lang.reflect.Modifier.isProtected(it.modifiers)
}.filter {
it.name.startsWith("on")
}
callbacks.addAll(startWithOnMethods)
val callbackInterface = getActivityMethods(clazz).filter {
java.lang.reflect.Modifier.isPublic(it.modifiers) or
java.lang.reflect.Modifier.isProtected(it.modifiers)
}.filter {
it.hasSameDefineIn(Window.Callback::class.java) or
it.hasSameDefineIn(KeyEvent.Callback::class.java)
}
callbacks.addAll(callbackInterface)
return callbacks;
}
fun Method.hasSameDefineIn(clazz: Class<*>): Boolean {
return try {
clazz.getDeclaredMethod(name, *parameterTypes)
true
} catch (e: NoSuchMethodException) {
false
}
}
fun Method.hasSameMethodIn(clazz: Class<*>): Boolean {
return try {
try {
clazz.getDeclaredMethod(name, *parameterTypes)
true
} catch (e: NoSuchMethodException) {
clazz.getMethod(name, *parameterTypes)
true
}
} catch (e: NoSuchMethodException) {
false
}
}
fun getOtherMethods(clazz: Class<*>): Set {
val activityMethods = getActivityMethods(clazz)
val callbackMethods = getActivityCallbackMethods(clazz)
val filter = activityMethods.filterNot {
callbackMethods.contains(it)
}.filterNot {
(it.declaringClass != clazz) and
java.lang.reflect.Modifier.isFinal(it.modifiers)
}
val result = mutableSetOf()
result.addAll(filter)
result.addAll(getCustomMethods(clazz))
return result
}
fun Method.toMethodSpecBuilder(prefix: String = ""): MethodSpec.Builder {
val methodName = if (prefix.isEmpty()) name else prefix + name.capitalize()
val builder = MethodSpec.methodBuilder(methodName)
parameters.forEach {
builder.addParameter(
ParameterSpec.builder(it.parameterizedType, it.name).build()
)
}
builder.addExceptions(
exceptionTypes.map {
TypeName.get(it)
}
)
builder.addTypeVariables(typeParameters.map {
TypeVariableName.get(it)
})
builder.returns(genericReturnType)
return builder
}
fun Method.toInterfaceMethodSpec(prefix: String = ""): MethodSpec {
val builder = toMethodSpecBuilder(prefix)
builder.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
return builder.build()
}
/**
* 这个类集中解决一个问题的逻辑。问题Issue见https://github.com/Tencent/Shadow/issues/230
*
* 问题描述:
* 一般情况下,一个类的方法签名中如果含有找不到的类,在这个类本身被new出来时是不会出错的。
* JVM不会去检查一个类的所有方法的签名中涉及到的类是否都存在。只有当调用该方法时才会发现签名上的类找不到。
*
* 但是在com.tencent.shadow.core.loader.ShadowPluginLoader.getHostActivityDelegate中,
* 将实现以接口类型返回时,涉及一种特殊情况,就是实现和接口不是由同一个ClassLoader加载的。
* 在这种情况下,JVM会检查实现是否真的实现了接口的每一个方法定义。所以会去尝试加载接口上所有方法签名涉及的类。
* 这会导致在低版本系统上尝试加载高版本引入的类,比如android.app.role.RoleManager是API29引入的,
* 在API28的手机上就会出现LinkageError。
*
* 解决方案就是对于高版本API引入的类在生成代理转调相关代码时:在Delegate、ShadowActivityDelegate
* 两个生成类中使用Object类型替代原本类型,在PluginContainerActivity中调用Delegate方法时强制类型转换,
* 在ShadowActivityDelegate调用PluginActivity方法时也对参数进行强制类型转换。
*
* 这个方法中定义哪些类型对于低版本API是安全的,不需要采用上述方案。其余类型则采用上述方案。
*/
private fun Class<*>.isSafeForLowApi(): Boolean {
val safeType: List = listOf(
android.app.Activity::class,
android.app.Dialog::class,
android.app.Fragment::class,
android.content.ComponentName::class,
android.content.Context::class,
android.content.Intent::class,
android.content.res.Configuration::class,
android.content.res.Resources.Theme::class,
android.content.res.Resources::class,
android.graphics.Bitmap::class,
android.graphics.Canvas::class,
android.net.Uri::class,
android.os.Bundle::class,
android.util.AttributeSet::class,
android.view.ActionMode.Callback::class,
android.view.ActionMode::class,
android.view.ContextMenu.ContextMenuInfo::class,
android.view.ContextMenu::class,
android.view.KeyEvent::class,
android.view.Menu::class,
android.view.MenuItem::class,
android.view.MotionEvent::class,
android.view.View::class,
android.view.WindowManager.LayoutParams::class,
android.view.accessibility.AccessibilityEvent::class,
android.view.LayoutInflater::class
).map { it.java.canonicalName }
return isPrimitive or
(isArray && componentType.isSafeForLowApi()) or
(canonicalName.startsWith("java.lang")) or
safeType.contains(canonicalName)
}
fun Method.toMethodSpecBuilderWithObjectType(): MethodSpec.Builder {
val methodName = name
val builder = MethodSpec.methodBuilder(methodName)
parameters.forEach {
builder.addParameter(
ParameterSpec.builder(
if (it.type.isSafeForLowApi()) it.parameterizedType else Object::class.java,
it.name
)
.build()
)
}
if (exceptionTypes.isNotEmpty()) {
builder.addExceptions(listOf(TypeName.get(Throwable::class.java)))
}
if (returnType.isSafeForLowApi()) {
builder.returns(returnType)
} else {
builder.returns(Object::class.java)
}
return builder
}
fun Method.toInterfaceMethodSpecWithObjectType(): MethodSpec {
val builder = toMethodSpecBuilderWithObjectType()
builder.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
return builder.build()
}
}
val commonJavadoc =
"由\n" + "{@link com.tencent.shadow.coding.code_generator.ActivityCodeGenerator}\n" + "自动生成\n"
val activityDelegate = defineActivityDelegate()
val activityDelegator = defineActivityDelegator()
val pluginContainerActivity = definePluginContainerActivity(
CS_PluginContainerActivity,
ActivityClass
)
val nativePluginContainerActivity = definePluginContainerActivity(
CS_NativePluginContainerActivity,
NativeActivityClass
)
val pluginActivity = definePluginActivity()
val shadowActivityDelegate = defineShadowActivityDelegate()
init {
addMethods()
}
fun defineActivityDelegate() =
TypeSpec.interfaceBuilder(CS_HostActivityDelegate)
.addModifiers(Modifier.PUBLIC)
.addJavadoc(
commonJavadoc
+ "HostActivity的被委托者接口\n"
+ "被委托者通过实现这个接口中声明的方法达到替代委托者实现的目的,从而将HostActivity的行为动态化。\n"
)
fun defineActivityDelegator() =
TypeSpec.interfaceBuilder(CS_HostActivityDelegator)
.addModifiers(Modifier.PUBLIC)
.addJavadoc(
commonJavadoc
+ "HostActivityDelegator作为委托者的接口。主要提供它的委托方法的super方法,\n"
+ "以便Delegate可以通过这个接口调用到Activity的super方法。\n"
)
fun definePluginContainerActivity(className: String, superclass: Type) =
TypeSpec.classBuilder(className)
.addModifiers(Modifier.ABSTRACT)
.superclass(superclass)
.addSuperinterface(ClassName.get(ACTIVITY_CONTAINER_PACKAGE, CS_HostActivityDelegator))
.addAnnotation(
AnnotationSpec.builder(SuppressLint::class.java)
.addMember("value", "{\"NewApi\", \"MissingPermission\"}")
.build()
)
.addField(
ClassName.get(ACTIVITY_CONTAINER_PACKAGE, CS_HostActivityDelegate),
CS_delegate_field
)
.addJavadoc(commonJavadoc)
fun definePluginActivity() =
TypeSpec.classBuilder(CS_PluginActivity)
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.superclass(ClassName.get(RUNTIME_PACKAGE, "ShadowContext"))
.addSuperinterfaces(
listOf(
ClassName.get(ComponentCallbacks2::class.java),
ClassName.get(Window.Callback::class.java),
ClassName.get(KeyEvent.Callback::class.java)
)
)
.addAnnotation(
AnnotationSpec.builder(SuppressLint::class.java)
.addMember("value", "{\"NullableProblems\", \"deprecation\"}")
.build()
)
.addField(
ClassName.get(ACTIVITY_CONTAINER_PACKAGE, CS_HostActivityDelegator),
CS_delegator_field
)
.addJavadoc(commonJavadoc)
fun defineShadowActivityDelegate() =
TypeSpec.classBuilder(CS_ShadowActivityDelegate)
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.superclass(ClassName.get(DELEGATE_PACKAGE, "ShadowDelegate"))
.addSuperinterface(
ClassName.get(ACTIVITY_CONTAINER_PACKAGE, CS_HostActivityDelegate)
)
.addField(
ClassName.get(RUNTIME_PACKAGE, CS_PluginActivity),
CS_pluginActivity_field
)
.addJavadoc(commonJavadoc)
.addAnnotation(
AnnotationSpec.builder(SuppressLint::class.java)
.addMember("value", "\"NewApi\"")
.build()
)
.addAnnotation(
AnnotationSpec.builder(SuppressWarnings::class.java)
.addMember("value", "{\"unchecked\", \"JavadocReference\"}")
.build()
)
fun generate(outputDir: File, moduleName: String) {
outputDir.mkdirs()
when (moduleName) {
"activity_container" -> {
listOf(
activityDelegate,
activityDelegator,
pluginContainerActivity,
nativePluginContainerActivity,
).forEach {
JavaFile.builder(ACTIVITY_CONTAINER_PACKAGE, it.build())
.build().writeTo(outputDir)
}
}
"runtime" -> {
JavaFile.builder(RUNTIME_PACKAGE, pluginActivity.build())
.build().writeTo(outputDir)
}
"loader" -> {
JavaFile.builder(DELEGATE_PACKAGE, shadowActivityDelegate.build())
.build().writeTo(outputDir)
}
else -> throw IllegalArgumentException("非法的moduleName:$moduleName")
}
}
fun addMethods() {
//将系统会调用的方法都定义出来,供转调之用。
activityDelegate.addMethods(activityCallbackMethods.map { it.toInterfaceMethodSpecWithObjectType() })
//将Activity可以被调用的方法都暴露出来
activityDelegator.addMethods(
otherMethods.map { it.toInterfaceMethodSpec() }
)
//TODO:这些方法应该不需要定义出来,但先对齐原手工实现的类,保证单元测试检测生成类和原手工写的类一致可以通过。
activityDelegator.addMethods(
activityCallbackMethods.filter {
it.hasSameDefineIn(Window.Callback::class.java)
}.map { it.toInterfaceMethodSpec() }
)
//添加系统会调用的方法的对应super方法,这些super方法实现时实现为调用super同名方法
activityDelegator.addMethods(
activityCallbackMethods.map { it.toInterfaceMethodSpec("super") }
)
//TODO:这些方法并不需要添加super前缀方法,但先对齐原手工实现的类,保证单元测试检测生成类和原手工写的类一致可以通过。
activityDelegator.addMethods(
otherMethods.filterNot { activityCallbackMethods.contains(it) }
.map { it.toInterfaceMethodSpec("super") }
)
//对系统会调用的方法转调到hostActivityDelegate去,再生成对应的super方法
listOf(pluginContainerActivity, nativePluginContainerActivity).forEach {
it.addMethods(
activityCallbackMethods.map(::delegateCallbackMethod)
)
it.addMethods(
activityCallbackMethods.map(::implementSuperMethod)
)
//TODO:这些方法并不需要添加super前缀方法,但先对齐原手工实现的类,保证单元测试检测生成类和原手工写的类一致可以通过。
it.addMethods(
otherMethods.filterNot { activityCallbackMethods.contains(it) }
.map(::implementSuperMethod)
)
//将所有protected方法暴露成public方法
it.addMethods(
otherMethods.filter {
java.lang.reflect.Modifier.isProtected(it.modifiers)
}.map(::exposeProtectedMethod)
)
}
pluginActivity.addMethods(
activityCallbackMethodsModified
.filterNot { it.name == "getResources" }
.filterNot { it.name == "getClassLoader" }
.map {
defineMethodXXX(it, true)
}
)
pluginActivity.addMethods(
otherMethodsModified
.filterNot {
it.hasSameMethodIn(ContextThemeWrapper::class.java)
}
.filterNot { getCustomMethods(ModifiedActivityClass).contains(it) }
.map {
defineMethodXXX(it, false)
}
.toList()
)
//实现所有Delegate方法
shadowActivityDelegate.addMethods(
activityCallbackMethodsModified
.filter { it.isNotModified() }
.map {
implementDelegateMethod(it)
}
)
}
//定义转调一个系统会调用的方法的实现
fun delegateCallbackMethod(method: Method): MethodSpec {
val methodBuilder = method.toMethodSpecBuilder()
if (java.lang.reflect.Modifier.isPublic(method.modifiers)) {
methodBuilder.addModifiers(Modifier.PUBLIC)
} else {
methodBuilder.addModifiers(Modifier.PROTECTED)
}
methodBuilder.addAnnotation(Override::class.java)
val ret = if (method.returnType == Void::class.javaPrimitiveType) "" else "return "
methodBuilder.beginControlFlow("if (${CS_delegate_field} != null)")
val args = method.parameters.joinToString(separator = ", ") {
it.name
}
val retCast = if (method.returnType.isSafeForLowApi()) "" else "(${method.returnType.name})"
methodBuilder.addStatement("${ret}$retCast${CS_delegate_field}.${method.name}($args)")
methodBuilder.nextControlFlow("else")
methodBuilder.addStatement("${ret}super.${method.name}($args)")
methodBuilder.endControlFlow()
return methodBuilder.build()
}
//实现super前缀方法
fun implementSuperMethod(method: Method): MethodSpec {
val methodBuilder = method.toMethodSpecBuilder("super")
methodBuilder.addModifiers(Modifier.PUBLIC)
methodBuilder.addAnnotation(Override::class.java)
val ret = if (method.returnType == Void::class.javaPrimitiveType) "" else "return "
val args = method.parameters.joinToString(separator = ", ") {
it.name
}
methodBuilder.addStatement("${ret}super.${method.name}($args)")
return methodBuilder.build()
}
//定义暴露protected的方法
fun exposeProtectedMethod(method: Method): MethodSpec {
val methodBuilder = method.toMethodSpecBuilder()
methodBuilder.addModifiers(Modifier.PUBLIC)
val ret = if (method.returnType == Void::class.javaPrimitiveType) "" else "return "
val args = method.parameters.joinToString(separator = ", ") {
it.name
}
methodBuilder.addStatement("${ret}super.${method.name}($args)")
return methodBuilder.build()
}
//定义方法的实现
fun defineMethod(method: Method, hasSuperMethod: Boolean): MethodSpec {
val methodBuilder = method.toMethodSpecBuilder()
methodBuilder.addModifiers(Modifier.PUBLIC)
val ret = if (method.returnType == Void::class.javaPrimitiveType) "" else "return "
val args = method.parameters.joinToString(separator = ", ") {
it.name
}
val invokeMethod =
if (hasSuperMethod)
"super${method.name.capitalize()}"
else
"${method.name}"
methodBuilder.addStatement("${ret}${CS_delegator_field}.${invokeMethod}($args)")
return methodBuilder.build()
}
fun defineAbstractMethod(method: Method): MethodSpec {
val methodBuilder = method.toMethodSpecBuilder()
methodBuilder.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
return methodBuilder.build()
}
fun Method.isNotModified(): Boolean {
return try {
val m = ActivityClass.getDeclaredMethod(name, *parameterTypes)
m.returnType == returnType
} catch (e: NoSuchMethodException) {
try {
val m = ActivityClass.getMethod(name, *parameterTypes)
m.returnType == returnType
} catch (e: NoSuchMethodException) {
false
}
}
}
fun defineMethodXXX(method: Method, hasSuperMethod: Boolean) =
if (method.isNotModified()) {
defineMethod(method, hasSuperMethod)
} else {
defineAbstractMethod(method)
}
fun implementDelegateMethod(method: Method): MethodSpec {
val methodBuilder = method.toMethodSpecBuilderWithObjectType()
methodBuilder.addModifiers(Modifier.PUBLIC)
methodBuilder.addAnnotation(Override::class.java)
val ret = if (method.returnType == Void::class.javaPrimitiveType) "" else "return "
val args = method.parameters.joinToString(separator = ", ") {
(if (it.type.isSafeForLowApi()) CodeBlock.of(
"\$L",
it.name
) else CodeBlock.of("(\$T) \$L", it.parameterizedType, it.name)).toString()
}
methodBuilder.addStatement("${ret}${CS_pluginActivity_field}.${method.name}($args)")
return methodBuilder.build()
}
}
================================================
FILE: projects/sdk/coding/code-generator/src/main/kotlin/com/tencent/shadow/core/runtime/NeighborClass.kt
================================================
package com.tencent.shadow.core.runtime
/**
* 为了兼容JDK 17和javassist
* https://github.com/jboss-javassist/javassist/issues/369
*/
class NeighborClass
================================================
FILE: projects/sdk/coding/code-generator/src/test/kotlin/com/tencent/shadow/coding/code_generator/ActivityCodeGeneratorTest.kt
================================================
package com.tencent.shadow.coding.code_generator
import org.junit.Test
internal class ActivityCodeGeneratorTest {
@Test
fun testLoadAndroidClass() {
ActivityCodeGenerator.classPool.get("android.app.Activity")
}
}
================================================
FILE: projects/sdk/coding/common-jar-settings/.gitignore
================================================
/build
================================================
FILE: projects/sdk/coding/common-jar-settings/build.gradle
================================================
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.TypeSpec
import javax.lang.model.element.Modifier
buildscript {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath "com.squareup:javapoet:$javapoet_version"
}
}
apply plugin: 'java-gradle-plugin'
apply plugin: 'kotlin'
gradlePlugin {
plugins {
shadow {
id = "com.tencent.shadow.internal.common-jar-settings"
implementationClass = "com.tencent.shadow.coding.common_jar_settings.CommonJarSettingsPlugin"
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.tools.build:gradle:$build_gradle_version"
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
testImplementation "junit:junit:$junit_version"
testImplementation gradleTestKit()
}
java {
sourceSets {
main.java.srcDirs += 'build/generateCode'
}
}
def generateCode = tasks.register('generateCode') {
def androidJarPath = project(':get-android-jar').androidJarPath
inputs.property('androidJarPath', androidJarPath)
outputs.dir(layout.buildDirectory.dir('generateCode'))
.withPropertyName('outputDir')
doLast {
def androidJarPathField
= FieldSpec.builder(String, "ANDROID_JAR_PATH", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer('$S', androidJarPath).build()
def androidJarClass = TypeSpec.classBuilder("AndroidJar")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addField(androidJarPathField).build()
JavaFile.builder("com.tencent.shadow.coding.common_jar_settings", androidJarClass)
.build().writeTo(new File(project.getBuildDir(), 'generateCode'))
}
}
compileKotlin.dependsOn(generateCode)
================================================
FILE: projects/sdk/coding/common-jar-settings/src/main/kotlin/com/tencent/shadow/coding/common_jar_settings/CommonJarSettingsPlugin.kt
================================================
package com.tencent.shadow.coding.common_jar_settings;
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.compile.JavaCompile
@Suppress("unused")
class CommonJarSettingsPlugin : Plugin {
override fun apply(project: Project) {
project.pluginManager.apply("java-library")
val java = project.extensions.getByType(JavaPluginExtension::class.java)
java.sourceCompatibility = JavaVersion.VERSION_1_7
java.targetCompatibility = JavaVersion.VERSION_1_7
val androidJar = project.files(AndroidJar.ANDROID_JAR_PATH)
// 将android.jar设置为这些jar工程的bootclasspath,以便javac编译时使用的JDK标准库采用android平台的定义
project.tasks.withType(JavaCompile::class.java) {
it.options.bootstrapClasspath = androidJar
}
// IDE不会自动索引bootstrapClasspath,所以把bootstrapClasspath重复添加到compileOnly中
project.dependencies.add("compileOnly", androidJar)
}
}
================================================
FILE: projects/sdk/coding/get-android-jar/.gitignore
================================================
/build
================================================
FILE: projects/sdk/coding/get-android-jar/build.gradle
================================================
apply plugin: 'com.android.library'
android {
compileSdkVersion project.COMPILE_SDK_VERSION
}
def sdkPath = android.sdkDirectory
def androidJarPath = new File(sdkPath, "platforms/${android.compileSdkVersion}/android.jar")
if (!androidJarPath.exists()) {
println("File $androidJarPath not exists!")
throw new RuntimeException("Android SDK ${android.compileSdkVersion} 没有安装")
}
ext.set('androidJarPath', androidJarPath)
================================================
FILE: projects/sdk/coding/get-android-jar/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sdk/coding/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionUrl=https\://mirrors.tencent.com/gradle/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: projects/sdk/coding/gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: projects/sdk/coding/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: projects/sdk/coding/java-build-config/.gitignore
================================================
/build
================================================
FILE: projects/sdk/coding/java-build-config/build.gradle
================================================
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.TypeSpec
import javax.lang.model.element.Modifier
apply plugin: 'java'
buildscript {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath "com.squareup:javapoet:$javapoet_version"
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
sourceSets {
main.java.srcDirs += 'build/generateCode'
}
}
def generateCode = tasks.register('generateCode') {
inputs.property('VERSION_NAME', project.VERSION_NAME)
outputs.dir(layout.buildDirectory.dir('generateCode'))
.withPropertyName('outputDir')
doLast {
def versionNameField
= FieldSpec.builder(String, "VERSION_NAME", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer('$S', project.VERSION_NAME).build()
def buildConfigClass = TypeSpec.classBuilder("BuildConfig")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addField(versionNameField).build()
JavaFile.builder("com.tencent.shadow.coding.java_build_config", buildConfigClass)
.build().writeTo(new File(project.getBuildDir(), 'generateCode'))
}
}
compileJava.dependsOn(generateCode)
================================================
FILE: projects/sdk/coding/settings.gradle
================================================
include 'code-generator'
include 'aar-to-jar-plugin'
include 'common-jar-settings'
include 'get-android-jar'
include 'android-jar'
include 'java-build-config'
================================================
FILE: projects/sdk/core/.gitignore
================================================
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
.externalNativeBuild
.gradletasknamecache
================================================
FILE: projects/sdk/core/activity-container/.gitignore
================================================
/build
================================================
FILE: projects/sdk/core/activity-container/build.gradle
================================================
import com.tencent.shadow.coding.code_generator.ActivityCodeGenerator
buildscript {
dependencies {
classpath 'com.tencent.shadow.coding:android-jar'
classpath 'com.tencent.shadow.coding:code-generator'
}
}
apply plugin: 'com.tencent.shadow.internal.common-jar-settings'
java {
sourceSets {
main.java.srcDirs += 'build/generated/sources/code-generator'
}
}
def generateCode = tasks.register('generateCode') {
def outputDir = layout.buildDirectory.dir('generated/sources/code-generator')
outputs.dir(outputDir)
.withPropertyName('outputDir')
doLast {
ActivityCodeGenerator codeGenerator = new ActivityCodeGenerator()
codeGenerator.generate(outputDir.get().getAsFile(), "activity_container")
}
}
compileJava.dependsOn(generateCode)
dependencies {
implementation 'com.tencent.shadow.coding:java-build-config'
}
================================================
FILE: projects/sdk/core/activity-container/src/main/java/com/tencent/shadow/core/runtime/container/DelegateProvider.java
================================================
/*
* 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.runtime.container;
/**
* 宿主容器委托提供者
*
* 负责提供宿主容器委托实现
*
* @author cubershi
*/
public interface DelegateProvider {
String LOADER_VERSION_KEY = "LOADER_VERSION";
String PROCESS_ID_KEY = "PROCESS_ID_KEY";
/**
* 获取与delegator相应的HostActivityDelegate
*
* @param delegator HostActivity委托者
* @return HostActivity被委托者
*/
HostActivityDelegate getHostActivityDelegate(Class extends HostActivityDelegator> delegator);
}
================================================
FILE: projects/sdk/core/activity-container/src/main/java/com/tencent/shadow/core/runtime/container/DelegateProviderHolder.java
================================================
/*
* 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.runtime.container;
import android.os.SystemClock;
import java.util.HashMap;
import java.util.Map;
/**
* DelegateProvider依赖注入类
*
* dynamic-pluginloader通过这个类实现将PluginLoader中的DelegateProvider实现注入到plugincontainer中。
*
* @author cubershi
*/
public class DelegateProviderHolder {
public static final String DEFAULT_KEY = "DEFAULT_KEY";
private static Map delegateProviderMap = new HashMap<>();
/**
* 为了防止系统有一定概率出现进程号重启后一致的问题,我们使用开机时间作为进程号来判断进程是否重启
*/
public static long sCustomPid;
static {
sCustomPid = SystemClock.elapsedRealtime();
}
public static void setDelegateProvider(String key, DelegateProvider delegateProvider) {
delegateProviderMap.put(key, delegateProvider);
}
public static DelegateProvider getDelegateProvider(String key) {
return delegateProviderMap.get(key);
}
}
================================================
FILE: projects/sdk/core/activity-container/src/main/java/com/tencent/shadow/core/runtime/container/HostActivity.java
================================================
/*
* 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.runtime.container;
import android.app.Activity;
import android.view.Window;
/**
* 表示一个Activity是宿主程序中的Activity
*
* @author cubershi
*/
public interface HostActivity {
/**
* 返回Activity对象本身
*
* @return Activity对象本身
*/
Activity getImplementActivity();
/**
* 返回Activity的Window
*
* @return Activity的Window
*/
Window getImplementWindow();
}
================================================
FILE: projects/sdk/core/activity-container/src/main/java/com/tencent/shadow/core/runtime/container/HostActivityDelegate.java
================================================
/*
* 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.runtime.container;
/**
* HostActivity的被委托者接口
*
* 被委托者通过实现这个接口中声明的方法达到替代委托者实现的目的,从而将HostActivity的行为动态化。
*
* @author cubershi
*/
public interface HostActivityDelegate extends GeneratedHostActivityDelegate {
void setDelegator(HostActivityDelegator delegator);
Object getPluginActivity();
String getLoaderVersion();
}
================================================
FILE: projects/sdk/core/activity-container/src/main/java/com/tencent/shadow/core/runtime/container/HostActivityDelegator.java
================================================
/*
* 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.runtime.container;
/**
* HostActivity作为委托者的接口。主要提供它的委托方法的super方法,
* 以便Delegate可以通过这个接口调用到Activity的super方法。
*
* cubershi
*/
public interface HostActivityDelegator extends GeneratedHostActivityDelegator {
HostActivity getHostActivity();
}
================================================
FILE: projects/sdk/core/activity-container/src/main/java/com/tencent/shadow/core/runtime/container/HostNativeActivityDelegate.java
================================================
/*
* 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.runtime.container;
import android.content.pm.PackageManager;
import android.view.InputQueue;
import android.view.SurfaceHolder;
public interface HostNativeActivityDelegate extends HostActivityDelegate {
PackageManager getPackageManager();
void surfaceCreated(SurfaceHolder holder);
void surfaceChanged(SurfaceHolder holder, int format, int width, int height);
void surfaceRedrawNeeded(SurfaceHolder holder);
void surfaceDestroyed(SurfaceHolder holder);
void onInputQueueCreated(InputQueue queue);
void onInputQueueDestroyed(InputQueue queue);
void onGlobalLayout();
}
================================================
FILE: projects/sdk/core/activity-container/src/main/java/com/tencent/shadow/core/runtime/container/HostNativeActivityDelegator.java
================================================
/*
* 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.runtime.container;
import android.view.InputQueue;
import android.view.SurfaceHolder;
public interface HostNativeActivityDelegator extends HostActivityDelegator {
void superSurfaceCreated(SurfaceHolder holder);
void superSurfaceChanged(SurfaceHolder holder, int format, int width, int height);
void superSurfaceRedrawNeeded(SurfaceHolder holder);
void superSurfaceDestroyed(SurfaceHolder holder);
void superOnInputQueueCreated(InputQueue queue);
void superOnInputQueueDestroyed(InputQueue queue);
void superOnGlobalLayout();
}
================================================
FILE: projects/sdk/core/activity-container/src/main/java/com/tencent/shadow/core/runtime/container/NativePluginContainerActivity.java
================================================
/*
* 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.runtime.container;
import static com.tencent.shadow.core.runtime.container.DelegateProvider.LOADER_VERSION_KEY;
import static com.tencent.shadow.core.runtime.container.DelegateProvider.PROCESS_ID_KEY;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.view.InputQueue;
import android.view.SurfaceHolder;
import android.view.Window;
import com.tencent.shadow.coding.java_build_config.BuildConfig;
/**
* NativeActivity位于宿主中的壳子
*
* TODO 和PluginContainerActivity重复代码过多,但由于是继承自不同父类,需要用JavaPoet生成代码来减少重复。
*/
public class NativePluginContainerActivity extends GeneratedNativePluginContainerActivity implements HostActivity, HostNativeActivityDelegator {
private static final String TAG = "NativePluginConAct";
HostNativeActivityDelegate hostActivityDelegate;
private boolean isBeforeOnCreate = true;
public NativePluginContainerActivity() {
HostNativeActivityDelegate delegate;
DelegateProvider delegateProvider = DelegateProviderHolder.getDelegateProvider(getDelegateProviderKey());
if (delegateProvider != null) {
delegate = (HostNativeActivityDelegate) delegateProvider.getHostActivityDelegate(this.getClass());
delegate.setDelegator(this);
} else {
Log.e(TAG, "NativePluginContainerActivity: DelegateProviderHolder没有初始化");
delegate = null;
}
super.hostActivityDelegate = delegate;
hostActivityDelegate = delegate;
}
protected String getDelegateProviderKey() {
return DelegateProviderHolder.DEFAULT_KEY;
}
final public Object getPluginActivity() {
if (hostActivityDelegate != null) {
return hostActivityDelegate.getPluginActivity();
} else {
return null;
}
}
@Override
final protected void onCreate(Bundle savedInstanceState) {
isBeforeOnCreate = false;
mHostTheme = null;//释放资源
boolean illegalIntent = isIllegalIntent(savedInstanceState);
if (illegalIntent) {
super.hostActivityDelegate = null;
hostActivityDelegate = null;
Log.e(TAG, "illegalIntent savedInstanceState==" + savedInstanceState + " getIntent().getExtras()==" + getIntent().getExtras());
}
if (hostActivityDelegate != null) {
hostActivityDelegate.onCreate(savedInstanceState);
} else {
//这里是进程被杀后重启后走到,当需要恢复fragment状态的时候,由于系统保留了TAG,会因为找不到fragment引起crash
super.onCreate(null);
Log.e(TAG, "onCreate: hostActivityDelegate==null finish activity");
finish();
System.exit(0);
}
}
/**
* IllegalIntent指的是这些情况下的启动:
* 1.插件版本变化之后,残留于系统中的PendingIntent或系统因回收内存杀死进程残留的任务栈而启动。
* 由于插件版本变化,PluginLoader逻辑可能不一致,Intent中的参数可能不能满足新代码的启动条件。
* 2.外部的非法启动,无法确定一个插件的Activity。
*
*
* 3.不支持进程重启后莫名其妙的原因loader也加载了,但是可能要启动的plugin没有load,出现异常
*
* @param savedInstanceState onCreate时系统还回来的savedInstanceState
* @return true表示这次启动不是我们预料的,需要尽早finish并退出进程。
*/
private boolean isIllegalIntent(Bundle savedInstanceState) {
Bundle extras = getIntent().getExtras();
if (extras == null && savedInstanceState == null) {
return true;
}
Bundle bundle;
bundle = savedInstanceState == null ? extras : savedInstanceState;
try {
String loaderVersion = bundle.getString(LOADER_VERSION_KEY);
long processVersion = bundle.getLong(PROCESS_ID_KEY);
return !BuildConfig.VERSION_NAME.equals(loaderVersion) || processVersion != DelegateProviderHolder.sCustomPid;
} catch (Throwable ignored) {
//捕获可能的非法Intent中包含我们根本反序列化不了的数据
return true;
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if (hostActivityDelegate != null) {
hostActivityDelegate.onSaveInstanceState(outState);
} else {
super.onSaveInstanceState(outState);
}
//避免插件setIntent清空掉LOADER_VERSION_KEY
outState.putString(LOADER_VERSION_KEY, BuildConfig.VERSION_NAME);
outState.putLong(PROCESS_ID_KEY, DelegateProviderHolder.sCustomPid);
}
@Override
public HostActivity getHostActivity() {
return this;
}
@Override
public Activity getImplementActivity() {
return this;
}
@Override
public Window getImplementWindow() {
return getWindow();
}
/**
* Theme一旦设置了就不能更换Theme所在的Resouces了,见{@link Resources.Theme#setTo(Resources.Theme)}
* 而Activity在OnCreate之前需要设置Theme和使用Theme。我们需要在Activity OnCreate之后才能注入插件资源。
* 这就需要在Activity OnCreate之前不要调用Activity的setTheme方法,同时在getTheme时返回宿主的Theme资源。
* 注:{@link Activity#setTheme(int)}会触发初始化Theme,因此不能调用。
*/
private Resources.Theme mHostTheme;
@Override
public Resources.Theme getTheme() {
if (isBeforeOnCreate) {
if (mHostTheme == null) {
mHostTheme = super.getResources().newTheme();
}
return mHostTheme;
} else {
return super.getTheme();
}
}
@Override
public void setTheme(int resid) {
if (!isBeforeOnCreate) {
super.setTheme(resid);
}
}
@Override
public PackageManager getPackageManager() {
if (hostActivityDelegate != null) {
return hostActivityDelegate.getPackageManager();
} else {
return super.getPackageManager();
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (hostActivityDelegate != null) {
hostActivityDelegate.surfaceCreated(holder);
} else {
super.surfaceCreated(holder);
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (hostActivityDelegate != null) {
hostActivityDelegate.surfaceChanged(holder, format, width, height);
} else {
super.surfaceChanged(holder, format, width, height);
}
}
@Override
public void surfaceRedrawNeeded(SurfaceHolder holder) {
if (hostActivityDelegate != null) {
hostActivityDelegate.surfaceRedrawNeeded(holder);
} else {
super.surfaceRedrawNeeded(holder);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (hostActivityDelegate != null) {
hostActivityDelegate.surfaceDestroyed(holder);
} else {
super.surfaceDestroyed(holder);
}
}
@Override
public void onInputQueueCreated(InputQueue queue) {
if (hostActivityDelegate != null) {
hostActivityDelegate.onInputQueueCreated(queue);
} else {
super.onInputQueueCreated(queue);
}
}
@Override
public void onInputQueueDestroyed(InputQueue queue) {
if (hostActivityDelegate != null) {
hostActivityDelegate.onInputQueueDestroyed(queue);
} else {
super.onInputQueueDestroyed(queue);
}
}
@Override
public void onGlobalLayout() {
if (hostActivityDelegate != null) {
hostActivityDelegate.onGlobalLayout();
} else {
super.onGlobalLayout();
}
}
@Override
public void superSurfaceCreated(SurfaceHolder holder) {
super.surfaceCreated(holder);
}
@Override
public void superSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
super.surfaceChanged(holder, format, width, height);
}
@Override
public void superSurfaceRedrawNeeded(SurfaceHolder holder) {
super.surfaceRedrawNeeded(holder);
}
@Override
public void superSurfaceDestroyed(SurfaceHolder holder) {
super.surfaceDestroyed(holder);
}
@Override
public void superOnInputQueueCreated(InputQueue queue) {
super.onInputQueueCreated(queue);
}
@Override
public void superOnInputQueueDestroyed(InputQueue queue) {
super.onInputQueueDestroyed(queue);
}
@Override
public void superOnGlobalLayout() {
super.onGlobalLayout();
}
}
================================================
FILE: projects/sdk/core/activity-container/src/main/java/com/tencent/shadow/core/runtime/container/PluginContainerActivity.java
================================================
/*
* 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.runtime.container;
import static com.tencent.shadow.core.runtime.container.DelegateProvider.LOADER_VERSION_KEY;
import static com.tencent.shadow.core.runtime.container.DelegateProvider.PROCESS_ID_KEY;
import android.app.Activity;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.view.Window;
import com.tencent.shadow.coding.java_build_config.BuildConfig;
/**
* 插件的容器Activity。PluginLoader将把插件的Activity放在其中。
* PluginContainerActivity以委托模式将Activity的所有回调方法委托给DelegateProviderHolder提供的Delegate。
*
* @author cubershi
*/
public class PluginContainerActivity extends GeneratedPluginContainerActivity implements HostActivity, HostActivityDelegator {
private static final String TAG = "PluginContainerActivity";
HostActivityDelegate hostActivityDelegate;
private boolean isBeforeOnCreate = true;
public PluginContainerActivity() {
HostActivityDelegate delegate;
DelegateProvider delegateProvider = DelegateProviderHolder.getDelegateProvider(getDelegateProviderKey());
if (delegateProvider != null) {
delegate = delegateProvider.getHostActivityDelegate(this.getClass());
delegate.setDelegator(this);
} else {
Log.e(TAG, "PluginContainerActivity: DelegateProviderHolder没有初始化");
delegate = null;
}
super.hostActivityDelegate = delegate;
hostActivityDelegate = delegate;
}
protected String getDelegateProviderKey() {
return DelegateProviderHolder.DEFAULT_KEY;
}
final public Object getPluginActivity() {
if (hostActivityDelegate != null) {
return hostActivityDelegate.getPluginActivity();
} else {
return null;
}
}
@Override
final protected void onCreate(Bundle savedInstanceState) {
isBeforeOnCreate = false;
mHostTheme = null;//释放资源
boolean illegalIntent = isIllegalIntent(savedInstanceState);
if (illegalIntent) {
super.hostActivityDelegate = null;
hostActivityDelegate = null;
Log.e(TAG, "illegalIntent savedInstanceState==" + savedInstanceState + " getIntent().getExtras()==" + getIntent().getExtras());
}
if (hostActivityDelegate != null) {
hostActivityDelegate.onCreate(savedInstanceState);
} else {
//这里是进程被杀后重启后走到,当需要恢复fragment状态的时候,由于系统保留了TAG,会因为找不到fragment引起crash
super.onCreate(null);
Log.e(TAG, "onCreate: hostActivityDelegate==null finish activity");
finish();
System.exit(0);
}
}
/**
* IllegalIntent指的是这些情况下的启动:
* 1.插件版本变化之后,残留于系统中的PendingIntent或系统因回收内存杀死进程残留的任务栈而启动。
* 由于插件版本变化,PluginLoader逻辑可能不一致,Intent中的参数可能不能满足新代码的启动条件。
* 2.外部的非法启动,无法确定一个插件的Activity。
*
*
* 3.不支持进程重启后莫名其妙的原因loader也加载了,但是可能要启动的plugin没有load,出现异常
*
* @param savedInstanceState onCreate时系统还回来的savedInstanceState
* @return true表示这次启动不是我们预料的,需要尽早finish并退出进程。
*/
private boolean isIllegalIntent(Bundle savedInstanceState) {
Bundle extras = getIntent().getExtras();
if (extras == null && savedInstanceState == null) {
return true;
}
Bundle bundle;
bundle = savedInstanceState == null ? extras : savedInstanceState;
try {
String loaderVersion = bundle.getString(LOADER_VERSION_KEY);
long processVersion = bundle.getLong(PROCESS_ID_KEY);
return !BuildConfig.VERSION_NAME.equals(loaderVersion) || processVersion != DelegateProviderHolder.sCustomPid;
} catch (Throwable ignored) {
//捕获可能的非法Intent中包含我们根本反序列化不了的数据
return true;
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if (hostActivityDelegate != null) {
hostActivityDelegate.onSaveInstanceState(outState);
} else {
super.onSaveInstanceState(outState);
}
//避免插件setIntent清空掉LOADER_VERSION_KEY
outState.putString(LOADER_VERSION_KEY, BuildConfig.VERSION_NAME);
outState.putLong(PROCESS_ID_KEY, DelegateProviderHolder.sCustomPid);
}
@Override
public HostActivity getHostActivity() {
return this;
}
@Override
public Activity getImplementActivity() {
return this;
}
@Override
public Window getImplementWindow() {
return getWindow();
}
/**
* Theme一旦设置了就不能更换Theme所在的Resouces了,见{@link Resources.Theme#setTo(Resources.Theme)}
* 而Activity在OnCreate之前需要设置Theme和使用Theme。我们需要在Activity OnCreate之后才能注入插件资源。
* 这就需要在Activity OnCreate之前不要调用Activity的setTheme方法,同时在getTheme时返回宿主的Theme资源。
* 注:{@link Activity#setTheme(int)}会触发初始化Theme,因此不能调用。
*/
private Resources.Theme mHostTheme;
@Override
public Resources.Theme getTheme() {
if (isBeforeOnCreate) {
if (mHostTheme == null) {
mHostTheme = super.getResources().newTheme();
}
return mHostTheme;
} else {
return super.getTheme();
}
}
@Override
public void setTheme(int resid) {
if (!isBeforeOnCreate) {
super.setTheme(resid);
}
}
}
================================================
FILE: projects/sdk/core/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
//buildscript不能从其他gradle文件中apply,所以这段buildscript脚本存在于多个子构建中。
//请更新buildscript时同步更新。
buildscript {
loadVersions:
{// 读取versions.properties到ext中,供项目中直接用变量引用版本号
def versions_properties_path = '../../../buildScripts/gradle/versions.properties'
def versions = new Properties()
versions.load(file(versions_properties_path).newReader())
versions.forEach { key, stringValue ->
def value = stringValue?.isInteger() ? stringValue as Integer : stringValue
ext.set(key, value)
}
}
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath "com.android.tools.build:gradle:$build_gradle_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.tencent.shadow.coding:common-jar-settings'
}
}
apply from: '../../../buildScripts/gradle/common.gradle'
allprojects {
group 'com.tencent.shadow.core'
buildscript {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
}
}
tasks.create('test').dependsOn subprojects.collect { it.getTasksByName('test', false) }
================================================
FILE: projects/sdk/core/common/.gitignore
================================================
/build
================================================
FILE: projects/sdk/core/common/build.gradle
================================================
apply plugin: 'com.tencent.shadow.internal.common-jar-settings'
================================================
FILE: projects/sdk/core/common/src/main/java/com/tencent/shadow/core/common/ILoggerFactory.java
================================================
/*
* 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.common;
public interface ILoggerFactory {
Logger getLogger(String name);
}
================================================
FILE: projects/sdk/core/common/src/main/java/com/tencent/shadow/core/common/InstalledApk.java
================================================
/*
* 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.common;
import android.os.Parcel;
import android.os.Parcelable;
/**
* 安装完成的apk
*/
public class InstalledApk implements Parcelable {
public final String apkFilePath;
public final String oDexPath;
public final String libraryPath;
public final byte[] parcelExtras;
public InstalledApk(String apkFilePath, String oDexPath, String libraryPath) {
this(apkFilePath, oDexPath, libraryPath, null);
}
public InstalledApk(String apkFilePath, String oDexPath, String libraryPath, byte[] parcelExtras) {
this.apkFilePath = apkFilePath;
this.oDexPath = oDexPath;
this.libraryPath = libraryPath;
this.parcelExtras = parcelExtras;
}
protected InstalledApk(Parcel in) {
apkFilePath = in.readString();
oDexPath = in.readString();
libraryPath = in.readString();
int parcelExtrasLength = in.readInt();
if (parcelExtrasLength > 0) {
parcelExtras = new byte[parcelExtrasLength];
} else {
parcelExtras = null;
}
if (parcelExtras != null) {
in.readByteArray(parcelExtras);
}
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(apkFilePath);
dest.writeString(oDexPath);
dest.writeString(libraryPath);
dest.writeInt(parcelExtras == null ? 0 : parcelExtras.length);
if (parcelExtras != null) {
dest.writeByteArray(parcelExtras);
}
}
@Override
public int describeContents() {
return 0;
}
public static final Creator CREATOR = new Creator() {
@Override
public InstalledApk createFromParcel(Parcel in) {
return new InstalledApk(in);
}
@Override
public InstalledApk[] newArray(int size) {
return new InstalledApk[size];
}
};
}
================================================
FILE: projects/sdk/core/common/src/main/java/com/tencent/shadow/core/common/Logger.java
================================================
/*
* 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.common;
public interface Logger {
String getName();
boolean isTraceEnabled();
void trace(String msg);
void trace(String format, Object arg);
void trace(String format, Object arg1, Object arg2);
void trace(String format, Object... arguments);
void trace(String msg, Throwable t);
boolean isDebugEnabled();
void debug(String msg);
void debug(String format, Object arg);
void debug(String format, Object arg1, Object arg2);
void debug(String format, Object... arguments);
void debug(String msg, Throwable t);
boolean isInfoEnabled();
void info(String msg);
void info(String format, Object arg);
void info(String format, Object arg1, Object arg2);
void info(String format, Object... arguments);
void info(String msg, Throwable t);
boolean isWarnEnabled();
void warn(String msg);
void warn(String format, Object arg);
void warn(String format, Object... arguments);
void warn(String format, Object arg1, Object arg2);
void warn(String msg, Throwable t);
boolean isErrorEnabled();
void error(String msg);
void error(String format, Object arg);
void error(String format, Object arg1, Object arg2);
void error(String format, Object... arguments);
void error(String msg, Throwable t);
}
================================================
FILE: projects/sdk/core/common/src/main/java/com/tencent/shadow/core/common/LoggerFactory.java
================================================
/*
* 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.common;
public final class LoggerFactory {
volatile private static ILoggerFactory sILoggerFactory;
public static void setILoggerFactory(ILoggerFactory loggerFactory) {
if (sILoggerFactory != null) {
throw new RuntimeException("不能重复初始化");
}
sILoggerFactory = loggerFactory;
}
final public static Logger getLogger(Class> clazz) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(clazz.getName());
}
public static ILoggerFactory getILoggerFactory() {
if (sILoggerFactory == null) {
throw new RuntimeException("没有找到 ILoggerFactory 实现,请先调用setILoggerFactory");
}
return sILoggerFactory;
}
}
================================================
FILE: projects/sdk/core/common/src/main/java/com/tencent/shadow/core/runtime/container/ContentProviderDelegateProvider.java
================================================
/*
* 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.runtime.container;
/**
* ContentProvider宿主容器委托提供者
*
* 负责提供宿主容器委托实现
*
* @author owenguo
*/
public interface ContentProviderDelegateProvider {
/**
* 获取与delegator相应的HostContentProviderDelegator
*
* @return HostContentProvider被委托者
*/
HostContentProviderDelegate getHostContentProviderDelegate();
}
================================================
FILE: projects/sdk/core/common/src/main/java/com/tencent/shadow/core/runtime/container/ContentProviderDelegateProviderHolder.java
================================================
/*
* 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.runtime.container;
/**
* ContentProviderDelegateProvider依赖注入类
*
* dynamic-pluginloader通过这个类实现将PluginLoader中的ContentProviderDelegateProvider实现注入到plugincontainer中。
*
* @author owenguo
*/
public class ContentProviderDelegateProviderHolder {
static ContentProviderDelegateProvider contentProviderDelegateProvider;
public static void setContentProviderDelegateProvider(ContentProviderDelegateProvider contentProviderDelegateProvider) {
ContentProviderDelegateProviderHolder.contentProviderDelegateProvider = contentProviderDelegateProvider;
notifyDelegateProviderHolderPrepare();
}
private static DelegateProviderHolderPrepareListener sPrepareListener;
public static void setDelegateProviderHolderPrepareListener(DelegateProviderHolderPrepareListener prepareListener) {
sPrepareListener = prepareListener;
}
private static void notifyDelegateProviderHolderPrepare() {
if (sPrepareListener != null) {
sPrepareListener.onPrepare();
}
}
interface DelegateProviderHolderPrepareListener {
void onPrepare();
}
}
================================================
FILE: projects/sdk/core/common/src/main/java/com/tencent/shadow/core/runtime/container/HostContentProviderDelegate.java
================================================
/*
* 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.runtime.container;
import android.content.ContentValues;
import android.content.res.Configuration;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
/**
* PluginContainerContentProvider的被委托者接口
*
* 被委托者通过实现这个接口中声明的方法达到替代委托者实现的目的,从而将PluginContainerContentProvider的行为动态化。
*
* @author owenguo
*/
public interface HostContentProviderDelegate {
boolean onCreate();
void onConfigurationChanged(Configuration newConfig);
void onLowMemory();
void onTrimMemory(int level);
Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder);
String getType(Uri uri);
Uri insert(Uri uri, ContentValues values);
int delete(Uri uri, String selection, String[] selectionArgs);
int update(Uri uri, ContentValues values, String selection, String[] selectionArgs);
int bulkInsert(Uri uri, ContentValues[] values);
Bundle call(String method, String arg, Bundle extras);
ParcelFileDescriptor openFile(Uri uri, String mode);
ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal);
}
================================================
FILE: projects/sdk/core/common/src/main/java/com/tencent/shadow/core/runtime/container/PluginContainerContentProvider.java
================================================
/*
* 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.runtime.container;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.res.Configuration;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.FileNotFoundException;
public class PluginContainerContentProvider extends ContentProvider {
private final static String TAG = "ContentProvider_";
private HostContentProviderDelegate hostContentProviderDelegate;
public PluginContainerContentProvider() {
ContentProviderDelegateProvider p = ContentProviderDelegateProviderHolder.contentProviderDelegateProvider;
if (p != null) {
hostContentProviderDelegate = p.getHostContentProviderDelegate();
}
ContentProviderDelegateProviderHolder.setDelegateProviderHolderPrepareListener(new ContentProviderDelegateProviderHolder.DelegateProviderHolderPrepareListener() {
@Override
public void onPrepare() {
HostContentProviderDelegate delegate;
if (ContentProviderDelegateProviderHolder.contentProviderDelegateProvider != null) {
delegate = ContentProviderDelegateProviderHolder.contentProviderDelegateProvider.getHostContentProviderDelegate();
delegate.onCreate();
} else {
Log.e(TAG, "PluginContainerContentProvider: DelegateProviderHolder没有初始化");
delegate = null;
}
hostContentProviderDelegate = delegate;
}
});
}
@Override
public boolean onCreate() {
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
checkHostContentProviderDelegate();
if (hostContentProviderDelegate != null) {
return hostContentProviderDelegate.query(uri, projection, selection, selectionArgs, sortOrder);
}
return null;
}
@Override
public String getType(Uri uri) {
checkHostContentProviderDelegate();
if (hostContentProviderDelegate != null) {
return hostContentProviderDelegate.getType(uri);
}
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
checkHostContentProviderDelegate();
if (hostContentProviderDelegate != null) {
return hostContentProviderDelegate.insert(uri, values);
}
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
checkHostContentProviderDelegate();
if (hostContentProviderDelegate != null) {
return hostContentProviderDelegate.delete(uri, selection, selectionArgs);
}
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
checkHostContentProviderDelegate();
if (hostContentProviderDelegate != null) {
return hostContentProviderDelegate.update(uri, values, selection, selectionArgs);
}
return 0;
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
checkHostContentProviderDelegate();
if (hostContentProviderDelegate != null) {
return hostContentProviderDelegate.bulkInsert(uri, values);
}
return 0;
}
@Override
public Bundle call(String method, String arg, Bundle extras) {
checkHostContentProviderDelegate();
if (hostContentProviderDelegate != null) {
return hostContentProviderDelegate.call(method, arg, extras);
}
return null;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (hostContentProviderDelegate != null) {
hostContentProviderDelegate.onConfigurationChanged(newConfig);
}
}
@Override
public void onLowMemory() {
if (hostContentProviderDelegate != null) {
hostContentProviderDelegate.onLowMemory();
}
}
@Override
public void onTrimMemory(int level) {
if (hostContentProviderDelegate != null) {
hostContentProviderDelegate.onTrimMemory(level);
}
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
checkHostContentProviderDelegate();
if (hostContentProviderDelegate != null) {
return hostContentProviderDelegate.openFile(uri, mode);
} else {
return super.openFile(uri, mode);
}
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException {
checkHostContentProviderDelegate();
if (hostContentProviderDelegate != null) {
return hostContentProviderDelegate.openFile(uri, mode, signal);
} else {
return super.openFile(uri, mode);
}
}
private void checkHostContentProviderDelegate() {
if (hostContentProviderDelegate == null) {
throw new IllegalArgumentException("hostContentProviderDelegate is null ,请检查ContentProviderDelegateProviderHolder.setDelegateProviderHolderPrepareListener是否调用,或" + this.getClass().getSimpleName() + " 是否和插件在同一进程");
}
}
}
================================================
FILE: projects/sdk/core/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionUrl=https\://mirrors.tencent.com/gradle/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: projects/sdk/core/gradle-plugin/.gitignore
================================================
/build
================================================
FILE: projects/sdk/core/gradle-plugin/build.gradle
================================================
apply plugin: 'java-gradle-plugin'
apply plugin: 'kotlin'
gradlePlugin {
plugins {
shadow {
id = "com.tencent.shadow.plugin"
implementationClass = "com.tencent.shadow.core.gradle.ShadowPlugin"
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.tools.build:gradle:$build_gradle_version"
implementation "com.googlecode.json-simple:json-simple:$json_simple_version"
implementation project(':transform')
implementation project(':manifest-parser')
testImplementation "junit:junit:$junit_version"
testImplementation gradleTestKit()
}
compileKotlin {
kotlinOptions {
jvmTarget = "1.6"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "1.6"
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/AGPCompat.kt
================================================
/*
* 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.gradle
import com.android.build.gradle.AppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.internal.dsl.ProductFlavor
import org.gradle.api.Project
import org.gradle.api.Task
import java.io.File
/**
* 不同版本AGP的兼容层
*/
internal interface AGPCompat {
fun addFlavorDimension(baseExtension: BaseExtension, dimensionName: String)
fun setProductFlavorDefault(productFlavor: ProductFlavor, isDefault: Boolean)
fun getProcessResourcesTask(output: BaseVariantOutput): Task
fun getProcessResourcesFile(processResourcesTask: Task, variantName: String): File
fun getAaptAdditionalParameters(processResourcesTask: Task): List
fun getMinSdkVersion(pluginVariant: ApplicationVariant): Int
fun hasDeprecatedTransformApi(): Boolean
fun isGeneratePluginManifestByMergedManifest(
project: Project,
appExtension: AppExtension,
pluginVariant: ApplicationVariant
): Boolean
fun getProcessManifestTask(output: BaseVariantOutput): Task
fun getProcessManifestFile(
project: Project,
pluginVariant: ApplicationVariant,
output: BaseVariantOutput
): File
fun getRTxtFile(project: Project, processResourcesTask: Task?, variantName: String): File
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/AGPCompatImpl.kt
================================================
package com.tencent.shadow.core.gradle
import com.android.SdkConstants
import com.android.build.gradle.AppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.internal.dsl.ProductFlavor
import com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask
import com.android.build.gradle.internal.scope.InternalArtifactType
import com.android.sdklib.AndroidVersion.VersionCodes
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import java.io.File
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.jvm.isAccessible
internal class AGPCompatImpl : AGPCompat {
override fun getProcessResourcesTask(output: BaseVariantOutput): Task =
try {
output.processResourcesProvider.get()
} catch (e: NoSuchMethodError) {
output.processResources
}
override fun getProcessResourcesFile(processResourcesTask: Task, variantName: String): File {
val capitalizeVariantName = variantName.capitalize()
return try {
File(
processResourcesTask.outputs.files.files.first { it.name.equals("out") },
"resources-$variantName.ap_"
)
// 使用 resPackageOutputFolder
// 获取的路径和上方路径一致
// 备选(不推荐)
/*File(
(processResourcesTask as LinkApplicationAndroidResourcesTask).resPackageOutputFolder.asFile.get(),
"resources-$variantName.ap_"
)*/
} catch (ignored: Exception) {
// 高版本 AGP
try {
// 通过反射获取 KProperty: linkedResourcesOutputDir、linkedResourcesArtifactType
val linkedResourcesOutputDir =
LinkApplicationAndroidResourcesTask::class.declaredMemberProperties.first {
it.name == "linkedResourcesOutputDir"
}.let {
it.isAccessible = true
it.getter.call(processResourcesTask) as DirectoryProperty
}
@Suppress("UNCHECKED_CAST")
val linkedResourcesArtifactType =
LinkApplicationAndroidResourcesTask::class.declaredMemberProperties.first {
it.name == "linkedResourcesArtifactType"
}.let {
it.isAccessible = true
it.getter.call(processResourcesTask) as Property>
}
File(
linkedResourcesOutputDir.asFile.get(),
linkedResourcesArtifactType.get().name().lowercase()
.replace("_", "-") + "-" + variantName + SdkConstants.DOT_RES
)
} catch (ignored: Exception) {
// 反射获取出错,备用
File(
processResourcesTask.outputs.files.files.first { it.name.equals("process${capitalizeVariantName}Resources") },
"linked-resources-binary-format-$variantName.ap_"
)
}
}
}
@Suppress("PrivateApi")
override fun getAaptAdditionalParameters(processResourcesTask: Task): List =
try {
if (processResourcesTask is LinkApplicationAndroidResourcesTask) {
processResourcesTask.aaptAdditionalParameters.get()
} else {
TODO("不支持的AGP版本")
}
} catch (ignored: NoSuchMethodError) {
//AGP 4.0.0
val aaptOptionsField =
LinkApplicationAndroidResourcesTask::class.java.getDeclaredField("aaptOptions")
aaptOptionsField.isAccessible = true
val aaptOptions = aaptOptionsField.get(processResourcesTask)
val additionalParametersField = try {
aaptOptions.javaClass.getDeclaredField("additionalParameters")
} catch (ignored: NoSuchFieldException) {
//AGP 3.4.0
aaptOptions.javaClass.superclass.getDeclaredField("additionalParameters")
}
additionalParametersField.isAccessible = true
@Suppress("UNCHECKED_CAST")
val additionalParameters = additionalParametersField.get(aaptOptions) as? List
additionalParameters ?: listOf()
}
override fun addFlavorDimension(baseExtension: BaseExtension, dimensionName: String) {
val flavorDimensionList = baseExtension.flavorDimensionList
as MutableList? // AGP 3.6.0版本可能返回null
if (flavorDimensionList != null) {
flavorDimensionList.add(dimensionName)
} else {
baseExtension.flavorDimensions(dimensionName)
}
}
override fun setProductFlavorDefault(productFlavor: ProductFlavor, isDefault: Boolean) {
try {
productFlavor.isDefault = isDefault
} catch (ignored: NoSuchMethodError) {
// AGP 3.6.0版本没有这个方法,就不设置了。
// 设置Default主要是为了IDE中的Build Variants上下文自动选择时不要选成插件,
// 以便在IDE直接运行插件apk模块时运行Normal版本
}
}
override fun getMinSdkVersion(pluginVariant: ApplicationVariant): Int {
// AGP在版本升级中修改了MergedFlavor的包名,但是它实现的ProductFlavor接口没有变
val mergedFlavor = pluginVariant.mergedFlavor as com.android.builder.model.ProductFlavor
return mergedFlavor.minSdkVersion?.apiLevel ?: VersionCodes.BASE
}
override fun hasDeprecatedTransformApi(): Boolean {
try {
val version = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
val majorVersion = version.substringBefore('.', "0").toInt()
if (majorVersion >= 8) {
return false//能parse出来主版本号大于等于8,我们就认为旧版Transform API不可用了。
}
} catch (ignored: Error) {
}
//读取版本号失败,就推测是旧版本的AGP,就应该有旧版本的Transform API
return true
}
override fun isGeneratePluginManifestByMergedManifest(
project: Project,
appExtension: AppExtension,
pluginVariant: ApplicationVariant
): Boolean {
// 可以通过配置强制开启
if ("true" == project.findProperty("shadow.generatePluginManifestUseMergedManifest")) {
return true
}
// 没有开启无用资源删减,则不使用 merged manifest
try {
if (!pluginVariant.buildType.isMinifyEnabled) {
return false
}
// AppExtension 获取的 BuildType 无法获取 isShrinkResources 属性,只能查找原始的 BuildType 实现。
if (!appExtension.buildTypes.getByName(pluginVariant.buildType.name).isShrinkResources) {
return false
}
} catch (ignored: Error) {
}
// 开启无用资源删减功能,同时AGP 版本至少要为 8.9.0
try {
val version = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
val majorVersion = version.substringBefore('.', "0").toInt()
if (majorVersion > 8) {
return true
}
if (majorVersion == 8) {
val minorVersion =
version.substringAfter('.').substringBefore('.').toIntOrNull() ?: 0
return minorVersion >= 9
}
} catch (ignored: Error) {
}
// 默认不使用 merged manifest 。
return false
}
/**
* 获取生成最终 AndroidManifest.xml 文件的任务。
*/
override fun getProcessManifestTask(output: BaseVariantOutput): Task {
return try {
output.processManifestProvider.get()
} catch (_: Error) {
output.processManifest
}
}
/**
* 获取合并后的 AndroidManifest.xml 文件。
*
* 优先从 processManifest 任务输出获取,否则搜索 intermediates 目录。
*/
override fun getProcessManifestFile(
project: Project,
pluginVariant: ApplicationVariant,
output: BaseVariantOutput
): File {
// 1. 优先从任务输出获取
try {
output.processManifestProvider.get().outputs.files.files.forEach {
findFileByName(it, "AndroidManifest.xml")?.let { file -> return file }
}
} catch (_: Exception) {
// 忽略
}
val variantName = pluginVariant.name
// 2. 搜索中间产物目录
return listOf(
"intermediates/merged_manifests/$variantName", // AGP 4.x/7.x/8.x
"intermediates/manifests/full/$variantName", // AGP 3.x
)
.map { File(project.buildDir, it) }
.first {
findFileByName(it, "AndroidManifest.xml") != null
}
}
/**
* 获取 R.txt 文件。
*
* 优先从 processResources 任务的输出获取(最准确), 否则搜索 intermediates 目录。
*/
override fun getRTxtFile(
project: Project,
processResourcesTask: Task?,
variantName: String
): File {
// 1. 优先尝试从任务输出中查找
if (processResourcesTask != null) {
try {
processResourcesTask.outputs.files.files.forEach {
findFileByName(it, "R.txt")?.let { file -> return file }
}
} catch (_: Exception) {
// 忽略解析错误,继续走备选路径
}
}
// 2. 根据 AGP 版本已知的中间产物路径搜索
return listOf(
"intermediates/runtime_symbol_list/$variantName", // AGP 4.x/7.x/8.x
"intermediates/symbols/$variantName",
"intermediates/bundles/$variantName"
)
.map { File(project.buildDir, it) }
.first {
findFileByName(it, "R.txt") != null
}
}
/**
* 搜索指定目录下指定文件名的文件。
*
* @return 文件对象,若找不到则返回 null 。
*/
private fun findFileByName(file: File, fileName: String): File? {
if (!file.exists()) {
return null
}
if (file.isFile && file.name == fileName) {
return file
}
if (file.isDirectory) {
val subFiles = file.listFiles()
if (subFiles != null) {
for (subFile in subFiles) {
val resultFile = findFileByName(subFile, fileName)
if (resultFile != null) {
return resultFile
}
}
}
}
return null
}
companion object {
fun getStringFromProperty(x: Any?): String {
return when (x) {
is String -> x
is Property<*> -> x.get() as String
else -> throw Error("不支持的AGP版本")
}
}
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/CreatePackagePluginTask.kt
================================================
/*
* 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.gradle
import com.tencent.shadow.core.gradle.extensions.PackagePluginExtension
import com.tencent.shadow.core.gradle.extensions.PluginBuildType
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.bundling.Zip
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
internal fun createPackagePluginTask(project: Project, buildType: PluginBuildType): Task {
return project.tasks.create("package${buildType.name.capitalize()}Plugin", Zip::class.java) {
project.logger.info("PackagePluginTask task run")
//runtime apk file
val runtimeApkName: String = buildType.runtimeApkConfig.first
var runtimeFile: File? = null
if (runtimeApkName.isNotEmpty()) {
runtimeFile = ShadowPluginHelper.getRuntimeApkFile(project, buildType, false)
}
//loader apk file
val loaderApkName: String = buildType.loaderApkConfig.first
var loaderFile: File? = null
if (loaderApkName.isNotEmpty()) {
loaderFile = ShadowPluginHelper.getLoaderApkFile(project, buildType, false)
}
//config file
val targetConfigFile =
File(project.buildDir.absolutePath + "/intermediates/generatePluginConfig/${buildType.name}/config.json")
targetConfigFile.parentFile.mkdirs()
//all plugin apks
val pluginFiles: MutableList = mutableListOf()
for (i in buildType.pluginApks) {
pluginFiles.add(ShadowPluginHelper.getPluginFile(project, i, false))
}
it.group = "plugin"
it.description = "打包插件"
it.outputs.upToDateWhen { false }
if (runtimeFile != null) {
pluginFiles.add(runtimeFile)
}
if (loaderFile != null) {
pluginFiles.add(loaderFile)
}
it.from(pluginFiles, targetConfigFile)
val packagePlugin = project.extensions.findByName("packagePlugin")
val extension = packagePlugin as PackagePluginExtension
val suffix = if (extension.archiveSuffix.isEmpty()) "" else extension.archiveSuffix
val prefix = if (extension.archivePrefix.isEmpty()) "plugin" else extension.archivePrefix
val name =
if (suffix.isEmpty()) {
"$prefix-${buildType.name}.zip"
} else {
"$prefix-${buildType.name}-$suffix.zip"
}
it.archiveFileName.set(name)
it.destinationDirectory.set(
File(if (extension.destinationDir.isEmpty()) "${project.rootDir}/build" else extension.destinationDir)
)
}.dependsOn(createGenerateConfigTask(project, buildType))
}
private fun createGenerateConfigTask(project: Project, buildType: PluginBuildType): Task {
project.logger.info("GenerateConfigTask task run")
val packagePlugin = project.extensions.findByName("packagePlugin")
val extension = packagePlugin as PackagePluginExtension
//runtime apk build task
val runtimeApkName = buildType.runtimeApkConfig.first
var runtimeTask = ""
if (runtimeApkName.isNotEmpty()) {
runtimeTask = buildType.runtimeApkConfig.second
project.logger.info("runtime task = $runtimeTask")
}
//loader apk build task
val loaderApkName = buildType.loaderApkConfig.first
var loaderTask = ""
if (loaderApkName.isNotEmpty()) {
loaderTask = buildType.loaderApkConfig.second
project.logger.info("loader task = $loaderTask")
}
val targetConfigFile =
File(project.buildDir.absolutePath + "/intermediates/generatePluginConfig/${buildType.name}/config.json")
val pluginApkTasks: MutableList = mutableListOf()
for (i in buildType.pluginApks) {
val task = i.buildTask
project.logger.info("pluginApkProjects task = $task")
pluginApkTasks.add(task)
}
val task = project.tasks.create("generate${buildType.name.capitalize()}Config") {
it.group = "plugin"
it.description = "生成插件配置文件"
it.outputs.file(targetConfigFile)
it.outputs.upToDateWhen { false }
}
.dependsOn(pluginApkTasks)
.doLast {
project.logger.info("generateConfig task begin")
val json = extension.toJson(project, loaderApkName, runtimeApkName, buildType)
val bizWriter = BufferedWriter(FileWriter(targetConfigFile))
bizWriter.write(json.toJSONString())
bizWriter.newLine()
bizWriter.flush()
bizWriter.close()
project.logger.info("generateConfig task done")
}
if (loaderTask.isNotEmpty()) {
task.dependsOn(loaderTask)
}
if (runtimeTask.isNotEmpty()) {
task.dependsOn(runtimeTask)
}
return task
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/ShadowPlugin.kt
================================================
/*
* 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.gradle
import com.android.build.api.artifact.ScopedArtifact
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.ScopedArtifacts
import com.android.build.gradle.AppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.ApplicationVariant
import com.android.sdklib.AndroidVersion.VersionCodes
import com.tencent.shadow.core.gradle.extensions.PackagePluginExtension
import com.tencent.shadow.core.manifest_parser.createManifestValueParser
import com.tencent.shadow.core.manifest_parser.generatePluginManifest
import com.tencent.shadow.core.transform.DeprecatedTransformWrapper
import com.tencent.shadow.core.transform.GradleTransformWrapper
import com.tencent.shadow.core.transform.ShadowTransform
import com.tencent.shadow.core.transform_kit.AndroidClassPoolBuilder
import com.tencent.shadow.core.transform_kit.ClassPoolBuilder
import org.gradle.api.*
import org.gradle.api.tasks.compile.JavaCompile
import java.io.File
import java.net.URLClassLoader
import java.util.zip.ZipFile
class ShadowPlugin : Plugin {
private lateinit var androidClassPoolBuilder: ClassPoolBuilder
private lateinit var contextClassLoader: ClassLoader
private lateinit var agpCompat: AGPCompat
override fun apply(project: Project) {
agpCompat = buildAgpCompat(project)
val baseExtension = project.extensions.getByName("android") as BaseExtension
//在这里取到的contextClassLoader包含运行时库(classpath方式引入的)shadow-runtime
contextClassLoader = Thread.currentThread().contextClassLoader
val lateInitBuilder = object : ClassPoolBuilder {
override fun build() = androidClassPoolBuilder.build()
}
val shadowExtension = project.extensions.create("shadow", ShadowExtension::class.java)
if (!project.hasProperty("disable_shadow_transform")) {
val shadowTransform = ShadowTransform(
project,
lateInitBuilder,
{ shadowExtension.transformConfig.useHostContext }
)
if (agpCompat.hasDeprecatedTransformApi()) {
baseExtension.registerTransform(
DeprecatedTransformWrapper(
project,
shadowTransform
)
)
} else {
val androidComponentsExtension =
project.extensions.getByName("androidComponents") as ApplicationAndroidComponentsExtension
androidComponentsExtension.onVariants(
selector = androidComponentsExtension.selector()
.withFlavor(
ShadowTransform.DimensionName
to ShadowTransform.ApplyShadowTransformFlavorName
)
) { variant ->
val taskProvider = project.tasks.register(
"${variant.name}ShadowTransform",
GradleTransformWrapper::class.java,
shadowTransform
)
variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
.use(taskProvider)
.toTransform(
ScopedArtifact.CLASSES,
GradleTransformWrapper::allJars,
GradleTransformWrapper::allDirectories,
GradleTransformWrapper::output
)
}
}
}
addFlavorForTransform(baseExtension)
project.extensions.create("packagePlugin", PackagePluginExtension::class.java, project)
project.afterEvaluate {
initAndroidClassPoolBuilder(baseExtension, project)
createPackagePluginTasks(project)
addLocateApkanalyzerTask(project)
onEachPluginVariant(project) { pluginVariant ->
checkAaptPackageIdConfig(pluginVariant)
val appExtension: AppExtension =
project.extensions.getByType(AppExtension::class.java)
if (agpCompat.isGeneratePluginManifestByMergedManifest(
project,
appExtension,
pluginVariant
)
) {
createGeneratePluginManifestTasksByMergedManifest(
project,
appExtension,
pluginVariant
)
} else {
createGeneratePluginManifestTasks(project, appExtension, pluginVariant)
}
}
}
checkKotlinAndroidPluginForPluginManifestTask(project)
}
private fun addLocateApkanalyzerTask(project: Project) {
val appExtension: AppExtension =
project.extensions.getByType(AppExtension::class.java)
val sdkDirectory = appExtension.sdkDirectory
val outputFile = project.locateApkanalyzerResultPath()
project.tasks.register(locateApkanalyzerTaskName) {
it.inputs.property("sdkPath", sdkDirectory.path)
it.outputs.file(outputFile).withPropertyName("locateApkanalyzerResultPath")
it.doLast {
// 如果其他project的此任务执行过了,就不用再查找了
if (outputFile.exists() && File(outputFile.readText()).exists()) {
return@doLast
}
// 找出apkanalyzer.jar.它是build tool的一部分,但位置随着版本有变化,所以这里用搜索文件确定位置
// 如果有多个版本,随机取第一个,因为只用decodeXml方法,预期不同版本没什么区别。
val apkanalyzerJarFile =
try {
sdkDirectory.walk().filter { file ->
listOf(
"apkanalyzer.jar",// 低版本build tools
"apkanalyzer-classpath.jar",// 2020-06-05 cmdline-tools version 2.0
).any { it == file.name }
}.first()
} catch (e: NoSuchElementException) {
// https://developer.android.com/studio/command-line/apkanalyzer
// https://developer.android.com/studio/releases/sdk-tools
// https://cs.android.com/android/platform/superproject/+/master:prebuilts/cmdline-tools/tools/bin/apkanalyzer;l=67;bpv=1;bpt=0
throw Error(
"找不到apkanalyzer.它来自:" +
"cmdline-tools." +
"如果高版本SDK也找不到这个文件,Shadow就需要更新了。"
)
}
outputFile.parentFile.mkdirs()
outputFile.writeText(apkanalyzerJarFile.absolutePath)
}
}
}
/**
* GeneratePluginManifestTask会向android DSL添加新的java源码目录,
* 而kotlin-android会在syncKotlinAndAndroidSourceSets中接管java的源码目录,
* 从而使后添加到android DSL中的java目录失效。
*/
private fun checkKotlinAndroidPluginForPluginManifestTask(project: Project) {
if (project.plugins.hasPlugin("kotlin-android")) {
throw Error("必须在kotlin-android之前应用com.tencent.shadow.plugin")
}
}
private fun createPackagePluginTasks(project: Project) {
val packagePlugin = project.extensions.findByName("packagePlugin")
val extension = packagePlugin as PackagePluginExtension
val buildTypes = extension.buildTypes
val tasks = mutableListOf()
for (i in buildTypes) {
project.logger.info("buildTypes = " + i.name)
val task = createPackagePluginTask(project, i)
tasks.add(task)
}
if (tasks.isNotEmpty()) {
project.tasks.create("packageAllPlugin") {
it.group = "plugin"
it.description = "打包所有插件"
}.dependsOn(tasks)
}
}
private fun onEachPluginVariant(project: Project, actions: (ApplicationVariant) -> Unit) {
val appExtension: AppExtension = project.extensions.getByType(AppExtension::class.java)
val pluginVariants = appExtension.applicationVariants.filter { variant ->
variant.productFlavors.any { flavor ->
flavor.dimension == ShadowTransform.DimensionName &&
flavor.name == ShadowTransform.ApplyShadowTransformFlavorName
}
}
checkPluginVariants(pluginVariants, appExtension, project.name)
pluginVariants.forEach(actions)
}
/**
* 创建生成PluginManifest.java的任务
*/
@Suppress("PrivateApi")// for use BinaryXmlParser(apkanalyzer)
private fun createGeneratePluginManifestTasks(
project: Project,
appExtension: AppExtension,
pluginVariant: ApplicationVariant
) {
val output = pluginVariant.outputs.first()
val variantName = pluginVariant.name
val capitalizeVariantName = variantName.capitalize()
// 找出ap_文件
val processResourcesTask = agpCompat.getProcessResourcesTask(output)
val processedResFile = agpCompat.getProcessResourcesFile(processResourcesTask, variantName)
// decodeBinaryManifestTask输出的apkanalyzer manifest print结果文件
val decodeXml = File(
project.buildDir,
"intermediates/decodeBinaryManifest/$variantName/AndroidManifest.xml"
)
// 添加decodeXml任务
val decodeBinaryManifestTask =
project.tasks.register("decode${capitalizeVariantName}BinaryManifest") {
it.dependsOn(locateApkanalyzerTaskName)
it.dependsOn(processResourcesTask)
it.inputs.file(processedResFile)
it.outputs.file(decodeXml).withPropertyName("decodeXml")
it.doLast {
val zipFile = ZipFile(processedResFile)
val binaryXml = zipFile.getInputStream(
zipFile.getEntry("AndroidManifest.xml")
).readBytes()
val outputXmlBytes = decodeXml(project, binaryXml)
decodeXml.parentFile.mkdirs()
decodeXml.writeBytes(outputXmlBytes)
}
}
// 添加生成PluginManifest.java任务
val pluginManifestSourceDir =
File(project.buildDir, "generated/source/pluginManifest/$variantName")
val generatePluginManifestTask =
project.tasks.register("generate${capitalizeVariantName}PluginManifest") {
it.dependsOn(decodeBinaryManifestTask)
it.inputs.file(decodeXml)
it.outputs.dir(pluginManifestSourceDir).withPropertyName("pluginManifestSourceDir")
it.doLast {
generatePluginManifest(
decodeXml,
pluginManifestSourceDir,
"com.tencent.shadow.core.manifest_parser"
)
}
}
val javacTask = project.tasks.getByName("compile${capitalizeVariantName}JavaWithJavac")
javacTask.dependsOn(generatePluginManifestTask)
// 把PluginManifest.java添加为源码
val relativePath =
project.projectDir.toPath().relativize(pluginManifestSourceDir.toPath()).toString()
(javacTask as JavaCompile).source(project.fileTree(relativePath))
}
private fun createGeneratePluginManifestTasksByMergedManifest(
project: Project,
appExtension: AppExtension,
pluginVariant: ApplicationVariant
) {
val output = pluginVariant.outputs.first()
val variantName = pluginVariant.name
val capitalizeVariantName = variantName.capitalize()
// 添加生成PluginManifest.java任务
val pluginManifestSourceDir =
File(project.buildDir, "generated/source/pluginManifest/$variantName")
val javacTask = project.tasks.getByName("compile${capitalizeVariantName}JavaWithJavac")
val generatePluginManifestTask =
project.tasks.register("generate${capitalizeVariantName}PluginManifest") {
// 依赖 processManifest 任务以获取最终的 AndroidManifest.xml
val processManifestTask = agpCompat.getProcessManifestTask(output)
// 依赖 processResources 任务以获取 R.txt
val processResourcesTask = agpCompat.getProcessResourcesTask(output)
it.dependsOn(processManifestTask)
it.dependsOn(processResourcesTask)
it.outputs.dir(pluginManifestSourceDir).withPropertyName("pluginManifestSourceDir")
it.doLast {
// 解析合并后的 AndroidManifest.xml + R.txt
// 这种方案直接解析 XML 格式的 AndroidManifest.xml ,不再依赖 aapt2 产生的二进制产物
val mergedManifest =
agpCompat.getProcessManifestFile(project, pluginVariant, output)
val rTxt = agpCompat.getRTxtFile(project, processResourcesTask, variantName)
val manifestValueParser = createManifestValueParser(rTxt)
generatePluginManifest(
mergedManifest,
pluginManifestSourceDir,
"com.tencent.shadow.core.manifest_parser",
manifestValueParser
)
}
}
javacTask.dependsOn(generatePluginManifestTask)
// 把PluginManifest.java添加为源码
val relativePath =
project.projectDir.toPath().relativize(pluginManifestSourceDir.toPath()).toString()
(javacTask as JavaCompile).source(project.fileTree(relativePath))
}
/**
* 反射apkanalyzer中的BinaryXmlParser类的decodeXml方法
*/
@Suppress("PrivateApi")
private fun decodeXml(project: Project, binaryXml: ByteArray): ByteArray {
val jarPath = File(project.locateApkanalyzerResultPath().readText())
val tempCL = URLClassLoader(arrayOf(jarPath.toURL()), contextClassLoader)
val binaryXmlParserClass =
tempCL.loadClass("com.android.tools.apk.analyzer.BinaryXmlParser")
return try {
decodeXmlMethodV1(binaryXmlParserClass, binaryXml)
} catch (ignored: Exception) {
decodeXmlMethodV2(binaryXmlParserClass, binaryXml)
}
}
private fun decodeXmlMethodV1(binaryXmlParserClass: Class<*>, binaryXml: ByteArray): ByteArray {
val decodeXmlMethod = binaryXmlParserClass.getDeclaredMethod(
"decodeXml",
String::class.java,
ByteArray::class.java
)
return decodeXmlMethod.invoke(
null,
"AndroidManifest.xml",
binaryXml
) as ByteArray
}
/**
* 新版本代码中删掉了一个String参数,这个参数原来只用于log输出了
* https://cs.android.com/android-studio/platform/tools/base/+/6a81855c2fa102ae4532ad9a645e40177770a26a:apkparser/analyzer/src/main/java/com/android/tools/apk/analyzer/BinaryXmlParser.java;dlc=598c38100e4fb2b001385faea994fcb54cc515b1
*/
private fun decodeXmlMethodV2(binaryXmlParserClass: Class<*>, binaryXml: ByteArray): ByteArray {
val decodeXmlMethod = binaryXmlParserClass.getDeclaredMethod(
"decodeXml",
ByteArray::class.java
)
return decodeXmlMethod.invoke(
null,
binaryXml
) as ByteArray
}
/**
* 检查插件是否修改了资源ID分区
*
* 因为CreateResourceBloc在为插件创建Resources对象时,
* 将宿主和插件的apk都放进去了,所以不能让宿主和插件的资源ID冲突。详见CreateResourceBloc注释。
*
* 此任务只是检查任务,对构建无影响。
*/
private fun checkAaptPackageIdConfig(pluginVariant: ApplicationVariant) {
val output = pluginVariant.outputs.first()
val minSdkVersion = agpCompat.getMinSdkVersion(pluginVariant)
val processResourcesTask = agpCompat.getProcessResourcesTask(output)
processResourcesTask.doFirst {
val parameterList = agpCompat.getAaptAdditionalParameters(processResourcesTask)
var foundPackageIdParameter = false
parameterList.forEachIndexed { index, parameter ->
if (parameter == "--package-id" && parameterList.size >= index + 2) {
val packageIdSetting = parameterList[index + 1]
val packageIdValue = Integer.decode(packageIdSetting)
if (minSdkVersion > VersionCodes.O) {
if (packageIdValue <= 0x7f) {
throw Error("minSdkVersion大于26时--package-id必须大于0x7f")
} else {
foundPackageIdParameter = true
}
} else {
if (packageIdValue >= 0x7f) {
/*
为了兼容minSDK小于26,且packageId大于0x7f时Android系统的bug,aapt对id进行了修改,
导致Resources中记录的id值和layout中使用的id值不一致。
但是minSDK小于26时可以使用--allow-reserved-package-id选项使用小于0x7f的值。
https://android.googlesource.com/platform/frameworks/base/+/master/tools/aapt2/readme.md#version-2_14
https://developer.android.com/studio/command-line/aapt2#link_options
*/
throw Error(
"minSdkVersion小于26时--package-id必须小于0x7f," +
"同时使用--allow-reserved-package-id选项。"
)
} else {
foundPackageIdParameter = true
}
}
}
}
if (!foundPackageIdParameter) {
val example1 = "aaptOptions {\n" +
" additionalParameters \"--package-id\", \"0x80\"\n" +
"}"
val example2 = "aaptOptions {\n" +
" additionalParameters \"--package-id\", \"0x7E\", \"--allow-reserved-package-id\"\n" +
"}"
val example = if (minSdkVersion > VersionCodes.O) example1 else example2
throw Error(
"插件需要利用aapt2的修改资源ID前缀的选项使其与宿主不同。\n" +
"没有找到--package-id参数。示例:\n" + example
)
}
}
}
private fun checkPluginVariants(
pluginVariants: List,
appExtension: AppExtension,
projectName: String
) {
if (pluginVariants.isEmpty()) {
val errorMessage = StringBuilder()
errorMessage.appendLine("在${projectName}中找不到Shadow所添加的Dimension flavor")
errorMessage.appendLine("当前所有flavor打印如下:")
appExtension.applicationVariants.forEach { variant ->
errorMessage.appendLine("variant.name:${variant.name}")
variant.productFlavors.forEach { flavor ->
errorMessage.appendLine(
"flavor.name:${flavor.name} flavor.dimension:${flavor.dimension} "
)
}
}
errorMessage.appendLine("提示:添加flavorDimension时,不要覆盖已有flavorDimension")
errorMessage.appendLine("示例:flavorDimensions(*flavorDimensionList, 'new')")
throw Error(errorMessage.toString())
}
}
private fun addFlavorForTransform(baseExtension: BaseExtension) {
agpCompat.addFlavorDimension(baseExtension, ShadowTransform.DimensionName)
try {
baseExtension.productFlavors.create(ShadowTransform.NoShadowTransformFlavorName) {
it.dimension = ShadowTransform.DimensionName
agpCompat.setProductFlavorDefault(it, true)
}
baseExtension.productFlavors.create(ShadowTransform.ApplyShadowTransformFlavorName) {
it.dimension = ShadowTransform.DimensionName
agpCompat.setProductFlavorDefault(it, false)
}
} catch (e: InvalidUserDataException) {
throw Error("请在android{} DSL之前apply plugin: 'com.tencent.shadow.plugin'", e)
}
}
private fun initAndroidClassPoolBuilder(
baseExtension: BaseExtension,
project: Project
) {
val sdkDirectory = baseExtension.sdkDirectory
val compileSdkVersion =
baseExtension.compileSdkVersion ?: throw IllegalStateException("compileSdkVersion获取失败")
val androidJarPath = "platforms/${compileSdkVersion}/android.jar"
val androidJar = File(sdkDirectory, androidJarPath)
androidClassPoolBuilder = AndroidClassPoolBuilder(project, contextClassLoader, androidJar)
}
open class ShadowExtension {
var transformConfig = TransformConfig()
fun transform(action: Action) {
action.execute(transformConfig)
}
}
class TransformConfig {
var useHostContext: Array = emptyArray()
}
companion object {
const val locateApkanalyzerTaskName = "locateApkanalyzer"
private fun Project.locateApkanalyzerResultPath() =
File(rootProject.buildDir, "shadow/ApkanalyzerPath.txt")
private fun buildAgpCompat(project: Project): AGPCompat {
return AGPCompatImpl()
}
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/ShadowPluginHelper.kt
================================================
/*
* 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.gradle
import com.tencent.shadow.core.gradle.extensions.PackagePluginExtension
import com.tencent.shadow.core.gradle.extensions.PluginApkConfig
import com.tencent.shadow.core.gradle.extensions.PluginBuildType
import org.gradle.api.Project
import java.io.File
import java.io.FileInputStream
import java.security.MessageDigest
import kotlin.experimental.and
open class ShadowPluginHelper {
companion object {
fun getFileMD5(file: File): String? {
if (!file.isFile) {
return null
}
val buffer = ByteArray(1024)
var len: Int
var inStream: FileInputStream? = null
val digest = MessageDigest.getInstance("MD5")
try {
inStream = FileInputStream(file)
do {
len = inStream.read(buffer, 0, 1024)
if (len != -1) {
digest.update(buffer, 0, len)
}
} while (len != -1)
} catch (e: Exception) {
e.printStackTrace()
return null
} finally {
inStream?.close()
}
return bytes2HexStr(digest.digest())
}
private fun bytes2HexStr(bytes: ByteArray?): String {
val HEX_ARRAY = "0123456789ABCDEF".toCharArray()
if (bytes == null || bytes.isEmpty()) {
return ""
}
val buf = CharArray(2 * bytes.size)
try {
for (i in bytes.indices) {
var b = bytes[i]
buf[2 * i + 1] = HEX_ARRAY[(b and 0xF).toInt()]
b = b.toInt().ushr(4).toByte()
buf[2 * i + 0] = HEX_ARRAY[(b and 0xF).toInt()]
}
} catch (e: Exception) {
return ""
}
return String(buf)
}
fun getRuntimeApkFile(
project: Project,
buildType: PluginBuildType,
checkExist: Boolean
): File {
val packagePlugin = project.extensions.findByName("packagePlugin")
val extension = packagePlugin as PackagePluginExtension
val splitList = buildType.runtimeApkConfig.second.split(":")
val runtimeFileParent =
splitList[splitList.lastIndex].replace("assemble", "").toLowerCase()
val runtimeApkName: String = buildType.runtimeApkConfig.first
val runtimeFile = File(
"${project.rootDir}" +
"/${extension.runtimeApkProjectPath}/build/outputs/apk/$runtimeFileParent/$runtimeApkName"
)
if (checkExist && !runtimeFile.exists()) {
throw IllegalArgumentException(runtimeFile.absolutePath + " , runtime file not exist...")
}
project.logger.info("runtimeFile = $runtimeFile")
return runtimeFile
}
fun getLoaderApkFile(
project: Project,
buildType: PluginBuildType,
checkExist: Boolean
): File {
val packagePlugin = project.extensions.findByName("packagePlugin")
val extension = packagePlugin as PackagePluginExtension
val loaderApkName: String = buildType.loaderApkConfig.first
val splitList = buildType.loaderApkConfig.second.split(":")
val loaderFileParent =
splitList[splitList.lastIndex].replace("assemble", "").toLowerCase()
val loaderFile = File(
"${project.rootDir}" +
"/${extension.loaderApkProjectPath}/build/outputs/apk/$loaderFileParent/$loaderApkName"
)
if (checkExist && !loaderFile.exists()) {
throw IllegalArgumentException(loaderFile.absolutePath + " , loader file not exist...")
}
project.logger.info("loaderFile = $loaderFile")
return loaderFile
}
fun getPluginFile(
project: Project,
pluginConfig: PluginApkConfig,
checkExist: Boolean
): File {
val pluginFile = File(project.rootDir, pluginConfig.apkPath)
if (checkExist && !pluginFile.exists()) {
throw IllegalArgumentException(pluginFile.absolutePath + " , plugin file not exist...")
}
project.logger.info("pluginFile = $pluginFile")
return pluginFile
}
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/extensions/PackagePluginExtension.kt
================================================
/*
* 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.gradle.extensions
import com.tencent.shadow.core.gradle.ShadowPluginHelper
import groovy.lang.Closure
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.json.simple.JSONArray
import org.json.simple.JSONObject
import java.io.File
import java.util.*
open class PackagePluginExtension {
var loaderApkProjectPath = ""
var runtimeApkProjectPath = ""
var archivePrefix = ""
var archiveSuffix = ""
var destinationDir = ""
var uuid = ""
var version: Int = 0
var uuidNickName = ""
var compactVersion: Array = emptyArray()
var buildTypes: NamedDomainObjectContainer
constructor(project: Project) {
buildTypes = project.container(PluginBuildType::class.java)
buildTypes.all {
it.pluginApks = project.container(PluginApkConfig::class.java)
}
}
fun pluginTypes(closure: Closure) {
buildTypes.configure(closure)
}
fun toJson(
project: Project,
loaderApkName: String,
runtimeApkName: String,
buildType: PluginBuildType
): JSONObject {
val json = JSONObject()
if (loaderApkName.isNotEmpty()) {
//Json文件中 plugin-loader部分信息
val pluginLoaderObj = JSONObject()
pluginLoaderObj["apkName"] = loaderApkName
val loaderFile = ShadowPluginHelper.getLoaderApkFile(project, buildType, true)
pluginLoaderObj["hash"] = ShadowPluginHelper.getFileMD5(loaderFile)
json["pluginLoader"] = pluginLoaderObj
}
if (runtimeApkName.isNotEmpty()) {
//Json文件中 plugin-runtime部分信息
val runtimeObj = JSONObject()
runtimeObj["apkName"] = runtimeApkName
val runtimeFile = ShadowPluginHelper.getRuntimeApkFile(project, buildType, true)
runtimeObj["hash"] = ShadowPluginHelper.getFileMD5(runtimeFile)
json["runtime"] = runtimeObj
}
//Json文件中 plugin部分信息
val jsonArr = JSONArray()
for (i in buildType.pluginApks) {
val pluginObj = JSONObject()
pluginObj["businessName"] = i.businessName
pluginObj["partKey"] = i.partKey
pluginObj["apkName"] = File(i.apkPath).name
pluginObj["hash"] =
ShadowPluginHelper.getFileMD5(ShadowPluginHelper.getPluginFile(project, i, true))
if (i.dependsOn.isNotEmpty()) {
val dependsOnJson = JSONArray()
for (k in i.dependsOn) {
dependsOnJson.add(k)
}
pluginObj["dependsOn"] = dependsOnJson
}
if (i.hostWhiteList.isNotEmpty()) {
val hostWhiteListJson = JSONArray()
for (k in i.hostWhiteList) {
hostWhiteListJson.add(k)
}
pluginObj["hostWhiteList"] = hostWhiteListJson
}
jsonArr.add(pluginObj)
}
json["plugins"] = jsonArr
//Config.json版本号
if (version > 0) {
json["version"] = version
} else {
json["version"] = 1
}
//uuid UUID_NickName
val uuid = "${project.rootDir}" + "/build/uuid.txt"
val uuidFile = File(uuid)
when {
uuidFile.exists() -> {
json["UUID"] = uuidFile.readText()
project.logger.info("uuid = " + json["UUID"] + " 由文件生成")
}
this.uuid.isEmpty() -> {
json["UUID"] = UUID.randomUUID().toString().toUpperCase()
project.logger.info("uuid = " + json["UUID"] + " 随机生成")
}
else -> {
json["UUID"] = this.uuid
project.logger.info("uuid = " + json["UUID"] + " 由配置生成")
}
}
if (uuidNickName.isNotEmpty()) {
json["UUID_NickName"] = uuidNickName
} else {
json["UUID_NickName"] = "1.0"
}
if (compactVersion.isNotEmpty()) {
val jsonArray = JSONArray()
for (i in compactVersion) {
jsonArray.add(i)
}
json["compact_version"] = jsonArray
}
return json
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/extensions/PluginApkConfig.kt
================================================
/*
* 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.gradle.extensions
open class PluginApkConfig {
var name = ""
var partKey = ""
/**
* 业务名(空字符串表示同宿主相同业务)
*/
var businessName = ""
var apkPath = ""
var buildTask = ""
var dependsOn: Array = emptyArray()
var hostWhiteList: Array = emptyArray()
constructor(name: String) {
this.name = name
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/extensions/PluginBuildType.kt
================================================
/*
* 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.gradle.extensions
import groovy.lang.Closure
import groovy.lang.Tuple2
import org.gradle.api.NamedDomainObjectContainer
open class PluginBuildType {
var name = ""
var loaderApkConfig: Tuple2 = Tuple2("", "")
var runtimeApkConfig: Tuple2 = Tuple2("", "")
lateinit var pluginApks: NamedDomainObjectContainer
constructor(name: String) {
this.name = name
}
fun pluginApks(closure: Closure) {
pluginApks.configure(closure)
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackageMultiPluginTest.kt
================================================
/*
* 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.gradle
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.json.simple.JSONArray
import org.json.simple.JSONObject
import org.json.simple.parser.JSONParser
import org.junit.Assert
import org.junit.Test
import java.io.File
import java.util.zip.ZipFile
/**
* 测试打包多个工程的插件包 第一个插件包包含loader、runtime、插件1、config.json的插件包 第二个插件包 包含 插件2、config.json
* 其中插件包1 中的json文件中的uuid 需要和 插件包2 中json文件的uuid 一样
* ./gradlew -p projects/sdk/core :gradle-plugin:test --tests com.tencent.shadow.core.gradle.PackageMultiPluginTest.testPackageMultiPlugin
*/
class PackageMultiPluginTest {
@Test
fun testPackageMultiPlugin() {
GradleRunner.create()
.withProjectDir(ROOT_PROJECT_DIR)
.withPluginClasspath()
.withArguments("clean")
.build()
val result = GradleRunner.create()
.withProjectDir(ROOT_PROJECT_DIR)
.withPluginClasspath()
.withArguments(
listOf(
"-xgeneratePluginDebugPluginManifest",
"-Pdisable_shadow_transform=true",
":plugin1:PackageMultiPlugin"
)
)
.build()
val outcome = result.task(":plugin1:PackageMultiPlugin")!!.outcome
Assert.assertEquals(TaskOutcome.SUCCESS, outcome)
assertJson()
assertFile()
}
private fun assertFile() {
val zipFile = ZipFile(ROOT_PROJECT_DIR.absolutePath + "/build/plugin-debug.zip")
val zipFileNames = mutableSetOf()
zipFileNames.add("config.json")
zipFileNames.add("plugin1-plugin-debug.apk")
zipFileNames.add("loader-debug.apk")
zipFileNames.add("runtime-debug.apk")
var entries = zipFile.entries()
Assert.assertEquals(4, zipFile.size())
for (i in entries) {
zipFileNames.remove(i.name)
}
Assert.assertEquals(0, zipFileNames.size)
val case2ZipFile = ZipFile(ROOT_PROJECT_DIR.absolutePath + "/build/plugin-plugin2Debug.zip")
zipFileNames.add("config.json")
zipFileNames.add("plugin2-plugin-debug.apk")
entries = case2ZipFile.entries()
Assert.assertEquals(2, case2ZipFile.size())
for (i in entries) {
zipFileNames.remove(i.name)
}
Assert.assertEquals(0, zipFileNames.size)
}
private fun assertJson() {
val jsonFile =
File(PLUGIN1_PROJECT_DIR, "build/intermediates/generatePluginConfig/debug/config.json")
val json = JSONParser().parse(jsonFile.bufferedReader()) as JSONObject
Assert.assertEquals(4L, json["version"])
Assert.assertEquals("1.1.5", json["UUID_NickName"])
val compactVersionArr: JSONArray = json["compact_version"] as JSONArray
Assert.assertEquals(1L, compactVersionArr[0] as Long)
val loaderJson = json["pluginLoader"] as JSONObject
Assert.assertEquals("loader-debug.apk", loaderJson["apkName"])
Assert.assertNotNull(loaderJson["hash"])
val runtimeJson = json["runtime"] as JSONObject
Assert.assertEquals("runtime-debug.apk", runtimeJson["apkName"])
Assert.assertNotNull(runtimeJson["hash"])
val pluginsJson = json["plugins"] as JSONArray
val pluginJson = pluginsJson[0] as JSONObject
Assert.assertEquals("plugin1", pluginJson["partKey"])
Assert.assertEquals("plugin1-plugin-debug.apk", pluginJson["apkName"])
val dependsOnJson = pluginJson["dependsOn"] as JSONArray
Assert.assertEquals(2, dependsOnJson.size)
Assert.assertNotNull(pluginJson["hash"])
val hostWhiteListJson = pluginJson["hostWhiteList"] as JSONArray
Assert.assertEquals(2, hostWhiteListJson.size)
val case2JsonFile = File(
PLUGIN2_PROJECT_DIR,
"/build/intermediates/generatePluginConfig/plugin2Debug/config.json"
)
val case2Json = JSONParser().parse(case2JsonFile.bufferedReader()) as JSONObject
Assert.assertEquals(case2Json["UUID"], json["UUID"])
}
companion object {
val ROOT_PROJECT_DIR = File("src/test/testProjects/case1")
val PLUGIN1_PROJECT_DIR = File("src/test/testProjects/case1/plugin1")
val PLUGIN2_PROJECT_DIR = File("src/test/testProjects/case1/plugin2")
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackageOnlyPluginTest.kt
================================================
/*
* 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.gradle
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.json.simple.JSONArray
import org.json.simple.JSONObject
import org.json.simple.parser.JSONParser
import org.junit.Assert
import org.junit.Test
import java.io.File
import java.util.zip.ZipFile
/**
* 测试打包只包含、插件1、config.json的插件包
* ./gradlew -p projects/sdk/core :gradle-plugin:test --tests com.tencent.shadow.core.gradle.PackageOnlyPluginTest.testCase1PackageOnlyApk
*/
class PackageOnlyPluginTest {
@Test
fun testCase1PackageOnlyApk() {
GradleRunner.create()
.withProjectDir(PLUGIN1_PROJECT_DIR)
.withPluginClasspath()
.withArguments("clean")
.build()
val result = GradleRunner.create()
.withProjectDir(PLUGIN1_PROJECT_DIR)
.withPluginClasspath()
.withArguments(
listOf(
"-xgeneratePluginDebugPluginManifest",
"-Pdisable_shadow_transform=true",
":plugin1:packageOnlyApkPlugin"
)
)
.build()
val outcome = result.task(":plugin1:packageOnlyApkPlugin")!!.outcome
Assert.assertEquals(TaskOutcome.SUCCESS, outcome)
val jsonFile = File(
PLUGIN1_PROJECT_DIR,
"build/intermediates/generatePluginConfig/onlyApk/config.json"
)
val json = JSONParser().parse(jsonFile.bufferedReader()) as JSONObject
assertJson(json)
val zipFile = ZipFile(ROOT_PROJECT_DIR.absolutePath + "/build/plugin-onlyApk.zip")
assertFile(zipFile)
}
private fun assertFile(zipFile: ZipFile) {
val zipFileNames = mutableSetOf()
zipFileNames.add("config.json")
zipFileNames.add("plugin1-plugin-debug.apk")
val entries = zipFile.entries()
Assert.assertEquals(2, zipFile.size())
for (i in entries) {
zipFileNames.remove(i.name)
}
Assert.assertEquals(0, zipFileNames.size)
}
private fun assertJson(json: JSONObject) {
Assert.assertEquals(4L, json["version"])
Assert.assertEquals("1234567890", json["UUID"])
Assert.assertEquals("1.1.5", json["UUID_NickName"])
val compactVersionArr: JSONArray = json["compact_version"] as JSONArray
Assert.assertEquals(1L, compactVersionArr[0] as Long)
val pluginsJson = json["plugins"] as JSONArray
val pluginJson = pluginsJson[0] as JSONObject
Assert.assertEquals("plugin1", pluginJson["partKey"])
Assert.assertEquals("plugin1-plugin-debug.apk", pluginJson["apkName"])
val dependsOnJson = pluginJson["dependsOn"] as JSONArray
Assert.assertEquals(2, dependsOnJson.size)
Assert.assertNotNull(pluginJson["hash"])
val hostWhiteList = pluginJson["hostWhiteList"] as JSONArray
Assert.assertEquals(2, hostWhiteList.size)
}
companion object {
val ROOT_PROJECT_DIR = File("src/test/testProjects/case1")
val PLUGIN1_PROJECT_DIR = File("src/test/testProjects/case1/plugin1")
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackagePluginTaskTest.kt
================================================
/*
* 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.gradle
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome.SUCCESS
import org.json.simple.JSONArray
import org.json.simple.JSONObject
import org.json.simple.parser.JSONParser
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import java.io.File
import java.util.zip.ZipFile
/**
* 测试打包包含loader、runtime、插件1、config.json的插件包
* ./gradlew -p projects/sdk/core :gradle-plugin:test --tests com.tencent.shadow.core.gradle.PackagePluginTaskTest.testCase1PackageDebugPlugin
*/
class PackagePluginTaskTest {
@Test
fun testCase1PackageDebugPlugin() {
GradleRunner.create()
.withProjectDir(ROOT_PROJECT_DIR)
.withPluginClasspath()
.withArguments("clean")
.build()
val result = GradleRunner.create()
.withProjectDir(ROOT_PROJECT_DIR)
.withPluginClasspath()
.withArguments(
listOf(
"-xgeneratePluginDebugPluginManifest",
"-Pdisable_shadow_transform=true",
":plugin1:packageDebugPlugin"
)
)
.build()
val outcome = result.task(":plugin1:packageDebugPlugin")!!.outcome
assertEquals(SUCCESS, outcome)
val jsonFile =
File(PLUGIN1_PROJECT_DIR, "build/intermediates/generatePluginConfig/debug/config.json")
val json = JSONParser().parse(jsonFile.bufferedReader()) as JSONObject
assertJson(json)
val zipFile = ZipFile(ROOT_PROJECT_DIR.absolutePath + "/build/plugin-debug.zip")
assertFile(zipFile)
}
private fun assertFile(zipFile: ZipFile) {
val zipFileNames = mutableSetOf()
zipFileNames.add("config.json")
zipFileNames.add("plugin1-plugin-debug.apk")
zipFileNames.add("loader-debug.apk")
zipFileNames.add("runtime-debug.apk")
val entries = zipFile.entries()
assertEquals(4, zipFile.size())
for (i in entries) {
zipFileNames.remove(i.name)
}
assertEquals(0, zipFileNames.size)
}
private fun assertJson(json: JSONObject) {
assertEquals(4L, json["version"])
assertEquals("1234567890", json["UUID"])
assertEquals("1.1.5", json["UUID_NickName"])
val compactVersionArr: JSONArray = json["compact_version"] as JSONArray
assertEquals(1L, compactVersionArr[0] as Long)
val loaderJson = json["pluginLoader"] as JSONObject
assertEquals("loader-debug.apk", loaderJson["apkName"])
assertNotNull(loaderJson["hash"])
val runtimeJson = json["runtime"] as JSONObject
assertEquals("runtime-debug.apk", runtimeJson["apkName"])
assertNotNull(runtimeJson["hash"])
val pluginsJson = json["plugins"] as JSONArray
val pluginJson = pluginsJson[0] as JSONObject
assertEquals("plugin1", pluginJson["partKey"])
assertEquals("plugin1", pluginJson["businessName"])
assertEquals("plugin1-plugin-debug.apk", pluginJson["apkName"])
val dependsOnJson = pluginJson["dependsOn"] as JSONArray
assertEquals(2, dependsOnJson.size)
assertNotNull(pluginJson["hash"])
val hostWhiteList = pluginJson["hostWhiteList"] as JSONArray
Assert.assertEquals(2, hostWhiteList.size)
}
companion object {
val ROOT_PROJECT_DIR = File("src/test/testProjects/case1")
val PLUGIN1_PROJECT_DIR = File("src/test/testProjects/case1/plugin1")
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/.gitignore
================================================
*.iml
.gradle
/local.properties
.idea
.DS_Store
build
/captures
.externalNativeBuild
.gradletasknamecache
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/build.gradle
================================================
buildscript {
loadVersions:
{// 读取versions.properties到ext中,供项目中直接用变量引用版本号
def versions_properties_path = '../../../../../../../../buildScripts/gradle/versions.properties'
def versions = new Properties()
versions.load(file(versions_properties_path).newReader())
versions.forEach { key, stringValue ->
def value = stringValue?.isInteger() ? stringValue as Integer : stringValue
ext.set(key, value)
}
}
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath "com.android.tools.build:gradle:$build_gradle_version"
}
}
plugins {
id 'com.android.application'
}
allprojects {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
}
ext.disable_shadow_transform = true
android {
compileSdkVersion COMPILE_SDK_VERSION
defaultConfig {
applicationId "com.tencent.shadow.test.gradle.case1"
minSdkVersion MIN_SDK_VERSION
targetSdkVersion TARGET_SDK_VERSION
versionCode VERSION_CODE
versionName VERSION_NAME
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/loader/build.gradle
================================================
plugins {
id 'com.android.application'
}
android {
compileSdkVersion COMPILE_SDK_VERSION
defaultConfig {
applicationId "com.tencent.shadow.test.gradle.case1"
minSdkVersion MIN_SDK_VERSION
targetSdkVersion TARGET_SDK_VERSION
versionCode VERSION_CODE
versionName VERSION_NAME
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/loader/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/plugin1/.gitignore
================================================
*.iml
.gradle
/local.properties
.idea
.DS_Store
build
/captures
.externalNativeBuild
.gradletasknamecache
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/plugin1/build.gradle
================================================
buildscript {
loadVersions:
{// 读取versions.properties到ext中,供项目中直接用变量引用版本号
def versions_properties_path = '../../../../../../../../../buildScripts/gradle/versions.properties'
def versions = new Properties()
versions.load(file(versions_properties_path).newReader())
versions.forEach { key, stringValue ->
def value = stringValue?.isInteger() ? stringValue as Integer : stringValue
ext.set(key, value)
}
}
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath "com.android.tools.build:gradle:$build_gradle_version"
}
}
plugins {
id 'com.android.application'
id 'com.tencent.shadow.plugin'
}
allprojects {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
}
ext.disable_shadow_transform = true
android {
compileSdkVersion COMPILE_SDK_VERSION
defaultConfig {
applicationId "com.tencent.shadow.test.gradle.plugin1"
minSdkVersion MIN_SDK_VERSION
targetSdkVersion TARGET_SDK_VERSION
versionCode VERSION_CODE
versionName VERSION_NAME
}
// 将插件的资源ID分区改为和宿主0x7F不同的值
aaptOptions {
additionalParameters "--package-id", "0x7E", "--allow-reserved-package-id"
}
}
shadow {
packagePlugin {
pluginTypes {
debug {
loaderApkConfig = new Tuple2('loader-debug.apk', ':loader:assembleDebug')
runtimeApkConfig = new Tuple2('runtime-debug.apk', ':runtime:assembleDebug')
pluginApks {
pluginApk1 {
businessName = 'plugin1'
partKey = 'plugin1'
buildTask = ':plugin1:assemblePluginDebug'
apkPath = 'plugin1/build/outputs/apk/plugin/debug/plugin1-plugin-debug.apk'
dependsOn = ['Core', 'Base']
hostWhiteList = ["androidx.test.espresso",
"com.tencent.shadow.test.lib.plugin_use_host_code_lib.interfaces"]
}
}
}
onlyApk {
pluginApks {
pluginApk1 {
partKey = 'plugin1'
buildTask = ':plugin1:assemblePluginDebug'
apkPath = 'plugin1/build/outputs/apk/plugin/debug/plugin1-plugin-debug.apk'
dependsOn = ['Core', 'Base']
hostWhiteList = ["androidx.test.espresso",
"com.tencent.shadow.test.lib.plugin_use_host_code_lib.interfaces"]
}
}
}
}
loaderApkProjectPath = 'loader'
runtimeApkProjectPath = 'runtime'
uuid = '1234567890'
version = 4
compactVersion = [1, 2, 3]
uuidNickName = "1.1.5"
}
}
task genUUID() {
doFirst {
def uuidFile = file(rootProject.projectDir.absolutePath + '/build/uuid.txt')
uuidFile.getParentFile().mkdirs()
BufferedWriter writer = new BufferedWriter(new FileWriter(uuidFile))
writer.write(UUID.randomUUID().toString().toUpperCase())
writer.flush()
writer.close()
}
}
task PackageMultiPlugin(dependsOn: ['genUUID', 'packageDebugPlugin', ':plugin2:packagePlugin2DebugPlugin']) {
doLast {
file(rootProject.projectDir.absolutePath + '/build/uuid.txt').delete()
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/plugin1/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/plugin2/.gitignore
================================================
*.iml
.gradle
/local.properties
.idea
.DS_Store
build
/captures
.externalNativeBuild
.gradletasknamecache
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/plugin2/build.gradle
================================================
buildscript {
loadVersions:
{// 读取versions.properties到ext中,供项目中直接用变量引用版本号
def versions_properties_path = '../../../../../../../../../buildScripts/gradle/versions.properties'
def versions = new Properties()
versions.load(file(versions_properties_path).newReader())
versions.forEach { key, stringValue ->
def value = stringValue?.isInteger() ? stringValue as Integer : stringValue
ext.set(key, value)
}
}
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
dependencies {
classpath "com.android.tools.build:gradle:$build_gradle_version"
}
}
plugins {
id 'com.android.application'
id 'com.tencent.shadow.plugin'
}
allprojects {
repositories {
if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {
maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }
} else {
google()
mavenCentral()
}
}
}
ext.disable_shadow_transform = true
android {
compileSdkVersion COMPILE_SDK_VERSION
defaultConfig {
applicationId "com.tencent.shadow.test.gradle.case1"
minSdkVersion MIN_SDK_VERSION
targetSdkVersion TARGET_SDK_VERSION
versionCode VERSION_CODE
versionName VERSION_NAME
}
// 将插件的资源ID分区改为和宿主0x7F不同的值
aaptOptions {
additionalParameters "--package-id", "0x7E", "--allow-reserved-package-id"
}
}
shadow {
packagePlugin {
pluginTypes {
plugin2Debug {
pluginApks {
pluginApk1 {
partKey = 'plugin2'
buildTask = ':plugin2:assemblePluginDebug'
//这里因为单元测试时,会把项目根目录设置成case1的根目录
apkPath = 'plugin2/build/outputs/apk/plugin/debug/plugin2-plugin-debug.apk'
dependsOn = ['Core', 'Base']
hostWhiteList = ["androidx.test.espresso",
"com.tencent.shadow.test.lib.plugin_use_host_code_lib.interfaces"]
}
}
}
}
uuid = '1234567890'
version = 4
compactVersion = [1, 2, 3]
uuidNickName = "1.1.5"
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/plugin2/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/runtime/build.gradle
================================================
plugins {
id 'com.android.application'
}
android {
compileSdkVersion COMPILE_SDK_VERSION
defaultConfig {
applicationId "com.tencent.shadow.test.gradle.case1"
minSdkVersion MIN_SDK_VERSION
targetSdkVersion TARGET_SDK_VERSION
versionCode VERSION_CODE
versionName VERSION_NAME
}
}
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/runtime/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/settings.gradle
================================================
rootProject.name = 'case1'
include 'loader', 'runtime', 'plugin1', 'plugin2'
================================================
FILE: projects/sdk/core/gradle-plugin/src/test/testProjects/case1/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sdk/core/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx4096m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useAndroidX=true
================================================
FILE: projects/sdk/core/gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: projects/sdk/core/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: projects/sdk/core/load-parameters/.gitignore
================================================
/build
*.iml
================================================
FILE: projects/sdk/core/load-parameters/build.gradle
================================================
apply plugin: 'com.tencent.shadow.internal.common-jar-settings'
================================================
FILE: projects/sdk/core/load-parameters/src/main/java/com/tencent/shadow/core/load_parameters/LoadParameters.java
================================================
/*
* 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.load_parameters;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Loader加载插件的输入参数结构体
*
* 这个类不能用Kotlin写是因为这个类可能会由非Kotlin写的代码new出来,
* 而Loader打包的kotlin运行时可能连同Loader一起在一个独立的ClassLoader中。
* 如果这个类用Kotlin写,就要求构造这个类对象的代码具有Kotlin运行时。
*
* @author cubershi
*/
public class LoadParameters implements Parcelable {
public final String businessName;
public final String partKey;
public final String[] dependsOn;
public final String[] hostWhiteList;
public LoadParameters(String businessName, String partKey, String[] dependsOn, String[] hostWhiteList) {
this.businessName = businessName;
this.partKey = partKey;
this.dependsOn = dependsOn;
this.hostWhiteList = hostWhiteList;
}
public LoadParameters(Parcel in) {
businessName = in.readString();
partKey = in.readString();
dependsOn = in.createStringArray();
hostWhiteList = in.createStringArray();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(businessName);
dest.writeString(partKey);
dest.writeStringArray(dependsOn);
dest.writeStringArray(hostWhiteList);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator CREATOR = new Creator() {
@Override
public LoadParameters createFromParcel(Parcel in) {
return new LoadParameters(in);
}
@Override
public LoadParameters[] newArray(int size) {
return new LoadParameters[size];
}
};
}
================================================
FILE: projects/sdk/core/loader/.gitignore
================================================
/build
================================================
FILE: projects/sdk/core/loader/build.gradle
================================================
import com.tencent.shadow.coding.code_generator.ActivityCodeGenerator
import com.tencent.shadow.coding.common_jar_settings.AndroidJar
buildscript {
dependencies {
classpath 'com.tencent.shadow.coding:android-jar'
classpath 'com.tencent.shadow.coding:code-generator'
}
}
apply plugin: 'com.tencent.shadow.internal.common-jar-settings'
apply plugin: 'kotlin'
java {
sourceSets {
main.java.srcDirs += 'build/generated/sources/code-generator'
}
}
compileKotlin {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
kotlinOptions {
jvmTarget = "1.6"
noJdk = true
noStdlib = true
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation "junit:junit:$junit_version"
implementation 'com.tencent.shadow.coding:java-build-config'
api project(':runtime')
compileOnly project(':activity-container')
compileOnly project(':common')
api project(':load-parameters')
compileOnly files(AndroidJar.ANDROID_JAR_PATH)
}
def generateCode = tasks.register('generateCode') {
def outputDir = layout.buildDirectory.dir('generated/sources/code-generator')
outputs.dir(outputDir)
.withPropertyName('outputDir')
doLast {
ActivityCodeGenerator codeGenerator = new ActivityCodeGenerator()
codeGenerator.generate(outputDir.get().getAsFile(), "loader")
}
}
compileJava.dependsOn(generateCode)
compileKotlin.dependsOn(generateCode)
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/ShadowPluginLoader.kt
================================================
/*
* 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
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Parcel
import com.tencent.shadow.core.common.InstalledApk
import com.tencent.shadow.core.common.LoggerFactory
import com.tencent.shadow.core.load_parameters.LoadParameters
import com.tencent.shadow.core.loader.blocs.LoadPluginBloc
import com.tencent.shadow.core.loader.delegates.*
import com.tencent.shadow.core.loader.exceptions.LoadPluginException
import com.tencent.shadow.core.loader.infos.PluginParts
import com.tencent.shadow.core.loader.managers.ComponentManager
import com.tencent.shadow.core.loader.managers.PluginContentProviderManager
import com.tencent.shadow.core.loader.managers.PluginServiceManager
import com.tencent.shadow.core.runtime.UriConverter
import com.tencent.shadow.core.runtime.container.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
abstract class ShadowPluginLoader(hostAppContext: Context) : DelegateProvider, DI,
ContentProviderDelegateProvider {
protected val mExecutorService = Executors.newCachedThreadPool()
open val delegateProviderKey: String = DelegateProviderHolder.DEFAULT_KEY
/**
* loadPlugin方法是在子线程被调用的。而getHostActivityDelegate方法是在主线程被调用的。
* 两个方法需要传递数据(主要是PluginParts),因此需要同步。
*/
private val mLock = ReentrantLock()
/**
* 多插件Map
* key: partKey
* value: PluginParts
* @GuardedBy("mLock")
*/
private val mPluginPartsMap = hashMapOf()
lateinit var mComponentManager: ComponentManager
/**
* @GuardedBy("mLock")
*/
abstract fun getComponentManager(): ComponentManager
private lateinit var mPluginServiceManager: PluginServiceManager
private val mPluginContentProviderManager: PluginContentProviderManager =
PluginContentProviderManager()
private val mPluginServiceManagerLock = ReentrantLock()
private val mHostAppContext: Context = hostAppContext
private val mUiHandler = Handler(Looper.getMainLooper())
companion object {
private val mLogger = LoggerFactory.getLogger(ShadowPluginLoader::class.java)
}
init {
UriConverter.setUriParseDelegate(mPluginContentProviderManager)
}
fun getPluginServiceManager(): PluginServiceManager {
mPluginServiceManagerLock.withLock {
return mPluginServiceManager
}
}
fun getPluginParts(partKey: String): PluginParts? {
mLock.withLock {
return mPluginPartsMap[partKey]
}
}
fun getAllPluginPart(): HashMap {
mLock.withLock {
return mPluginPartsMap
}
}
fun onCreate() {
mComponentManager = getComponentManager()
mComponentManager.setPluginContentProviderManager(mPluginContentProviderManager)
}
fun callApplicationOnCreate(partKey: String) {
fun realAction() {
val pluginParts = getPluginParts(partKey)
pluginParts?.let {
val application = pluginParts.application
application.attachBaseContext(mHostAppContext)
mPluginContentProviderManager.createContentProviderAndCallOnCreate(
application, partKey, pluginParts
)
application.onCreate()
}
}
if (isUiThread()) {
realAction()
} else {
val waitUiLock = CountDownLatch(1)
mUiHandler.post {
realAction()
waitUiLock.countDown()
}
waitUiLock.await();
}
}
@Throws(LoadPluginException::class)
open fun loadPlugin(
installedApk: InstalledApk
): Future<*> {
val loadParameters = installedApk.getLoadParameters()
if (mLogger.isInfoEnabled) {
mLogger.info("start loadPlugin")
}
// 在这里初始化PluginServiceManager
mPluginServiceManagerLock.withLock {
if (!::mPluginServiceManager.isInitialized) {
mPluginServiceManager = PluginServiceManager(this, mHostAppContext)
}
mComponentManager.setPluginServiceManager(mPluginServiceManager)
}
return LoadPluginBloc.loadPlugin(
mExecutorService,
mComponentManager,
mLock,
mPluginPartsMap,
mHostAppContext,
installedApk,
loadParameters
)
}
override fun getHostActivityDelegate(aClass: Class): HostActivityDelegate {
return if (HostNativeActivityDelegator::class.java.isAssignableFrom(aClass)) {
ShadowNativeActivityDelegate(this)
} else {
ShadowActivityDelegate(this)
}
}
override fun getHostContentProviderDelegate(): HostContentProviderDelegate {
return ShadowContentProviderDelegate(mPluginContentProviderManager)
}
override fun inject(delegate: ShadowDelegate, partKey: String) {
mLock.withLock {
val pluginParts = mPluginPartsMap[partKey]
if (pluginParts == null) {
throw IllegalStateException("partKey==${partKey}在map中找不到。此时map:${mPluginPartsMap}")
} else {
delegate.inject(pluginParts.appComponentFactory)
delegate.inject(pluginParts.application)
delegate.inject(pluginParts.classLoader)
delegate.inject(pluginParts.resources)
delegate.inject(mComponentManager)
}
}
}
fun InstalledApk.getLoadParameters(): LoadParameters {
val parcel = Parcel.obtain()
parcel.unmarshall(parcelExtras, 0, parcelExtras.size)
parcel.setDataPosition(0)
val loadParameters = LoadParameters(parcel)
parcel.recycle()
return loadParameters
}
private fun isUiThread(): Boolean {
return Thread.currentThread() === Looper.getMainLooper().thread
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/blocs/CheckPackageNameBloc.kt
================================================
/*
* 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.blocs
import android.content.Context
import com.tencent.shadow.core.loader.exceptions.ParsePluginApkException
import com.tencent.shadow.core.runtime.PluginManifest
object CheckPackageNameBloc {
@Throws(ParsePluginApkException::class)
fun check(
pluginManifest: PluginManifest,
hostAppContext: Context
) {
if (pluginManifest.applicationPackageName != hostAppContext.packageName) {
/*
要求插件和宿主包名一致有两方面原因:
1.正常的构建过程中,aapt会将包名写入到arsc文件中。插件正常安装运行时,如果以
android.content.Context.getPackageName为参数传给
android.content.res.Resources.getIdentifier方法,可以正常获取到资源。但是在插件环境运行时,
Context.getPackageName会得到宿主的packageName,则getIdentifier方法不能正常获取到资源。为此,
一个可选的办法是继承Resources,覆盖getIdentifier方法。但是Resources的构造器已经被标记为
@Deprecated了,未来可能会不可用,因此不首选这个方法。
2.Android系统,更多情况下是OEM修改的Android系统,会在我们的context上调用getPackageName或者
getOpPackageName等方法,然后将这个packageName跨进程传递做它用。系统的其他代码会以这个packageName
去PackageManager中查询权限等信息。如果插件使用自己的包名,就需要在Context的getPackageName等实现中
new Throwable(),然后判断调用来源以决定返回自己的包名还是插件的包名。但是如果保持采用宿主的包名,则没有
这个烦恼。
我们也可以始终认为Shadow App是宿主的扩展代码,使用是宿主的一部分,那么采用宿主的包名就是理所应当的了。
*/
throw ParsePluginApkException("插件和宿主包名不一致。宿主:${hostAppContext.packageName} 插件:${pluginManifest.applicationPackageName}")
}
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/blocs/CreateApplicationBloc.kt
================================================
/*
* 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.blocs
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.res.Resources
import com.tencent.shadow.core.load_parameters.LoadParameters
import com.tencent.shadow.core.loader.classloaders.PluginClassLoader
import com.tencent.shadow.core.loader.exceptions.CreateApplicationException
import com.tencent.shadow.core.loader.managers.ComponentManager
import com.tencent.shadow.core.runtime.PluginManifest
import com.tencent.shadow.core.runtime.ShadowAppComponentFactory
import com.tencent.shadow.core.runtime.ShadowApplication
/**
* 初始化插件Application类
*
* @author cubershi
*/
object CreateApplicationBloc {
@Throws(CreateApplicationException::class)
fun createShadowApplication(
pluginClassLoader: PluginClassLoader,
loadParameters: LoadParameters,
pluginManifest: PluginManifest,
resources: Resources,
hostAppContext: Context,
componentManager: ComponentManager,
pluginApplicationInfo: ApplicationInfo,
appComponentFactory: ShadowAppComponentFactory
): ShadowApplication {
try {
val appClassName = pluginManifest.applicationClassName
?: ShadowApplication::class.java.name
val shadowApplication =
appComponentFactory.instantiateApplication(pluginClassLoader, appClassName)
val partKey = loadParameters.partKey
shadowApplication.setPluginResources(resources)
shadowApplication.setPluginClassLoader(pluginClassLoader)
shadowApplication.setPluginComponentLauncher(componentManager)
shadowApplication.setBroadcasts(pluginManifest.receivers)
shadowApplication.setAppComponentFactory(appComponentFactory)
shadowApplication.applicationInfo = pluginApplicationInfo
shadowApplication.setBusinessName(loadParameters.businessName)
shadowApplication.setPluginPartKey(partKey)
shadowApplication.setShadowApplication(shadowApplication)
//和ShadowActivityDelegate.initPluginActivity一样,attachBaseContext放到最后
shadowApplication.setHostApplicationContextAsBase(hostAppContext)
shadowApplication.setTheme(pluginManifest.applicationTheme)
return shadowApplication
} catch (e: Exception) {
throw CreateApplicationException(e)
}
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/blocs/CreatePluginApplicationInfoBloc.kt
================================================
package com.tencent.shadow.core.loader.blocs
import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Build
import com.tencent.shadow.core.common.InstalledApk
import com.tencent.shadow.core.load_parameters.LoadParameters
import com.tencent.shadow.core.runtime.PluginManifest
import com.tencent.shadow.core.runtime.ShadowContext
import java.io.File
object CreatePluginApplicationInfoBloc {
fun create(
installedApk: InstalledApk,
loadParameters: LoadParameters,
pluginManifest: PluginManifest,
hostAppContext: Context
): ApplicationInfo {
val result = ApplicationInfo(hostAppContext.applicationInfo)
result.sourceDir = installedApk.apkFilePath
result.nativeLibraryDir = installedApk.libraryPath
result.dataDir = makeDataDir(loadParameters, hostAppContext).absolutePath
result.packageName = pluginManifest.applicationPackageName
result.className = pluginManifest.applicationClassName
result.theme = pluginManifest.applicationTheme
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
result.appComponentFactory = pluginManifest.appComponentFactory
}
return result
}
fun makeDataDir(loadParameters: LoadParameters, hostAppContext: Context): File {
val tempContext = ShadowContext(hostAppContext, 0).apply {
setBusinessName(loadParameters.businessName)
}
val dataDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
tempContext.dataDir
} else {
File(tempContext.filesDir, "dataDir")
}
dataDir.mkdirs()
return dataDir
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/blocs/CreateResourceBloc.kt
================================================
/*
* 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.blocs
import android.annotation.TargetApi
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.util.TypedValue
import android.webkit.WebView
import java.util.concurrent.CountDownLatch
object CreateResourceBloc {
/**
* 构造插件Resources时有两个方案,都要解决一个共同的问题:
* 系统有可能从宿主Manifest中获取app icon或者logo的资源ID,
* 然后直接向插件的Resources对象查询这些资源。
*
* 第一个方案是MixResources方案,但该方案依赖Resources的Deprecated构造器,
* 未来可能会不可用。实际上Resources的构造器如果不取消的话,这个方案可以一直使用下去。
*
* 第二个方案是利用资源分区,这是一个和AAB设计中dynamic-feature相同的方案,
* 将宿主和插件apk添加到同一个Resources对象中。
* 尽管构造这种带有多资源ID分区的Resources对象所需的API在低版本系统上就已经有了,
* 但通过不断测试发现MAX_API_FOR_MIX_RESOURCES及更低的API系统上,有个别API不能正确支持非0x7f分区的资源。
*/
const val MAX_API_FOR_MIX_RESOURCES = Build.VERSION_CODES.O_MR1
fun create(archiveFilePath: String, hostAppContext: Context): Resources {
triggerWebViewHookResources(hostAppContext)
val packageManager = hostAppContext.packageManager
val applicationInfo = ApplicationInfo()
val hostApplicationInfo = hostAppContext.applicationInfo
applicationInfo.packageName = hostApplicationInfo.packageName
applicationInfo.uid = hostApplicationInfo.uid
if (Build.VERSION.SDK_INT > MAX_API_FOR_MIX_RESOURCES) {
fillApplicationInfoForNewerApi(applicationInfo, hostApplicationInfo, archiveFilePath)
} else {
fillApplicationInfoForLowerApi(applicationInfo, hostApplicationInfo, archiveFilePath)
}
try {
val pluginResource = packageManager.getResourcesForApplication(applicationInfo)
return if (Build.VERSION.SDK_INT > MAX_API_FOR_MIX_RESOURCES) {
pluginResource
} else {
val hostResources = hostAppContext.resources
MixResources(pluginResource, hostResources)
}
} catch (e: PackageManager.NameNotFoundException) {
throw RuntimeException(e)
}
}
/**
* WebView初始化时会向系统构造的Resources对象注入webview.apk,
* 以便WebView可以使用自己的资源。
*
* 由于它不会向插件构造的Resources对象注入apk,
* 所以我们先初始化它,让它注入给宿主,等插件构造Resources时从宿主中复制出该apk路径。
*/
private fun triggerWebViewHookResources(hostAppContext: Context) {
//先用宿主context初始化一个WebView,以便WebView的逻辑去修改sharedLibraryFiles,将webview.apk添加进去
val latch = CountDownLatch(1)
Handler(Looper.getMainLooper()).post {
try {
WebView(hostAppContext)
} catch (ignored: Exception) {
// API 26虚拟机报No WebView installed
}
latch.countDown()
}
latch.await()
}
private fun fillApplicationInfoForNewerApi(
applicationInfo: ApplicationInfo,
hostApplicationInfo: ApplicationInfo,
pluginApkPath: String
) {
/**
* 这里虽然sourceDir和sharedLibraryFiles中指定的apk都会进入Resources对象,
* 但是只有资源id分区大于0x7f时才能在加载之后保持住资源id分区。
* 如果把宿主的apk路径放到sharedLibraryFiles中,我们假设宿主资源id分区是0x7f,
* 则加载后会变为一个随机的分区,如0x30。因此放入sharedLibraryFiles中的apk的
* 资源id分区都需要改为0x80或更大的值。
*
* 考虑到现网可能已经有旧方案运行的宿主和插件,而宿主不易更新。
* 因此新方案假设宿主保持0x7f固定不能修改,但是插件可以重新编译新版本修改资源id分区。
* 因此把插件apk路径放到sharedLibraryFiles中。
*
* 复制宿主的sharedLibraryFiles,主要是为了获取前面WebView初始化时,
* 系统使用私有API注入的webview.apk
*/
applicationInfo.publicSourceDir = hostApplicationInfo.publicSourceDir
applicationInfo.sourceDir = hostApplicationInfo.sourceDir
// hostSharedLibraryFiles中可能有webview通过私有api注入的webview.apk
val hostSharedLibraryFiles = hostApplicationInfo.sharedLibraryFiles
val otherApksAddToResources =
if (hostSharedLibraryFiles == null)
arrayOf(pluginApkPath)
else
arrayOf(
*hostSharedLibraryFiles,
pluginApkPath
)
applicationInfo.sharedLibraryFiles = otherApksAddToResources
}
/**
* API 25及以下系统,单独构造插件资源
*/
private fun fillApplicationInfoForLowerApi(
applicationInfo: ApplicationInfo,
hostApplicationInfo: ApplicationInfo,
pluginApkPath: String
) {
applicationInfo.publicSourceDir = pluginApkPath
applicationInfo.sourceDir = pluginApkPath
applicationInfo.sharedLibraryFiles = hostApplicationInfo.sharedLibraryFiles
}
}
/**
* 在API 25及以下代替设置sharedLibraryFiles后通过getResourcesForApplication创建资源的方案。
* 因调用addAssetPath方法也无法满足CreateResourceTest涉及的场景。
*/
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
@TargetApi(CreateResourceBloc.MAX_API_FOR_MIX_RESOURCES)
private class MixResources(
private val mainResources: Resources,
private val sharedResources: Resources
) : Resources(mainResources.assets, mainResources.displayMetrics, mainResources.configuration) {
private var beforeInitDone = false
private var updateConfigurationCalledInInit = false
/**
* 低版本系统中Resources构造器中会调用updateConfiguration方法,
* 此时mainResources还没有初始化。
*/
init {
if (updateConfigurationCalledInInit) {
updateConfiguration(mainResources.configuration, mainResources.displayMetrics)
}
beforeInitDone = true
}
private fun tryMainThenShared(function: (res: Resources) -> R) = try {
function(mainResources)
} catch (e: NotFoundException) {
function(sharedResources)
}
override fun getText(id: Int) = tryMainThenShared { it.getText(id) }
override fun getText(id: Int, def: CharSequence?) = tryMainThenShared { it.getText(id, def) }
override fun getQuantityText(id: Int, quantity: Int) =
tryMainThenShared { it.getQuantityText(id, quantity) }
override fun getString(id: Int) =
tryMainThenShared { it.getString(id) }
override fun getString(id: Int, vararg formatArgs: Any?) =
tryMainThenShared { it.getString(id, *formatArgs) }
override fun getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any?) =
tryMainThenShared { it.getQuantityString(id, quantity, *formatArgs) }
override fun getQuantityString(id: Int, quantity: Int) =
tryMainThenShared {
it.getQuantityString(id, quantity)
}
override fun getTextArray(id: Int) =
tryMainThenShared {
it.getTextArray(id)
}
override fun getStringArray(id: Int) =
tryMainThenShared {
it.getStringArray(id)
}
override fun getIntArray(id: Int) =
tryMainThenShared {
it.getIntArray(id)
}
override fun obtainTypedArray(id: Int) =
tryMainThenShared {
it.obtainTypedArray(id)
}
override fun getDimension(id: Int) =
tryMainThenShared {
it.getDimension(id)
}
override fun getDimensionPixelOffset(id: Int) =
tryMainThenShared {
it.getDimensionPixelOffset(id)
}
override fun getDimensionPixelSize(id: Int) =
tryMainThenShared {
it.getDimensionPixelSize(id)
}
override fun getFraction(id: Int, base: Int, pbase: Int) =
tryMainThenShared {
it.getFraction(id, base, pbase)
}
override fun getDrawable(id: Int) =
tryMainThenShared {
it.getDrawable(id)
}
override fun getDrawable(id: Int, theme: Theme?) =
tryMainThenShared {
it.getDrawable(id, theme)
}
override fun getDrawableForDensity(id: Int, density: Int) =
tryMainThenShared {
it.getDrawableForDensity(id, density)
}
override fun getDrawableForDensity(id: Int, density: Int, theme: Theme?) =
tryMainThenShared {
it.getDrawableForDensity(id, density, theme)
}
override fun getMovie(id: Int) =
tryMainThenShared {
it.getMovie(id)
}
override fun getColor(id: Int) =
tryMainThenShared {
it.getColor(id)
}
override fun getColor(id: Int, theme: Theme?) =
tryMainThenShared {
it.getColor(id, theme)
}
override fun getColorStateList(id: Int) =
tryMainThenShared {
it.getColorStateList(id)
}
override fun getColorStateList(id: Int, theme: Theme?) =
tryMainThenShared {
it.getColorStateList(id, theme)
}
override fun getBoolean(id: Int) =
tryMainThenShared {
it.getBoolean(id)
}
override fun getInteger(id: Int) =
tryMainThenShared {
it.getInteger(id)
}
override fun getLayout(id: Int) =
tryMainThenShared {
it.getLayout(id)
}
override fun getAnimation(id: Int) =
tryMainThenShared {
it.getAnimation(id)
}
override fun getXml(id: Int) =
tryMainThenShared {
it.getXml(id)
}
override fun openRawResource(id: Int) =
tryMainThenShared {
it.openRawResource(id)
}
override fun openRawResource(id: Int, value: TypedValue?) =
tryMainThenShared {
it.openRawResource(id, value)
}
override fun openRawResourceFd(id: Int) =
tryMainThenShared {
it.openRawResourceFd(id)
}
override fun getValue(id: Int, outValue: TypedValue?, resolveRefs: Boolean) =
tryMainThenShared {
it.getValue(id, outValue, resolveRefs)
}
override fun getValue(name: String?, outValue: TypedValue?, resolveRefs: Boolean) =
tryMainThenShared {
it.getValue(name, outValue, resolveRefs)
}
override fun getValueForDensity(
id: Int,
density: Int,
outValue: TypedValue?,
resolveRefs: Boolean
) =
tryMainThenShared {
it.getValueForDensity(id, density, outValue, resolveRefs)
}
override fun obtainAttributes(set: AttributeSet?, attrs: IntArray?) =
tryMainThenShared {
it.obtainAttributes(set, attrs)
}
override fun updateConfiguration(config: Configuration?, metrics: DisplayMetrics?) {
if (beforeInitDone) {
tryMainThenShared {
it.updateConfiguration(config, metrics)
}
}
}
override fun getDisplayMetrics() =
tryMainThenShared {
it.getDisplayMetrics()
}
override fun getConfiguration() =
tryMainThenShared {
it.getConfiguration()
}
override fun getIdentifier(name: String?, defType: String?, defPackage: String?) =
tryMainThenShared {
it.getIdentifier(name, defType, defPackage)
}
override fun getResourceName(resid: Int) =
tryMainThenShared {
it.getResourceName(resid)
}
override fun getResourcePackageName(resid: Int) =
tryMainThenShared {
it.getResourcePackageName(resid)
}
override fun getResourceTypeName(resid: Int) =
tryMainThenShared {
it.getResourceTypeName(resid)
}
override fun getResourceEntryName(resid: Int) =
tryMainThenShared {
it.getResourceEntryName(resid)
}
override fun parseBundleExtras(parser: XmlResourceParser?, outBundle: Bundle?) =
tryMainThenShared {
it.parseBundleExtras(parser, outBundle)
}
override fun parseBundleExtra(tagName: String?, attrs: AttributeSet?, outBundle: Bundle?) =
tryMainThenShared {
it.parseBundleExtra(tagName, attrs, outBundle)
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/blocs/LoadApkBloc.kt
================================================
/*
* 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.blocs
import com.tencent.shadow.core.common.InstalledApk
import com.tencent.shadow.core.common.Logger
import com.tencent.shadow.core.load_parameters.LoadParameters
import com.tencent.shadow.core.loader.classloaders.CombineClassLoader
import com.tencent.shadow.core.loader.classloaders.PluginClassLoader
import com.tencent.shadow.core.loader.exceptions.LoadApkException
import com.tencent.shadow.core.loader.infos.PluginParts
import java.io.File
/**
* 加载插件到ClassLoader中
*
* @author cubershi
*/
object LoadApkBloc {
/**
* 加载插件到ClassLoader中.
*
* @param installedPlugin 已安装(PluginManager已经下载解包)的插件
* @return 加载了插件的ClassLoader
*/
@Throws(LoadApkException::class)
fun loadPlugin(
installedApk: InstalledApk,
loadParameters: LoadParameters,
pluginPartsMap: MutableMap
): PluginClassLoader {
val apk = File(installedApk.apkFilePath)
val odexDir = if (installedApk.oDexPath == null) null else File(installedApk.oDexPath)
val dependsOn = loadParameters.dependsOn
//Logger类一定打包在宿主中,所在的classLoader即为加载宿主的classLoader
val hostClassLoader: ClassLoader = Logger::class.java.classLoader!!
val hostParentClassLoader = hostClassLoader.parent
if (dependsOn == null || dependsOn.isEmpty()) {
return PluginClassLoader(
apk.absolutePath,
odexDir,
installedApk.libraryPath,
hostClassLoader,
hostParentClassLoader,
loadParameters.hostWhiteList
)
} else if (dependsOn.size == 1) {
val partKey = dependsOn[0]
val pluginParts = pluginPartsMap[partKey]
if (pluginParts == null) {
throw LoadApkException("加载" + loadParameters.partKey + "时它的依赖" + partKey + "还没有加载")
} else {
return PluginClassLoader(
apk.absolutePath,
odexDir,
installedApk.libraryPath,
pluginParts.classLoader,
null,
loadParameters.hostWhiteList
)
}
} else {
val dependsOnClassLoaders = dependsOn.map {
val pluginParts = pluginPartsMap[it]
if (pluginParts == null) {
throw LoadApkException("加载" + loadParameters.partKey + "时它的依赖" + it + "还没有加载")
} else {
pluginParts.classLoader
}
}.toTypedArray()
val combineClassLoader =
CombineClassLoader(dependsOnClassLoaders, hostParentClassLoader)
return PluginClassLoader(
apk.absolutePath,
odexDir,
installedApk.libraryPath,
combineClassLoader,
null,
loadParameters.hostWhiteList
)
}
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/blocs/LoadPluginBloc.kt
================================================
/*
* 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.blocs
import android.content.Context
import com.tencent.shadow.core.common.InstalledApk
import com.tencent.shadow.core.load_parameters.LoadParameters
import com.tencent.shadow.core.loader.exceptions.LoadPluginException
import com.tencent.shadow.core.loader.infos.PluginParts
import com.tencent.shadow.core.loader.managers.ComponentManager
import com.tencent.shadow.core.loader.managers.PluginPackageManagerImpl
import com.tencent.shadow.core.runtime.PluginPartInfo
import com.tencent.shadow.core.runtime.PluginPartInfoManager
import com.tencent.shadow.core.runtime.ShadowAppComponentFactory
import java.io.File
import java.util.concurrent.Callable
import java.util.concurrent.ExecutorService
import java.util.concurrent.Future
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
object LoadPluginBloc {
@Throws(LoadPluginException::class)
fun loadPlugin(
executorService: ExecutorService,
componentManager: ComponentManager,
lock: ReentrantLock,
pluginPartsMap: MutableMap,
hostAppContext: Context,
installedApk: InstalledApk,
loadParameters: LoadParameters
): Future<*> {
if (installedApk.apkFilePath == null) {
throw LoadPluginException("apkFilePath==null")
} else {
val buildClassLoader = executorService.submit(Callable {
lock.withLock {
LoadApkBloc.loadPlugin(installedApk, loadParameters, pluginPartsMap)
}
})
val buildPluginManifest = executorService.submit(Callable {
val pluginClassLoader = buildClassLoader.get()
val pluginManifest = pluginClassLoader.loadPluginManifest()
CheckPackageNameBloc.check(pluginManifest, hostAppContext)
pluginManifest
})
val buildPluginApplicationInfo = executorService.submit(Callable {
val pluginManifest = buildPluginManifest.get()
val pluginApplicationInfo = CreatePluginApplicationInfoBloc.create(
installedApk,
loadParameters,
pluginManifest,
hostAppContext
)
pluginApplicationInfo
})
val buildPackageManager = executorService.submit(Callable {
val pluginApplicationInfo = buildPluginApplicationInfo.get()
val hostPackageManager = hostAppContext.packageManager
PluginPackageManagerImpl(
pluginApplicationInfo,
installedApk.apkFilePath,
componentManager,
hostPackageManager,
)
})
val buildResources = executorService.submit(Callable {
CreateResourceBloc.create(installedApk.apkFilePath, hostAppContext)
})
val buildAppComponentFactory = executorService.submit(Callable {
val pluginClassLoader = buildClassLoader.get()
val pluginManifest = buildPluginManifest.get()
val appComponentFactory = pluginManifest.appComponentFactory
if (appComponentFactory != null) {
val clazz = pluginClassLoader.loadClass(appComponentFactory)
ShadowAppComponentFactory::class.java.cast(clazz.newInstance())
} else ShadowAppComponentFactory()
})
val buildApplication = executorService.submit(Callable {
val pluginClassLoader = buildClassLoader.get()
val resources = buildResources.get()
val appComponentFactory = buildAppComponentFactory.get()
val pluginManifest = buildPluginManifest.get()
val pluginApplicationInfo = buildPluginApplicationInfo.get()
CreateApplicationBloc.createShadowApplication(
pluginClassLoader,
loadParameters,
pluginManifest,
resources,
hostAppContext,
componentManager,
pluginApplicationInfo,
appComponentFactory
)
})
val buildRunningPlugin = executorService.submit {
if (File(installedApk.apkFilePath).exists().not()) {
throw LoadPluginException("插件文件不存在.pluginFile==" + installedApk.apkFilePath)
}
val pluginPackageManager = buildPackageManager.get()
val pluginClassLoader = buildClassLoader.get()
val resources = buildResources.get()
val shadowApplication = buildApplication.get()
val appComponentFactory = buildAppComponentFactory.get()
val pluginManifest = buildPluginManifest.get()
lock.withLock {
componentManager.addPluginApkInfo(
pluginManifest,
loadParameters,
installedApk.apkFilePath,
)
pluginPartsMap[loadParameters.partKey] = PluginParts(
appComponentFactory,
shadowApplication,
pluginClassLoader,
resources,
pluginPackageManager
)
PluginPartInfoManager.addPluginInfo(
pluginClassLoader, PluginPartInfo(
shadowApplication, resources,
pluginClassLoader, pluginPackageManager
)
)
}
}
return buildRunningPlugin
}
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/classloaders/CombineClassLoader.kt
================================================
/*
* 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.classloaders
import android.os.Build
class CombineClassLoader(private val classLoaders: Array, parent: ClassLoader) :
ClassLoader(parent) {
@Throws(ClassNotFoundException::class)
override fun loadClass(name: String, resolve: Boolean): Class<*> {
var c: Class<*>? = findLoadedClass(name)
val classNotFoundException = ClassNotFoundException(name)
if (c == null) {
try {
c = super.loadClass(name, resolve)
} catch (e: ClassNotFoundException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
classNotFoundException.addSuppressed(e)
}
}
if (c == null) {
for (classLoader in classLoaders) {
try {
c = classLoader.loadClass(name)!!
break
} catch (e: ClassNotFoundException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
classNotFoundException.addSuppressed(e)
}
}
}
if (c == null) {
throw classNotFoundException
}
}
}
return c
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/classloaders/PluginClassLoader.kt
================================================
/*
* 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.classloaders
import android.os.Build
import com.tencent.shadow.core.runtime.PluginManifest
import dalvik.system.BaseDexClassLoader
import org.jetbrains.annotations.TestOnly
import java.io.File
/**
* 用于加载插件的ClassLoader,插件内部的classLoader树结构如下
* BootClassLoader
* |
* xxxClassLoader
* | |
* PathClassLoader |
* | |
* PluginClassLoaderA CombineClassLoader
* |
* PluginClassLoaderB PluginClassLoaderC
*
*/
class PluginClassLoader(
dexPath: String,
optimizedDirectory: File?,
librarySearchPath: String?,
parent: ClassLoader,
private val specialClassLoader: ClassLoader?, hostWhiteList: Array?
) : BaseDexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parent) {
/**
* 宿主的白名单包名
* 在白名单包里面的宿主类,插件才可以访问
*/
private val allHostWhiteTrie = PackageNameTrie()
private val loaderClassLoader = PluginClassLoader::class.java.classLoader!!
init {
hostWhiteList?.forEach {
allHostWhiteTrie.insert(it)
}
//org.apache.commons.logging是非常特殊的的包,由系统放到App的PathClassLoader中.
allHostWhiteTrie.insert("org.apache.commons.logging")
//Android 9.0以下的系统里面带有http包,走系统的不走本地的
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
allHostWhiteTrie.insert("org.apache.http")
allHostWhiteTrie.insert("org.apache.http.**")
}
}
@Throws(ClassNotFoundException::class)
override fun loadClass(className: String, resolve: Boolean): Class<*> {
var clazz: Class<*>? = findLoadedClass(className)
if (clazz == null) {
//specialClassLoader 为null 表示该classLoader依赖了其他的插件classLoader,需要遵循双亲委派
if (specialClassLoader == null) {
return super.loadClass(className, resolve)
}
//插件依赖跟loader一起打包的runtime类,如ShadowActivity,从loader的ClassLoader加载
if (className.subStringBeforeDot() == "com.tencent.shadow.core.runtime") {
return loaderClassLoader.loadClass(className)
}
//包名在白名单中的类按双亲委派逻辑,从宿主中加载
if (className.inPackage(allHostWhiteTrie)) {
return super.loadClass(className, resolve)
}
var suppressed: ClassNotFoundException? = null
try {
//正常的ClassLoader这里是parent.loadClass,插件用specialClassLoader以跳过parent
clazz = specialClassLoader.loadClass(className)!!
} catch (e: ClassNotFoundException) {
suppressed = e
}
if (clazz == null) {
try {
clazz = findClass(className)!!
} catch (e: ClassNotFoundException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
e.addSuppressed(suppressed)
}
throw e
}
}
}
return clazz
}
internal fun loadPluginManifest(): PluginManifest {
try {
val clazz = findClass("com.tencent.shadow.core.manifest_parser.PluginManifest")
return PluginManifest::class.java.cast(clazz.newInstance())
} catch (e: ClassNotFoundException) {
throw Error(
"请注意每个插件apk构建时都需要" +
"apply plugin: 'com.tencent.shadow.plugin'", e
)
}
}
}
private fun String.subStringBeforeDot() = substringBeforeLast('.', "")
@Deprecated("use PackageNameTrie instead.")
@TestOnly
internal fun String.inPackage(packageNames: Array): Boolean {
val trie = PackageNameTrie()
packageNames.forEach {
trie.insert(it)
}
return inPackage(trie)
}
private fun String.inPackage(packageNames: PackageNameTrie): Boolean {
val packageName = subStringBeforeDot()
return packageNames.isMatch(packageName)
}
/**
* 基于Trie算法对包名进行前缀匹配
*/
private class PackageNameTrie {
private class Node {
val subNodes = mutableMapOf()
var isLastPackageOfARule = false
}
private val root = Node()
fun insert(packageNameRule: String) {
var node = root
packageNameRule.split('.').forEach {
if (it.isEmpty()) return //"",".*",".**"这种无包名情况不允许设置
var subNode = node.subNodes[it]
if (subNode == null) {
subNode = Node()
node.subNodes[it] = subNode
}
node = subNode
}
node.isLastPackageOfARule = true
}
fun isMatch(packageName: String): Boolean {
var node = root
val split = packageName.split('.')
val lastIndex = split.size - 1
for ((index, name) in split.withIndex()) {
// 只要下级包名规则中有**,就完成了匹配
val twoStars = node.subNodes["**"]
if (twoStars != null) {
return true
}
// 剩最后一级包名时,如果规则是*则完成比配
if (index == lastIndex) {
val oneStar = node.subNodes["*"]
if (oneStar != null) {
return true
}
}
// 找不到下级包名时即匹配失败
val subNode = node.subNodes[name]
if (subNode == null) {
return false
} else {
node = subNode
}
}
return node.isLastPackageOfARule
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/delegates/DI.kt
================================================
/*
* 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.delegates
interface DI {
fun inject(delegate: ShadowDelegate, partKey: String)
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/delegates/PackageManagerWrapper.java
================================================
package com.tencent.shadow.core.loader.delegates;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ChangedPackages;
import android.content.pm.FeatureInfo;
import android.content.pm.InstallSourceInfo;
import android.content.pm.InstrumentationInfo;
import android.content.pm.ModuleInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.SharedLibraryInfo;
import android.content.pm.VersionedPackage;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
import java.util.List;
import java.util.Set;
@SuppressLint("NewApi")
class PackageManagerWrapper extends PackageManager {
final private PackageManager proxy;
PackageManagerWrapper(PackageManager proxy) {
this.proxy = proxy;
}
@Override
public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException {
return proxy.getPackageInfo(packageName, flags);
}
@Override
public PackageInfo getPackageInfo(VersionedPackage versionedPackage, int flags) throws NameNotFoundException {
return proxy.getPackageInfo(versionedPackage, flags);
}
@Override
public String[] currentToCanonicalPackageNames(String[] packageNames) {
return proxy.currentToCanonicalPackageNames(packageNames);
}
@Override
public String[] canonicalToCurrentPackageNames(String[] packageNames) {
return proxy.canonicalToCurrentPackageNames(packageNames);
}
@Override
public Intent getLaunchIntentForPackage(String packageName) {
return proxy.getLaunchIntentForPackage(packageName);
}
@Override
public Intent getLeanbackLaunchIntentForPackage(String packageName) {
return proxy.getLeanbackLaunchIntentForPackage(packageName);
}
@Override
public int[] getPackageGids(String packageName) throws NameNotFoundException {
return proxy.getPackageGids(packageName);
}
@Override
public int[] getPackageGids(String packageName, int flags) throws NameNotFoundException {
return proxy.getPackageGids(packageName, flags);
}
@Override
public int getPackageUid(String packageName, int flags) throws NameNotFoundException {
return proxy.getPackageUid(packageName, flags);
}
@Override
public PermissionInfo getPermissionInfo(String permName, int flags) throws NameNotFoundException {
return proxy.getPermissionInfo(permName, flags);
}
@Override
public List queryPermissionsByGroup(String permissionGroup, int flags) throws NameNotFoundException {
return proxy.queryPermissionsByGroup(permissionGroup, flags);
}
@Override
public PermissionGroupInfo getPermissionGroupInfo(String permName, int flags) throws NameNotFoundException {
return proxy.getPermissionGroupInfo(permName, flags);
}
@Override
public List getAllPermissionGroups(int flags) {
return proxy.getAllPermissionGroups(flags);
}
@Override
public ApplicationInfo getApplicationInfo(String packageName, int flags) throws NameNotFoundException {
return proxy.getApplicationInfo(packageName, flags);
}
@Override
public ActivityInfo getActivityInfo(ComponentName component, int flags) throws NameNotFoundException {
return proxy.getActivityInfo(component, flags);
}
@Override
public ActivityInfo getReceiverInfo(ComponentName component, int flags) throws NameNotFoundException {
return proxy.getReceiverInfo(component, flags);
}
@Override
public ServiceInfo getServiceInfo(ComponentName component, int flags) throws NameNotFoundException {
return proxy.getServiceInfo(component, flags);
}
@Override
public ProviderInfo getProviderInfo(ComponentName component, int flags) throws NameNotFoundException {
return proxy.getProviderInfo(component, flags);
}
@Override
public ModuleInfo getModuleInfo(String packageName, int flags) throws NameNotFoundException {
return proxy.getModuleInfo(packageName, flags);
}
@Override
public List getInstalledModules(int flags) {
return proxy.getInstalledModules(flags);
}
@Override
public List getInstalledPackages(int flags) {
return proxy.getInstalledPackages(flags);
}
@Override
public List getPackagesHoldingPermissions(String[] permissions, int flags) {
return proxy.getPackagesHoldingPermissions(permissions, flags);
}
@Override
public int checkPermission(String permName, String packageName) {
return proxy.checkPermission(permName, packageName);
}
@Override
public boolean isPermissionRevokedByPolicy(String permName, String packageName) {
return proxy.isPermissionRevokedByPolicy(permName, packageName);
}
@Override
public boolean addPermission(PermissionInfo info) {
return proxy.addPermission(info);
}
@Override
public boolean addPermissionAsync(PermissionInfo info) {
return proxy.addPermissionAsync(info);
}
@Override
public void removePermission(String permName) {
proxy.removePermission(permName);
}
@Override
public Set getWhitelistedRestrictedPermissions(String packageName, int whitelistFlag) {
return proxy.getWhitelistedRestrictedPermissions(packageName, whitelistFlag);
}
@Override
public boolean addWhitelistedRestrictedPermission(String packageName, String permName, int whitelistFlags) {
return proxy.addWhitelistedRestrictedPermission(packageName, permName, whitelistFlags);
}
@Override
public boolean removeWhitelistedRestrictedPermission(String packageName, String permName, int whitelistFlags) {
return proxy.removeWhitelistedRestrictedPermission(packageName, permName, whitelistFlags);
}
@Override
public boolean setAutoRevokeWhitelisted(String packageName, boolean whitelisted) {
return proxy.setAutoRevokeWhitelisted(packageName, whitelisted);
}
@Override
public boolean isAutoRevokeWhitelisted(String packageName) {
return proxy.isAutoRevokeWhitelisted(packageName);
}
@Override
public CharSequence getBackgroundPermissionOptionLabel() {
return proxy.getBackgroundPermissionOptionLabel();
}
@Override
public int checkSignatures(String packageName1, String packageName2) {
return proxy.checkSignatures(packageName1, packageName2);
}
@Override
public int checkSignatures(int uid1, int uid2) {
return proxy.checkSignatures(uid1, uid2);
}
@Override
public String[] getPackagesForUid(int uid) {
return proxy.getPackagesForUid(uid);
}
@Override
public String getNameForUid(int uid) {
return proxy.getNameForUid(uid);
}
@Override
public List getInstalledApplications(int flags) {
return proxy.getInstalledApplications(flags);
}
@Override
public boolean isInstantApp() {
return proxy.isInstantApp();
}
@Override
public boolean isInstantApp(String packageName) {
return proxy.isInstantApp(packageName);
}
@Override
public int getInstantAppCookieMaxBytes() {
return proxy.getInstantAppCookieMaxBytes();
}
@Override
public byte[] getInstantAppCookie() {
return proxy.getInstantAppCookie();
}
@Override
public void clearInstantAppCookie() {
proxy.clearInstantAppCookie();
}
@Override
public void updateInstantAppCookie(byte[] cookie) {
proxy.updateInstantAppCookie(cookie);
}
@Override
public String[] getSystemSharedLibraryNames() {
return proxy.getSystemSharedLibraryNames();
}
@Override
public List getSharedLibraries(int flags) {
return proxy.getSharedLibraries(flags);
}
@Override
public ChangedPackages getChangedPackages(int sequenceNumber) {
return proxy.getChangedPackages(sequenceNumber);
}
@Override
public FeatureInfo[] getSystemAvailableFeatures() {
return proxy.getSystemAvailableFeatures();
}
@Override
public boolean hasSystemFeature(String featureName) {
return proxy.hasSystemFeature(featureName);
}
@Override
public boolean hasSystemFeature(String featureName, int version) {
return proxy.hasSystemFeature(featureName, version);
}
@Override
public ResolveInfo resolveActivity(Intent intent, int flags) {
return proxy.resolveActivity(intent, flags);
}
@Override
public List queryIntentActivities(Intent intent, int flags) {
return proxy.queryIntentActivities(intent, flags);
}
@Override
public List queryIntentActivityOptions(ComponentName caller, Intent[] specifics, Intent intent, int flags) {
return proxy.queryIntentActivityOptions(caller, specifics, intent, flags);
}
@Override
public List queryBroadcastReceivers(Intent intent, int flags) {
return proxy.queryBroadcastReceivers(intent, flags);
}
@Override
public ResolveInfo resolveService(Intent intent, int flags) {
return proxy.resolveService(intent, flags);
}
@Override
public List queryIntentServices(Intent intent, int flags) {
return proxy.queryIntentServices(intent, flags);
}
@Override
public List queryIntentContentProviders(Intent intent, int flags) {
return proxy.queryIntentContentProviders(intent, flags);
}
@Override
public ProviderInfo resolveContentProvider(String authority, int flags) {
return proxy.resolveContentProvider(authority, flags);
}
@Override
public List queryContentProviders(String processName, int uid, int flags) {
return proxy.queryContentProviders(processName, uid, flags);
}
@Override
public InstrumentationInfo getInstrumentationInfo(ComponentName className, int flags) throws NameNotFoundException {
return proxy.getInstrumentationInfo(className, flags);
}
@Override
public List queryInstrumentation(String targetPackage, int flags) {
return proxy.queryInstrumentation(targetPackage, flags);
}
@Override
public Drawable getDrawable(String packageName, int resid, ApplicationInfo appInfo) {
return proxy.getDrawable(packageName, resid, appInfo);
}
@Override
public Drawable getActivityIcon(ComponentName activityName) throws NameNotFoundException {
return proxy.getActivityIcon(activityName);
}
@Override
public Drawable getActivityIcon(Intent intent) throws NameNotFoundException {
return proxy.getActivityIcon(intent);
}
@Override
public Drawable getActivityBanner(ComponentName activityName) throws NameNotFoundException {
return proxy.getActivityBanner(activityName);
}
@Override
public Drawable getActivityBanner(Intent intent) throws NameNotFoundException {
return proxy.getActivityBanner(intent);
}
@Override
public Drawable getDefaultActivityIcon() {
return proxy.getDefaultActivityIcon();
}
@Override
public Drawable getApplicationIcon(ApplicationInfo info) {
return proxy.getApplicationIcon(info);
}
@Override
public Drawable getApplicationIcon(String packageName) throws NameNotFoundException {
return proxy.getApplicationIcon(packageName);
}
@Override
public Drawable getApplicationBanner(ApplicationInfo info) {
return proxy.getApplicationBanner(info);
}
@Override
public Drawable getApplicationBanner(String packageName) throws NameNotFoundException {
return proxy.getApplicationBanner(packageName);
}
@Override
public Drawable getActivityLogo(ComponentName activityName) throws NameNotFoundException {
return proxy.getActivityLogo(activityName);
}
@Override
public Drawable getActivityLogo(Intent intent) throws NameNotFoundException {
return proxy.getActivityLogo(intent);
}
@Override
public Drawable getApplicationLogo(ApplicationInfo info) {
return proxy.getApplicationLogo(info);
}
@Override
public Drawable getApplicationLogo(String packageName) throws NameNotFoundException {
return proxy.getApplicationLogo(packageName);
}
@Override
public Drawable getUserBadgedIcon(Drawable drawable, UserHandle user) {
return proxy.getUserBadgedIcon(drawable, user);
}
@Override
public Drawable getUserBadgedDrawableForDensity(Drawable drawable, UserHandle user, Rect badgeLocation, int badgeDensity) {
return proxy.getUserBadgedDrawableForDensity(drawable, user, badgeLocation, badgeDensity);
}
@Override
public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
return proxy.getUserBadgedLabel(label, user);
}
@Override
public CharSequence getText(String packageName, int resid, ApplicationInfo appInfo) {
return proxy.getText(packageName, resid, appInfo);
}
@Override
public XmlResourceParser getXml(String packageName, int resid, ApplicationInfo appInfo) {
return proxy.getXml(packageName, resid, appInfo);
}
@Override
public CharSequence getApplicationLabel(ApplicationInfo info) {
return proxy.getApplicationLabel(info);
}
@Override
public Resources getResourcesForActivity(ComponentName activityName) throws NameNotFoundException {
return proxy.getResourcesForActivity(activityName);
}
@Override
public Resources getResourcesForApplication(ApplicationInfo app) throws NameNotFoundException {
return proxy.getResourcesForApplication(app);
}
@Override
public Resources getResourcesForApplication(String packageName) throws NameNotFoundException {
return proxy.getResourcesForApplication(packageName);
}
@Override
public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) {
return proxy.getPackageArchiveInfo(archiveFilePath, flags);
}
@Override
public void verifyPendingInstall(int id, int verificationCode) {
proxy.verifyPendingInstall(id, verificationCode);
}
@Override
public void extendVerificationTimeout(int id, int verificationCodeAtTimeout, long millisecondsToDelay) {
proxy.extendVerificationTimeout(id, verificationCodeAtTimeout, millisecondsToDelay);
}
@Override
public void setInstallerPackageName(String targetPackage, String installerPackageName) {
proxy.setInstallerPackageName(targetPackage, installerPackageName);
}
@Override
@Deprecated
public String getInstallerPackageName(String packageName) {
return proxy.getInstallerPackageName(packageName);
}
@Override
public InstallSourceInfo getInstallSourceInfo(String packageName) throws NameNotFoundException {
return proxy.getInstallSourceInfo(packageName);
}
@Override
@Deprecated
public void addPackageToPreferred(String packageName) {
proxy.addPackageToPreferred(packageName);
}
@Override
@Deprecated
public void removePackageFromPreferred(String packageName) {
proxy.removePackageFromPreferred(packageName);
}
@Override
@Deprecated
public List getPreferredPackages(int flags) {
return proxy.getPreferredPackages(flags);
}
@Override
@Deprecated
public void addPreferredActivity(IntentFilter filter, int match, ComponentName[] set, ComponentName activity) {
proxy.addPreferredActivity(filter, match, set, activity);
}
@Override
@Deprecated
public void clearPackagePreferredActivities(String packageName) {
proxy.clearPackagePreferredActivities(packageName);
}
@Override
@Deprecated
public int getPreferredActivities(List outFilters, List outActivities, String packageName) {
return proxy.getPreferredActivities(outFilters, outActivities, packageName);
}
@Override
public void setComponentEnabledSetting(ComponentName componentName, int newState, int flags) {
proxy.setComponentEnabledSetting(componentName, newState, flags);
}
@Override
public int getComponentEnabledSetting(ComponentName componentName) {
return proxy.getComponentEnabledSetting(componentName);
}
@Override
public boolean getSyntheticAppDetailsActivityEnabled(String packageName) {
return proxy.getSyntheticAppDetailsActivityEnabled(packageName);
}
@Override
public void setApplicationEnabledSetting(String packageName, int newState, int flags) {
proxy.setApplicationEnabledSetting(packageName, newState, flags);
}
@Override
public int getApplicationEnabledSetting(String packageName) {
return proxy.getApplicationEnabledSetting(packageName);
}
@Override
public boolean isSafeMode() {
return proxy.isSafeMode();
}
@Override
public boolean isPackageSuspended(String packageName) throws NameNotFoundException {
return proxy.isPackageSuspended(packageName);
}
@Override
public boolean isPackageSuspended() {
return proxy.isPackageSuspended();
}
@Override
public Bundle getSuspendedPackageAppExtras() {
return proxy.getSuspendedPackageAppExtras();
}
@Override
public void setApplicationCategoryHint(String packageName, int categoryHint) {
proxy.setApplicationCategoryHint(packageName, categoryHint);
}
@Override
public boolean isDeviceUpgrading() {
return proxy.isDeviceUpgrading();
}
@Override
public PackageInstaller getPackageInstaller() {
return proxy.getPackageInstaller();
}
@Override
public boolean canRequestPackageInstalls() {
return proxy.canRequestPackageInstalls();
}
@Override
public boolean hasSigningCertificate(String packageName, byte[] certificate, int type) {
return proxy.hasSigningCertificate(packageName, certificate, type);
}
@Override
public boolean hasSigningCertificate(int uid, byte[] certificate, int type) {
return proxy.hasSigningCertificate(uid, certificate, type);
}
@Override
public boolean isAutoRevokeWhitelisted() {
return proxy.isAutoRevokeWhitelisted();
}
@Override
public boolean isDefaultApplicationIcon(Drawable drawable) {
return proxy.isDefaultApplicationIcon(drawable);
}
@Override
public void setMimeGroup(String mimeGroup, Set mimeTypes) {
proxy.setMimeGroup(mimeGroup, mimeTypes);
}
@Override
public Set getMimeGroup(String mimeGroup) {
return proxy.getMimeGroup(mimeGroup);
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/delegates/ShadowActivityDelegate.kt
================================================
/*
* 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.delegates
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.WindowManager
import com.tencent.shadow.coding.java_build_config.BuildConfig
import com.tencent.shadow.core.common.LoggerFactory
import com.tencent.shadow.core.loader.managers.ComponentManager.Companion.CM_ACTIVITY_INFO_KEY
import com.tencent.shadow.core.loader.managers.ComponentManager.Companion.CM_BUSINESS_NAME_KEY
import com.tencent.shadow.core.loader.managers.ComponentManager.Companion.CM_CALLING_ACTIVITY_KEY
import com.tencent.shadow.core.loader.managers.ComponentManager.Companion.CM_CLASS_NAME_KEY
import com.tencent.shadow.core.loader.managers.ComponentManager.Companion.CM_EXTRAS_BUNDLE_KEY
import com.tencent.shadow.core.loader.managers.ComponentManager.Companion.CM_LOADER_BUNDLE_KEY
import com.tencent.shadow.core.loader.managers.ComponentManager.Companion.CM_PART_KEY
import com.tencent.shadow.core.runtime.PluginActivity
import com.tencent.shadow.core.runtime.PluginManifest
import com.tencent.shadow.core.runtime.ShadowActivity
import com.tencent.shadow.core.runtime.container.HostActivityDelegate
import com.tencent.shadow.core.runtime.container.HostActivityDelegator
/**
* 壳子Activity与插件Activity转调关系的实现类
* 它是抽象的是因为它缺少必要的业务信息.业务必须继承这个类提供业务信息.
*
* @author cubershi
*/
open class ShadowActivityDelegate(private val mDI: DI) : GeneratedShadowActivityDelegate(),
HostActivityDelegate {
companion object {
const val PLUGIN_OUT_STATE_KEY = "PLUGIN_OUT_STATE_KEY"
val mLogger = LoggerFactory.getLogger(ShadowActivityDelegate::class.java)
}
protected lateinit var mHostActivityDelegator: HostActivityDelegator
private val mPluginActivity get() = super.pluginActivity
private lateinit var mBusinessName: String
private lateinit var mPartKey: String
private lateinit var mBundleForPluginLoader: Bundle
private var mRawIntentExtraBundle: Bundle? = null
private var mPluginActivityCreated = false
private var mDependenciesInjected = false
private var mRecreateCalled = false
/**
* 判断是否调用过OnWindowAttributesChanged,如果调用过就说明需要在onCreate之前调用
*/
private var mCallOnWindowAttributesChanged = false
private var mBeforeOnCreateOnWindowAttributesChangedCalledParams: WindowManager.LayoutParams? =
null
override fun setDelegator(hostActivityDelegator: HostActivityDelegator) {
mHostActivityDelegator = hostActivityDelegator
}
override fun getPluginActivity(): Any = mPluginActivity
private lateinit var mCurrentConfiguration: Configuration
private var mPluginHandleConfigurationChange: Int = 0
private var mCallingActivity: ComponentName? = null
protected lateinit var mPluginActivityInfo: PluginManifest.ActivityInfo
override fun onCreate(savedInstanceState: Bundle?) {
val pluginInitBundle = savedInstanceState ?: mHostActivityDelegator.intent.extras!!
mCallingActivity = pluginInitBundle.getParcelable(CM_CALLING_ACTIVITY_KEY)
mBusinessName = pluginInitBundle.getString(CM_BUSINESS_NAME_KEY, "")
val partKey = pluginInitBundle.getString(CM_PART_KEY)!!
mPartKey = partKey
mDI.inject(this, partKey)
mDependenciesInjected = true
val bundleForPluginLoader = pluginInitBundle.getBundle(CM_LOADER_BUNDLE_KEY)!!
mBundleForPluginLoader = bundleForPluginLoader
bundleForPluginLoader.classLoader = this.javaClass.classLoader
val pluginActivityClassName = bundleForPluginLoader.getString(CM_CLASS_NAME_KEY)!!
val pluginActivityInfo: PluginManifest.ActivityInfo =
bundleForPluginLoader.getParcelable(CM_ACTIVITY_INFO_KEY)!!
mPluginActivityInfo = pluginActivityInfo
mCurrentConfiguration = Configuration(resources.configuration)
mPluginHandleConfigurationChange =
(pluginActivityInfo.configChanges
or ActivityInfo.CONFIG_SCREEN_SIZE//系统本身就会单独对待这个属性,不声明也不会重启Activity。
or ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE//系统本身就会单独对待这个属性,不声明也不会重启Activity。
or 0x20000000 //见ActivityInfo.CONFIG_WINDOW_CONFIGURATION 系统处理属性
)
if (savedInstanceState == null) {
mRawIntentExtraBundle = pluginInitBundle.getBundle(CM_EXTRAS_BUNDLE_KEY)
mHostActivityDelegator.intent.replaceExtras(mRawIntentExtraBundle)
}
mHostActivityDelegator.intent.setExtrasClassLoader(mPluginClassLoader)
try {
val pluginActivity = mAppComponentFactory.instantiateActivity(
mPluginClassLoader,
pluginActivityClassName,
mHostActivityDelegator.intent
)
initPluginActivity(pluginActivity, pluginActivityInfo)
super.pluginActivity = pluginActivity
if (mLogger.isDebugEnabled) {
mLogger.debug(
"{} mPluginHandleConfigurationChange=={}",
mPluginActivity.javaClass.canonicalName,
mPluginHandleConfigurationChange
)
}
//使PluginActivity替代ContainerActivity接收Window的Callback
mHostActivityDelegator.window.callback = pluginActivity
//设置插件AndroidManifest.xml 中注册的WindowSoftInputMode
mHostActivityDelegator.window.setSoftInputMode(pluginActivityInfo.softInputMode)
//Activity.onCreate调用之前应该先收到onWindowAttributesChanged。
if (mCallOnWindowAttributesChanged) {
pluginActivity.onWindowAttributesChanged(
mBeforeOnCreateOnWindowAttributesChangedCalledParams
)
mBeforeOnCreateOnWindowAttributesChangedCalledParams = null
}
val pluginSavedInstanceState: Bundle? =
savedInstanceState?.getBundle(PLUGIN_OUT_STATE_KEY)
pluginSavedInstanceState?.classLoader = mPluginClassLoader
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
notifyPluginActivityPreCreated(pluginActivity, pluginSavedInstanceState)
}
pluginActivity.onCreate(pluginSavedInstanceState)
mPluginActivityCreated = true
} catch (e: Exception) {
throw RuntimeException(e)
}
}
private fun initPluginActivity(
pluginActivity: PluginActivity,
pluginActivityInfo: PluginManifest.ActivityInfo
) {
pluginActivity.setHostActivityDelegator(mHostActivityDelegator)
pluginActivity.setPluginResources(mPluginResources)
pluginActivity.setPluginClassLoader(mPluginClassLoader)
pluginActivity.setPluginComponentLauncher(mComponentManager)
pluginActivity.setPluginApplication(mPluginApplication)
pluginActivity.setShadowApplication(mPluginApplication)
pluginActivity.applicationInfo = mPluginApplication.applicationInfo
pluginActivity.setBusinessName(mBusinessName)
pluginActivity.callingActivity = mCallingActivity
pluginActivity.setPluginPartKey(mPartKey)
//前面的所有set方法都是PluginActivity定义的方法,
//业务的Activity子类不会覆盖这些方法。调用它们不会执行业务Activity的任何逻辑。
//最后这个setHostContextAsBase会调用插件Activity的attachBaseContext方法,
//有可能会执行业务Activity覆盖的逻辑。
//所以,这个调用要放在最后。
pluginActivity.setHostContextAsBase(mHostActivityDelegator.hostActivity as Context)
val activityTheme = if (pluginActivityInfo.theme != 0) {
pluginActivityInfo.theme
} else {
pluginActivity.applicationInfo.theme
}
pluginActivity.setTheme(activityTheme)
val screenOrientation = pluginActivityInfo.screenOrientation
if (screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED && screenOrientation != pluginActivity.requestedOrientation) {
pluginActivity.requestedOrientation = screenOrientation
}
}
override fun getLoaderVersion() = BuildConfig.VERSION_NAME
override fun onNewIntent(intent: Intent) {
val pluginExtras: Bundle? = intent.getBundleExtra(CM_EXTRAS_BUNDLE_KEY)
intent.replaceExtras(pluginExtras)
mPluginActivity.onNewIntent(intent)
}
override fun onNavigateUpFromChild(arg0: Activity?): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onSaveInstanceState(outState: Bundle) {
val pluginOutState = Bundle(mPluginClassLoader)
mPluginActivity.onSaveInstanceState(pluginOutState)
outState.putBundle(PLUGIN_OUT_STATE_KEY, pluginOutState)
outState.putString(CM_PART_KEY, mPartKey)
outState.putBundle(CM_LOADER_BUNDLE_KEY, mBundleForPluginLoader)
if (mRecreateCalled) {
outState.putBundle(CM_EXTRAS_BUNDLE_KEY, mHostActivityDelegator.intent.extras)
} else {
outState.putBundle(CM_EXTRAS_BUNDLE_KEY, mRawIntentExtraBundle)
}
}
override fun onChildTitleChanged(arg0: Activity?, arg1: CharSequence?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onConfigurationChanged(newConfig: Configuration) {
val diff = newConfig.diff(mCurrentConfiguration)
if (mLogger.isDebugEnabled) {
mLogger.debug(
"{} onConfigurationChanged diff=={}",
mPluginActivity.javaClass.canonicalName,
diff
)
}
if (diff == (diff and mPluginHandleConfigurationChange)) {
mPluginActivity.onConfigurationChanged(newConfig)
mCurrentConfiguration = Configuration(newConfig)
} else {
mHostActivityDelegator.superOnConfigurationChanged(newConfig)
mHostActivityDelegator.recreate()
}
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
val pluginSavedInstanceState: Bundle? = savedInstanceState?.getBundle(PLUGIN_OUT_STATE_KEY)
mPluginActivity.onRestoreInstanceState(pluginSavedInstanceState)
}
override fun onPostCreate(savedInstanceState: Bundle?) {
val pluginSavedInstanceState: Bundle? = savedInstanceState?.getBundle(PLUGIN_OUT_STATE_KEY)
mPluginActivity.onPostCreate(pluginSavedInstanceState)
}
override fun onWindowAttributesChanged(params: WindowManager.LayoutParams) {
if (mPluginActivityCreated) {
mPluginActivity.onWindowAttributesChanged(params)
} else {
mBeforeOnCreateOnWindowAttributesChangedCalledParams = params
}
mCallOnWindowAttributesChanged = true
}
override fun onApplyThemeResource(theme: Resources.Theme, resid: Int, first: Boolean) {
mHostActivityDelegator.superOnApplyThemeResource(theme, resid, first)
if (mPluginActivityCreated) {
mPluginActivity.onApplyThemeResource(theme, resid, first)
}
}
override fun getClassLoader(): ClassLoader {
return mPluginClassLoader
}
override fun getLayoutInflater(): LayoutInflater = LayoutInflater.from(mPluginActivity)
override fun getResources(): Resources {
if (mDependenciesInjected) {
return mPluginResources;
} else {
//预期只有android.view.Window.getDefaultFeatures会调用到这个分支,此时我们还无法确定插件资源
//而getDefaultFeatures只需要访问系统资源
return Resources.getSystem()
}
}
override fun recreate() {
mRecreateCalled = true
mHostActivityDelegator.superRecreate()
}
private fun notifyPluginActivityPreCreated(
pluginActivity: ShadowActivity,
pluginSavedInstanceState: Bundle?
) {
mPluginApplication.mActivityLifecycleCallbacksHolder.notifyPluginActivityPreCreated(
pluginActivity,
pluginSavedInstanceState
)
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/delegates/ShadowContentProviderDelegate.kt
================================================
/*
* 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.delegates
import android.annotation.TargetApi
import android.content.ContentValues
import android.content.res.Configuration
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import com.tencent.shadow.core.loader.managers.PluginContentProviderManager
import com.tencent.shadow.core.runtime.container.HostContentProviderDelegate
class ShadowContentProviderDelegate(private val mProviderManager: PluginContentProviderManager) :
ShadowDelegate(), HostContentProviderDelegate {
override fun onConfigurationChanged(newConfig: Configuration) {
mProviderManager.getAllContentProvider().forEach {
it.onConfigurationChanged(newConfig)
}
}
override fun onLowMemory() {
mProviderManager.getAllContentProvider().forEach {
it.onLowMemory()
}
}
override fun onTrimMemory(level: Int) {
mProviderManager.getAllContentProvider().forEach {
it.onTrimMemory(level)
}
}
override fun onCreate(): Boolean {
return true
}
override fun query(
uri: Uri,
projection: Array?,
selection: String?,
selectionArgs: Array?,
sortOrder: String?
): Cursor? {
val pluginUri = mProviderManager.convert2PluginUri(uri)
return mProviderManager.getPluginContentProvider(pluginUri.authority!!)!!
.query(pluginUri, projection, selection, selectionArgs, sortOrder)
}
override fun getType(uri: Uri): String? {
val pluginUri = mProviderManager.convert2PluginUri(uri)
return mProviderManager.getPluginContentProvider(pluginUri.authority!!)!!.getType(pluginUri)
}
override fun insert(uri: Uri, values: ContentValues): Uri? {
val pluginUri = mProviderManager.convert2PluginUri(uri)
return mProviderManager.getPluginContentProvider(pluginUri.authority!!)!!
.insert(pluginUri, values)
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
val pluginUri = mProviderManager.convert2PluginUri(uri)
return mProviderManager.getPluginContentProvider(pluginUri.authority!!)!!
.delete(pluginUri, selection, selectionArgs)
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array?
): Int {
val pluginUri = mProviderManager.convert2PluginUri(uri)
return mProviderManager.getPluginContentProvider(pluginUri.authority!!)!!
.update(pluginUri, values, selection, selectionArgs)
}
override fun bulkInsert(uri: Uri, values: Array): Int {
val pluginUri = mProviderManager.convert2PluginUri(uri)
return mProviderManager.getPluginContentProvider(pluginUri.authority!!)!!
.bulkInsert(pluginUri, values)
}
override fun call(method: String, arg: String?, extras: Bundle): Bundle? {
val pluginUri = mProviderManager.convert2PluginUri(extras)
return mProviderManager.getPluginContentProvider(pluginAuthority = pluginUri.authority!!)!!
.call(method, arg, extras)
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
val pluginUri = mProviderManager.convert2PluginUri(uri)
return mProviderManager.getPluginContentProvider(pluginUri.authority!!)!!
.openFile(pluginUri, mode)
}
@TargetApi(Build.VERSION_CODES.KITKAT)
override fun openFile(
uri: Uri,
mode: String,
signal: CancellationSignal?
): ParcelFileDescriptor? {
val pluginUri = mProviderManager.convert2PluginUri(uri)
return mProviderManager.getPluginContentProvider(pluginUri.authority!!)!!
.openFile(pluginUri, mode, signal)
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/delegates/ShadowDelegate.kt
================================================
/*
* 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.delegates
import android.content.res.Resources
import com.tencent.shadow.core.loader.classloaders.PluginClassLoader
import com.tencent.shadow.core.loader.managers.ComponentManager
import com.tencent.shadow.core.runtime.ShadowAppComponentFactory
import com.tencent.shadow.core.runtime.ShadowApplication
abstract class ShadowDelegate() {
fun inject(shadowApplication: ShadowApplication) {
_pluginApplication = shadowApplication
}
fun inject(appComponentFactory: ShadowAppComponentFactory) {
_appComponentFactory = appComponentFactory
}
fun inject(pluginClassLoader: PluginClassLoader) {
_pluginClassLoader = pluginClassLoader
}
fun inject(resources: Resources) {
_pluginResources = resources
}
fun inject(componentManager: ComponentManager) {
_componentManager = componentManager
}
private lateinit var _appComponentFactory: ShadowAppComponentFactory
private lateinit var _pluginApplication: ShadowApplication
private lateinit var _pluginClassLoader: PluginClassLoader
private lateinit var _pluginResources: Resources
private lateinit var _componentManager: ComponentManager
protected val mAppComponentFactory: ShadowAppComponentFactory
get() = _appComponentFactory
protected val mPluginApplication: ShadowApplication
get() = _pluginApplication
protected val mPluginClassLoader: PluginClassLoader
get() = _pluginClassLoader
protected val mPluginResources: Resources
get() = _pluginResources
protected val mComponentManager: ComponentManager
get() = _componentManager
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/delegates/ShadowNativeActivityDelegate.kt
================================================
package com.tencent.shadow.core.loader.delegates
import android.content.ComponentName
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.view.InputQueue
import android.view.SurfaceHolder
import com.tencent.shadow.core.runtime.PackageManagerInvokeRedirect
import com.tencent.shadow.core.runtime.ShadowNativeActivity
import com.tencent.shadow.core.runtime.container.HostNativeActivityDelegate
class ShadowNativeActivityDelegate(mDI: DI) : ShadowActivityDelegate(mDI),
HostNativeActivityDelegate {
private val mPluginActivity: ShadowNativeActivity
get()
= super.pluginActivity as ShadowNativeActivity
override fun surfaceCreated(holder: SurfaceHolder) {
return mPluginActivity.surfaceCreated(holder)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
return mPluginActivity.surfaceChanged(holder, format, width, height)
}
override fun surfaceRedrawNeeded(holder: SurfaceHolder) {
return mPluginActivity.surfaceRedrawNeeded(holder)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
return mPluginActivity.surfaceDestroyed(holder)
}
override fun onInputQueueCreated(queue: InputQueue) {
return mPluginActivity.onInputQueueCreated(queue)
}
override fun onInputQueueDestroyed(queue: InputQueue) {
return mPluginActivity.onInputQueueCreated(queue)
}
override fun onGlobalLayout() {
return mPluginActivity.onGlobalLayout()
}
//预期只有NativeActivity会调用这个方法
override fun getPackageManager(): PackageManager {
val pluginPackageManager =
PackageManagerInvokeRedirect.getPluginPackageManager(mPluginActivity.classLoader)
return object : PackageManagerWrapper(mHostActivityDelegator.superGetPackageManager()) {
override fun getActivityInfo(component: ComponentName, flags: Int): ActivityInfo {
return pluginPackageManager.getActivityInfo(component, flags)
}
}
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/exceptions/CreateApplicationException.kt
================================================
/*
* 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.exceptions
class CreateApplicationException(cause: Throwable) : Exception(cause)
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/exceptions/LoadApkException.kt
================================================
/*
* 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.exceptions
class LoadApkException(message: String) : Exception(message)
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/exceptions/LoadPluginException.kt
================================================
/*
* 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.exceptions
import android.annotation.TargetApi
class LoadPluginException : Exception {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
@TargetApi(24)
constructor(
message: String?,
cause: Throwable?,
enableSuppression: Boolean,
writableStackTrace: Boolean
) : super(message, cause, enableSuppression, writableStackTrace)
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/exceptions/ParsePluginApkException.kt
================================================
/*
* 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.exceptions
/**
* 解析插件apk异常
*
* @author cubershi
*/
class ParsePluginApkException(message: String) : Exception(message)
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/infos/ContainerProviderInfo.kt
================================================
/*
* 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.infos
open class ContainerProviderInfo(var className: String, var authority: String)
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/infos/PluginParts.kt
================================================
/*
* 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.infos
import android.content.res.Resources
import com.tencent.shadow.core.loader.classloaders.PluginClassLoader
import com.tencent.shadow.core.runtime.PluginPackageManager
import com.tencent.shadow.core.runtime.ShadowAppComponentFactory
import com.tencent.shadow.core.runtime.ShadowApplication
class PluginParts(
val appComponentFactory: ShadowAppComponentFactory,
val application: ShadowApplication,
val classLoader: PluginClassLoader,
val resources: Resources,
val pluginPackageManager: PluginPackageManager
)
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/managers/ComponentManager.kt
================================================
/*
* 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.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.util.Pair
import com.tencent.shadow.coding.java_build_config.BuildConfig
import com.tencent.shadow.core.load_parameters.LoadParameters
import com.tencent.shadow.core.loader.infos.ContainerProviderInfo
import com.tencent.shadow.core.runtime.PluginManifest
import com.tencent.shadow.core.runtime.ShadowContext
import com.tencent.shadow.core.runtime.ShadowContext.PluginComponentLauncher
import com.tencent.shadow.core.runtime.container.DelegateProvider.LOADER_VERSION_KEY
import com.tencent.shadow.core.runtime.container.DelegateProvider.PROCESS_ID_KEY
import com.tencent.shadow.core.runtime.container.DelegateProviderHolder
import com.tencent.shadow.core.runtime.container.GeneratedHostActivityDelegator
/**
* 插件组件管理
* 主要功能是管理组件和宿主中注册的壳子之间的配对关系
*
* @author cubershi
*/
abstract class ComponentManager : PluginComponentLauncher {
companion object {
const val CM_LOADER_BUNDLE_KEY = "CM_LOADER_BUNDLE"
const val CM_EXTRAS_BUNDLE_KEY = "CM_EXTRAS_BUNDLE"
const val CM_ACTIVITY_INFO_KEY = "CM_ACTIVITY_INFO"
const val CM_CLASS_NAME_KEY = "CM_CLASS_NAME"
const val CM_CALLING_ACTIVITY_KEY = "CM_CALLING_ACTIVITY_KEY"
const val CM_PACKAGE_NAME_KEY = "CM_PACKAGE_NAME"
const val CM_BUSINESS_NAME_KEY = "CM_BUSINESS_NAME"
const val CM_PART_KEY = "CM_PART"
}
/**
* @param pluginActivity 插件Activity
* @return 容器Activity
*/
abstract fun onBindContainerActivity(pluginActivity: ComponentName): ComponentName
abstract fun onBindContainerContentProvider(pluginContentProvider: ComponentName): ContainerProviderInfo
override fun startActivity(
shadowContext: ShadowContext,
pluginIntent: Intent,
option: Bundle?
): Boolean {
return if (pluginIntent.isPluginComponent()) {
shadowContext.superStartActivity(pluginIntent.toActivityContainerIntent(), option)
true
} else {
false
}
}
override fun startActivityForResult(
delegator: GeneratedHostActivityDelegator,
pluginIntent: Intent,
requestCode: Int,
option: Bundle?,
callingActivity: ComponentName
): Boolean {
return if (pluginIntent.isPluginComponent()) {
val containerIntent = pluginIntent.toActivityContainerIntent()
containerIntent.putExtra(CM_CALLING_ACTIVITY_KEY, callingActivity)
delegator.startActivityForResult(containerIntent, requestCode, option)
true
} else {
false
}
}
override fun startService(
context: ShadowContext,
service: Intent
): Pair {
if (service.isPluginComponent()) {
// 插件service intent不需要转换成container service intent,直接使用intent
val component = mPluginServiceManager!!.startPluginService(service)
if (component != null) {
return Pair(true, component)
}
}
return Pair(false, service.component)
}
override fun stopService(context: ShadowContext, intent: Intent): Pair {
if (intent.isPluginComponent()) {
// 插件service intent不需要转换成container service intent,直接使用intent
val stopped = mPluginServiceManager!!.stopPluginService(intent)
return Pair(true, stopped)
}
return Pair(false, true)
}
override fun bindService(
context: ShadowContext,
intent: Intent,
conn: ServiceConnection,
flags: Int
): Pair {
return if (intent.isPluginComponent()) {
// 插件service intent不需要转换成container service intent,直接使用intent
mPluginServiceManager!!.bindPluginService(intent, conn, flags)
Pair(true, true)
} else {
Pair(false, false)
}
}
override fun unbindService(
context: ShadowContext,
conn: ServiceConnection
): Pair {
return Pair.create(
mPluginServiceManager!!.unbindPluginService(conn).first,
Unit
)
}
override fun convertPluginActivityIntent(pluginIntent: Intent): Intent {
return if (pluginIntent.isPluginComponent()) {
pluginIntent.toActivityContainerIntent()
} else {
pluginIntent
}
}
/**
* key:插件Activity类名
* value:插件PackageName
*/
private val packageNameMap: MutableMap = HashMap()
/**
* key:插件ComponentName
* value:壳子ComponentName
*/
private val componentMap: MutableMap = HashMap()
/**
* key:插件ComponentName
* value:LoadParameters
*/
private val loadParametersMap: MutableMap = hashMapOf()
/**
* key:插件ComponentName
* value:PluginManifest.ActivityInfo
*/
private val pluginActivityInfoMap: MutableMap =
hashMapOf()
/**
* 保存所有已加载插件对PluginManifest和apk文件路径对应关系
* 用于在同一个Loader加载对多个插件之间相互查找组件
*/
private val allLoadedPlugin: MutableList> = mutableListOf()
fun addPluginApkInfo(
pluginManifest: PluginManifest,
loadParameters: LoadParameters,
archiveFilePath: String
) {
fun common(componentInfo: PluginManifest.ComponentInfo, componentName: ComponentName) {
packageNameMap[componentInfo.className] = componentName.packageName
val previousValue = loadParametersMap.put(componentName, loadParameters)
if (previousValue != null) {
throw IllegalStateException("重复添加Component:$componentName")
}
}
val applicationPackageName = pluginManifest.applicationPackageName
pluginManifest.activities?.forEach {
val componentName = ComponentName(applicationPackageName, it.className)
common(it, componentName)
componentMap[componentName] = onBindContainerActivity(componentName)
pluginActivityInfoMap[componentName] = it
}
pluginManifest.services?.forEach {
val componentName = ComponentName(applicationPackageName, it.className)
common(it, componentName)
}
pluginManifest.providers?.forEach {
val componentName = ComponentName(applicationPackageName, it.className)
mPluginContentProviderManager!!.addContentProviderInfo(
loadParameters.partKey,
it,
onBindContainerContentProvider(componentName)
)
}
pluginManifest.receivers?.forEach {
val componentName = ComponentName(applicationPackageName, it.className)
common(it, componentName)
}
allLoadedPlugin.add(pluginManifest to archiveFilePath)
}
fun getComponentBusinessName(componentName: ComponentName): String? {
return loadParametersMap[componentName]?.businessName
}
fun getComponentPartKey(componentName: ComponentName): String? {
return loadParametersMap[componentName]?.partKey
}
private var mPluginServiceManager: PluginServiceManager? = null
fun setPluginServiceManager(pluginServiceManager: PluginServiceManager) {
mPluginServiceManager = pluginServiceManager
}
private var mPluginContentProviderManager: PluginContentProviderManager? = null
fun setPluginContentProviderManager(pluginContentProviderManager: PluginContentProviderManager) {
mPluginContentProviderManager = pluginContentProviderManager
}
private fun Intent.isPluginComponent(): Boolean {
val component = component ?: return false
val className = component.className
return packageNameMap.containsKey(className)
}
/**
* 调用前必须先调用isPluginComponent判断Intent确实一个插件内的组件
*/
private fun Intent.toActivityContainerIntent(): Intent {
val bundleForPluginLoader = Bundle()
val pluginActivityInfo = pluginActivityInfoMap[component]!!
bundleForPluginLoader.putParcelable(CM_ACTIVITY_INFO_KEY, pluginActivityInfo)
return toContainerIntent(bundleForPluginLoader)
}
/**
* 构造pluginIntent对应的ContainerIntent
* 调用前必须先调用isPluginComponent判断Intent确实一个插件内的组件
*/
private fun Intent.toContainerIntent(bundleForPluginLoader: Bundle): Intent {
val component = this.component
?: throw IllegalArgumentException("Activity Intent必须指定ComponentName")
val className = component.className
val packageName = packageNameMap[className]
?: throw IllegalArgumentException("已加载的插件中找不到${className}对应的packageName")
this.component = ComponentName(packageName, className)
val loadParameters = loadParametersMap[component]
?: throw IllegalArgumentException("已加载的插件中找不到${component}对应的LoadParameters")
val businessName = loadParameters.businessName
val partKey = loadParameters.partKey
val pluginExtras: Bundle? = extras
replaceExtras(null as Bundle?)
val containerComponent = componentMap[component]
?: throw IllegalArgumentException("已加载的插件中找不到${component}对应的ContainerActivity")
val containerIntent = Intent(this)
containerIntent.component = containerComponent
bundleForPluginLoader.putString(CM_CLASS_NAME_KEY, className)
bundleForPluginLoader.putString(CM_PACKAGE_NAME_KEY, packageName)
containerIntent.putExtra(CM_EXTRAS_BUNDLE_KEY, pluginExtras)
containerIntent.putExtra(CM_BUSINESS_NAME_KEY, businessName)
containerIntent.putExtra(CM_PART_KEY, partKey)
containerIntent.putExtra(CM_LOADER_BUNDLE_KEY, bundleForPluginLoader)
containerIntent.putExtra(LOADER_VERSION_KEY, BuildConfig.VERSION_NAME)
containerIntent.putExtra(PROCESS_ID_KEY, DelegateProviderHolder.sCustomPid)
return containerIntent
}
fun getArchiveFilePathForActivity(className: String) =
getArchiveFilePath(className, PluginManifest::getActivities)
fun getArchiveFilePathForService(className: String) =
getArchiveFilePath(className, PluginManifest::getServices)
fun getArchiveFilePathForProviderByAction(action: String?): kotlin.Pair {
for ((pluginManifest, archiveFilePath) in allLoadedPlugin) {
val providers = pluginManifest.providers
if (providers != null) {
for (provider in providers) {
if (action?.equals(provider.authorities) == true) {
return provider.className to archiveFilePath
}
}
}
}
return null to null
}
fun getArchiveFilePathForProviderByClassName(className: String): kotlin.Pair {
for ((pluginManifest, archiveFilePath) in allLoadedPlugin) {
val providers = pluginManifest.providers
if (providers != null) {
for (provider in providers) {
if (className == provider.className) {
return provider.className to archiveFilePath
}
}
}
}
return null to null
}
fun getAllArchiveFilePaths() = allLoadedPlugin.map { it.second }.toList()
private fun getArchiveFilePath(
className: String,
getComponents: (PluginManifest) -> Array?
): String? {
for ((pluginManifest, archiveFilePath) in allLoadedPlugin) {
val components = getComponents(pluginManifest)
if (components != null) {
for (component in components) {
if (component.className == className) {
return archiveFilePath
}
}
}
}
return null
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/managers/PluginContentProviderManager.kt
================================================
/*
* 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.content.ContentProvider
import android.content.Context
import android.content.pm.ProviderInfo
import android.net.Uri
import android.os.Bundle
import com.tencent.shadow.core.loader.infos.ContainerProviderInfo
import com.tencent.shadow.core.loader.infos.PluginParts
import com.tencent.shadow.core.runtime.PluginManifest
import com.tencent.shadow.core.runtime.UriConverter
import java.util.*
import kotlin.collections.HashSet
import kotlin.collections.set
class PluginContentProviderManager() : UriConverter.UriParseDelegate {
/**
* key : pluginAuthority
* value : plugin ContentProvider
*/
private val providerMap = HashMap()
/**
* key : plugin Authority
* value : containerProvider Authority
*/
private val providerAuthorityMap = HashMap()
private val pluginProviderInfoMap = HashMap?>()
override fun parse(uriString: String): Uri {
if (uriString.startsWith(CONTENT_PREFIX)) {
val uriContent = uriString.substring(CONTENT_PREFIX.length)
val index = uriContent.indexOf("/")
val originalAuthority = if (index != -1) uriContent.substring(0, index) else uriContent
val containerAuthority = getContainerProviderAuthority(originalAuthority)
if (containerAuthority != null) {
return Uri.parse("$CONTENT_PREFIX$containerAuthority/$uriContent")
}
}
return Uri.parse(uriString)
}
override fun parseCall(uriString: String, extra: Bundle): Uri {
val pluginUri = parse(uriString)
extra.putString(SHADOW_BUNDLE_KEY, pluginUri.toString())
return pluginUri
}
fun addContentProviderInfo(
partKey: String,
pluginProviderInfo: PluginManifest.ProviderInfo,
containerProviderInfo: ContainerProviderInfo
) {
if (providerMap.containsKey(pluginProviderInfo.authorities)) {
throw RuntimeException("重复添加 ContentProvider")
}
providerAuthorityMap[pluginProviderInfo.authorities] = containerProviderInfo.authority
var pluginProviderInfos: HashSet? = null
if (pluginProviderInfoMap.containsKey(partKey)) {
pluginProviderInfos = pluginProviderInfoMap[partKey]
} else {
pluginProviderInfos = HashSet()
}
pluginProviderInfos?.add(pluginProviderInfo)
pluginProviderInfoMap.put(partKey, pluginProviderInfos)
}
fun createContentProviderAndCallOnCreate(
context: Context,
partKey: String,
pluginParts: PluginParts?
) {
pluginProviderInfoMap[partKey]?.forEach {
try {
val contentProvider = pluginParts!!.appComponentFactory
.instantiateProvider(pluginParts.classLoader, it.className)
//convert PluginManifest.ProviderInfo to android.content.pm.ProviderInfo
val providerInfo = ProviderInfo()
providerInfo.packageName = context.packageName
providerInfo.name = it.className
providerInfo.authority = it.authorities
providerInfo.grantUriPermissions = it.grantUriPermissions
contentProvider?.attachInfo(context, providerInfo)
providerMap[it.authorities] = contentProvider
} catch (e: Exception) {
throw RuntimeException(
"partKey==$partKey className==${it.className} authorities==${it.authorities}",
e
)
}
}
}
fun getPluginContentProvider(pluginAuthority: String): ContentProvider? {
return providerMap[pluginAuthority]
}
fun getContainerProviderAuthority(pluginAuthority: String): String? {
return providerAuthorityMap[pluginAuthority]
}
fun getAllContentProvider(): Set {
val contentProviders = hashSetOf()
providerMap.keys.forEach {
contentProviders.add(providerMap[it]!!)
}
return contentProviders
}
fun convert2PluginUri(uri: Uri): Uri {
val containerAuthority: String? = uri.authority
if (!providerAuthorityMap.values.contains(containerAuthority)) {
throw IllegalArgumentException("不能识别的uri Authority:$containerAuthority")
}
val uriString = uri.toString()
return Uri.parse(uriString.replace("$containerAuthority/", ""))
}
fun convert2PluginUri(extra: Bundle): Uri {
val uriString = extra.getString(SHADOW_BUNDLE_KEY)
extra.remove(SHADOW_BUNDLE_KEY)
return convert2PluginUri(Uri.parse(uriString))
}
companion object {
private val CONTENT_PREFIX = "content://"
private val SHADOW_BUNDLE_KEY = "shadow_cp_bundle_key"
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/managers/PluginPackageManagerImpl.kt
================================================
/*
* 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.annotation.SuppressLint
import android.content.ComponentName
import android.content.Intent
import android.content.pm.*
import com.tencent.shadow.core.runtime.PluginPackageManager
@SuppressLint("WrongConstant")
internal class PluginPackageManagerImpl(
private val pluginApplicationInfoFromPluginManifest: ApplicationInfo,
private val pluginArchiveFilePath: String,
private val componentManager: ComponentManager,
private val hostPackageManager: PackageManager
) : PluginPackageManager {
override fun getApplicationInfo(packageName: String, flags: Int): ApplicationInfo =
if (packageName.isPlugin()) {
getPluginApplicationInfo(flags)
} else {
hostPackageManager.getApplicationInfo(packageName, flags)
}
/**
* 所有插件中的各种方法签名的getPackageInfo方法汇总到这里。
* 如果包名是插件的,优先返回插件的PackageInfo。
* 否则返回从宿主(系统)查询到的PackageInfo。
* 直接由getPackageArchiveInfo构造的PackageInfo和从getPackageInfo得到的正常的PackageInfo不完全一致。
* 在这里修改它使其尽可能像系统返回的。
*/
private fun getPluginPackageInfoIfPossible(
packageName: String,
flags: Int,
hostPackageInfo: PackageInfo,
): PackageInfo? {
return if (packageName.isPlugin()) {
val packageInfo = hostPackageManager.getPackageArchiveInfo(pluginArchiveFilePath, flags)
if (packageInfo != null) {
packageInfo.applicationInfo = getPluginApplicationInfo(flags)
packageInfo.permissions = hostPackageInfo.permissions
packageInfo.requestedPermissions = hostPackageInfo.requestedPermissions
}
packageInfo
} else {
hostPackageInfo
}
}
@Suppress("DEPRECATION")
override fun getPackageInfo(packageName: String, flags: Int) =
getPluginPackageInfoIfPossible(
packageName,
flags,
hostPackageManager.getPackageInfo(packageName, flags)
)
@Suppress("DEPRECATION")
@SuppressLint("NewApi")
override fun getPackageInfo(versionedPackage: VersionedPackage, flags: Int) =
getPluginPackageInfoIfPossible(
versionedPackage.packageName,
flags,
hostPackageManager.getPackageInfo(versionedPackage, flags)
)
@SuppressLint("NewApi")
override fun getPackageInfo(packageName: String, flags: PackageManager.PackageInfoFlags) =
getPluginPackageInfoIfPossible(
packageName,
flags.value.toInt(),//FIXME 这里会丢失flags升级到Long新增的标志位
hostPackageManager.getPackageInfo(packageName, flags)
)
@SuppressLint("NewApi")
override fun getPackageInfo(
versionedPackage: VersionedPackage,
flags: PackageManager.PackageInfoFlags
) =
getPluginPackageInfoIfPossible(
versionedPackage.packageName,
flags.value.toInt(),//FIXME 这里会丢失flags升级到Long新增的标志位
hostPackageManager.getPackageInfo(versionedPackage, flags)
)
override fun getActivityInfo(component: ComponentName, flags: Int): ActivityInfo? =
getComponentInfo(
component,
flags,
ComponentManager::getArchiveFilePathForActivity,
PackageManager.GET_ACTIVITIES,
{ it?.activities },
PackageManager::getActivityInfo
)
override fun getServiceInfo(component: ComponentName, flags: Int): ServiceInfo? =
getComponentInfo(
component,
flags,
ComponentManager::getArchiveFilePathForService,
PackageManager.GET_SERVICES,
{ it?.services },
PackageManager::getServiceInfo
)
override fun getProviderInfo(component: ComponentName, flags: Int): ProviderInfo {
val (className, archiveFilePath)
= componentManager.getArchiveFilePathForProviderByClassName(component.className)
if (archiveFilePath != null) {
val packageInfo = hostPackageManager.getPackageArchiveInfo(
archiveFilePath, PackageManager.GET_PROVIDERS or flags
)
val componentInfo = packageInfo?.providers?.find {
it.name == className
}
if (componentInfo != null) {
return componentInfo
}
}
return hostPackageManager.getProviderInfo(component, flags)
}
override fun resolveActivity(intent: Intent, flags: Int): ResolveInfo? {
val component = intent.component
if (component != null) {
val activityInfo = getActivityInfo(component, flags)
if (activityInfo != null) {
val resolveInfo = ResolveInfo()
resolveInfo.activityInfo = activityInfo
return resolveInfo
}
}
return hostPackageManager.resolveActivity(intent, flags)
}
override fun resolveService(intent: Intent, flags: Int): ResolveInfo? {
val component = intent.component
if (component != null) {
val serviceInfo = getServiceInfo(component, flags)
if (serviceInfo != null) {
val resolveInfo = ResolveInfo()
resolveInfo.serviceInfo = serviceInfo
return resolveInfo
}
}
return hostPackageManager.resolveService(intent, flags)
}
override fun getArchiveFilePath() = pluginArchiveFilePath
private fun getComponentInfo(
component: ComponentName,
flags: Int,
getArchiveFilePath: ComponentManager.(String) -> String?,
componentGetFlag: Int,
getFromPackageInfo: (PackageInfo?) -> Array?,
getFromHost: PackageManager.(component: ComponentName, flags: Int) -> T?
): T? {
if (component.packageName.isPlugin()) {
val archiveFilePath = componentManager.getArchiveFilePath(component.className)
if (archiveFilePath != null) {
val packageInfo = hostPackageManager.getPackageArchiveInfo(
archiveFilePath, componentGetFlag or flags
)
val componentInfo = getFromPackageInfo(packageInfo)?.find {
it.name == component.className
}
if (componentInfo != null) {
return componentInfo
}
}
}
return hostPackageManager.getFromHost(component, flags)
}
override fun resolveContentProvider(name: String, flags: Int): ProviderInfo? {
val (className, archiveFilePath) = componentManager.getArchiveFilePathForProviderByAction(
name
)
if (archiveFilePath != null) {
val packageInfo = hostPackageManager.getPackageArchiveInfo(
archiveFilePath, PackageManager.GET_PROVIDERS or flags
)
val componentInfo = packageInfo?.providers?.find {
it.name == className
}
if (componentInfo != null) {
return componentInfo
}
}
return hostPackageManager.resolveContentProvider(name, flags)
}
override fun queryContentProviders(processName: String?, uid: Int, flags: Int) =
if (processName == null) {
val allNormalProviders =
hostPackageManager.queryContentProviders(null, 0, flags)
val allPluginProviders = allPluginProviders(flags)
listOf(allNormalProviders, allPluginProviders).flatten()
} else if (processName == pluginApplicationInfoFromPluginManifest.processName &&
uid == pluginApplicationInfoFromPluginManifest.uid
) {
allPluginProviders(flags)
} else {
hostPackageManager.queryContentProviders(processName, uid, flags)
}
private fun allPluginProviders(flags: Int): List =
componentManager.getAllArchiveFilePaths().flatMap {
val packageInfo = hostPackageManager.getPackageArchiveInfo(
it,
PackageManager.GET_PROVIDERS or flags
)
packageInfo?.providers?.asList().orEmpty()
}
private fun String.isPlugin() = pluginApplicationInfoFromPluginManifest.packageName == this
private fun getPluginApplicationInfo(flags: Int): ApplicationInfo {
val copy = ApplicationInfo(pluginApplicationInfoFromPluginManifest)
val needMetaData = flags and PackageManager.GET_META_DATA != 0
if (needMetaData) {
val packageInfo = hostPackageManager.getPackageArchiveInfo(
pluginArchiveFilePath,
PackageManager.GET_META_DATA
)!!
val metaData = packageInfo.applicationInfo.metaData
copy.metaData = metaData
}
return copy
}
}
================================================
FILE: projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/managers/PluginServiceManager.kt
================================================
/*
* 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.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import com.tencent.shadow.core.loader.ShadowPluginLoader
import com.tencent.shadow.core.loader.classloaders.PluginClassLoader
import com.tencent.shadow.core.loader.delegates.ShadowDelegate
import com.tencent.shadow.core.runtime.ShadowApplication
import com.tencent.shadow.core.runtime.ShadowService
import java.util.concurrent.CountDownLatch
/**
* 插件service管理类,负责插件框架内所有service启动,销毁,生命周期管理
* Created by jaylanchen on 2018/11/29.
*/
class PluginServiceManager(mPluginLoader: ShadowPluginLoader, mHostContext: Context) {
private val delegate =
UnsafePluginServiceManager(mPluginLoader, mHostContext)
private val mainThreadHandler = Handler(Looper.getMainLooper())
private inline fun execInMainThread(crossinline action: () -> T): T {
if (Thread.currentThread() === Looper.getMainLooper().thread) {
return action()
} else {
val countDownLatch = CountDownLatch(1)
val result = arrayOfNulls(1)
mainThreadHandler.post {
result[0] = action()
countDownLatch.countDown()
}
countDownLatch.await()
return result[0] as T
}
}
fun startPluginService(service: Intent) =
execInMainThread {
delegate.startPluginService(service)
}
fun stopPluginService(intent: Intent) =
execInMainThread {
delegate.stopPluginService(intent)
}
fun bindPluginService(intent: Intent, conn: ServiceConnection, flags: Int) =
execInMainThread {
delegate.bindPluginService(intent, conn, flags)
}
fun unbindPluginService(conn: ServiceConnection) =
execInMainThread {
delegate.unbindPluginService(conn)
}
}
private open class UnsafePluginServiceManager(
private val mPluginLoader: ShadowPluginLoader,
private val mHostContext: Context
) {
// 保存service的binder
private val mServiceBinderMap = HashMap()
// service对应ServiceConnection集合
private val mServiceConnectionMap = HashMap>()
// ServiceConnection与对应的Intent的集合
private val mConnectionIntentMap = HashMap()
// 所有已启动的service集合
private val mAliveServicesMap = HashMap()
// 通过startService启动起来的service集合
private val mServiceStartByStartServiceSet = HashSet()
// 存在mAliveServicesMap中,且stopService已经调用的service集合
private val mServiceStopCalledMap = HashSet()
private val allDelegates: Collection
get() = mAliveServicesMap.values
companion object {
private var startId: Int = 0
fun getNewStartId(): Int {
startId++
return startId
}
}
fun startPluginService(intent: Intent): ComponentName? {
val componentName = intent.component!!
// 检查所请求的service是否已经存在
if (!mAliveServicesMap.containsKey(componentName)) {
// 不存在则创建
val service = createServiceAndCallOnCreate(intent)
mAliveServicesMap[componentName] = service
// 通过startService启动集合
mServiceStartByStartServiceSet.add(componentName)
}
mAliveServicesMap[componentName]?.onStartCommand(intent, 0, getNewStartId())
return componentName
}
fun stopPluginService(intent: Intent): Boolean {
val componentName = intent.component!!
if (mAliveServicesMap.containsKey(componentName)) {
mServiceStopCalledMap.add(componentName)
// 看是否需要结束掉该service
return destroyServiceIfNeed(componentName)
}
return false
}
fun bindPluginService(intent: Intent, conn: ServiceConnection, flags: Int): Boolean {
// todo #25 目前实现未处理flags,后续实现补上
val componentName = intent.component!!
// 1. 看要bind的service是否创建并在运行了
if (!mAliveServicesMap.containsKey(componentName)) {
// 如果还没创建,则创建,并保持
val service = createServiceAndCallOnCreate(intent)
mAliveServicesMap[componentName] = service
}
val service = mAliveServicesMap[componentName]!!
// 2. 检查是否该Service之前是否被绑定过了
if (!mServiceBinderMap.containsKey(componentName)) {
// 还没调用过onBinder,在这里调用
mServiceBinderMap[componentName] = service.onBind(intent)
}
// 3. 如果binder不为空,则要回调onServiceConnected
mServiceBinderMap[componentName]?.let {
// 检查该connections是否存在了
if (mServiceConnectionMap.containsKey(componentName)) {
if (!mServiceConnectionMap[componentName]!!.contains(conn)) {
// 如果service的bind connection集合中不包含该connection,则加入
mServiceConnectionMap[componentName]!!.add(conn)
mConnectionIntentMap[conn] = intent
// 回调onServiceConnected
conn.onServiceConnected(componentName, it)
} else {
// 已经包含该connection了,说明onServiceConnected已经回调过了,所以这里什么也不用干
}
} else {
// 该connection是第一个bind connection
val connectionSet = HashSet()
connectionSet.add(conn)
mServiceConnectionMap[componentName] = connectionSet
mConnectionIntentMap[conn] = intent
// 回调onServiceConnected
conn.onServiceConnected(componentName, it)
}
}
return true
}
fun unbindPluginService(connection: ServiceConnection): Pair {
var isPluginService = false
var isPluginServiceStopped = false
for ((componentName, connSet) in mServiceConnectionMap) {
if (connSet.contains(connection)) {
isPluginService = true
connSet.remove(connection)
val intent = mConnectionIntentMap.remove(connection)
if (connSet.size == 0) {
// 已经没有任何connection了,mServiceConnectionMap移除该service数据
mServiceConnectionMap.remove(componentName)
// 所有connection都unbind了
mAliveServicesMap[componentName]?.onUnbind(intent!!)
}
// 结束该service
isPluginServiceStopped = destroyServiceIfNeed(componentName)
connection.onServiceDisconnected(componentName)
break
}
}
return Pair(isPluginService, isPluginServiceStopped)
}
fun onConfigurationChanged(newConfig: Configuration?) {
allDelegates.forEach {
it.onConfigurationChanged(newConfig)
}
}
fun onLowMemory() {
allDelegates.forEach {
it.onLowMemory()
}
}
fun onTrimMemory(level: Int) {
allDelegates.forEach {
it.onTrimMemory(level)
}
}
fun onTaskRemoved(rootIntent: Intent) {
allDelegates.forEach {
it.onTaskRemoved(rootIntent)
}
}
fun onDestroy() {
mServiceBinderMap.clear()
mServiceConnectionMap.clear()
mConnectionIntentMap.clear()
mAliveServicesMap.clear()
mServiceStartByStartServiceSet.clear()
mServiceStopCalledMap.clear()
}
private fun createServiceAndCallOnCreate(intent: Intent): ShadowService {
val service = newServiceInstance(intent)
service.onCreate()
return service
}
private fun newServiceInstance(intent: Intent): ShadowService {
val componentName = intent.component!!
val businessName = mPluginLoader.mComponentManager.getComponentBusinessName(componentName)
val partKey = mPluginLoader.mComponentManager.getComponentPartKey(componentName)
val className = componentName.className
val tmpShadowDelegate = TmpShadowDelegate()
mPluginLoader.inject(tmpShadowDelegate, partKey!!)
val service = tmpShadowDelegate.getAppComponentFactory()
.instantiateService(tmpShadowDelegate.getPluginClassLoader(), className, intent)
service.setPluginResources(tmpShadowDelegate.getPluginResources())
service.setPluginClassLoader(tmpShadowDelegate.getPluginClassLoader())
service.setShadowApplication(tmpShadowDelegate.getPluginApplication())
service.setPluginComponentLauncher(tmpShadowDelegate.getComponentManager())
service.applicationInfo = tmpShadowDelegate.getPluginApplication().applicationInfo
service.setBusinessName(businessName)
service.setPluginPartKey(partKey)
//和ShadowActivityDelegate.initPluginActivity一样,attachBaseContext放到最后
service.setHostContextAsBase(mHostContext)
return service
}
private fun destroyServiceIfNeed(service: ComponentName): Boolean {
val destroy = {
// 移除该service,及相关数据
val serviceDelegate = mAliveServicesMap.remove(service)
mServiceStopCalledMap.remove(service)
mServiceBinderMap.remove(service)
mServiceStartByStartServiceSet.remove(service)
// 调用service的onDestroy
serviceDelegate!!.onDestroy()
}
// 如果不是通过startService启动的,则所有connection unbind后就可以结束了
if (!mServiceStartByStartServiceSet.contains(service)) {
if (mServiceConnectionMap[service] == null) {
// 结束该service
destroy()
return true
}
} else {
// 如果该service,有通过startService,则必须调用过stopService且没有bind了,才能销毁
if (mServiceStopCalledMap.contains(service) && !mServiceConnectionMap.containsKey(
service
)
) {
// 结束该service
destroy()
return true
}
}
return false
}
}
private class TmpShadowDelegate : ShadowDelegate() {
fun getPluginApplication(): ShadowApplication = mPluginApplication
fun getAppComponentFactory() = mAppComponentFactory
fun getPluginClassLoader(): PluginClassLoader = mPluginClassLoader
fun getPluginResources(): Resources = mPluginResources
fun getComponentManager(): ComponentManager = mComponentManager
}
================================================
FILE: projects/sdk/core/loader/src/test/kotlin/com/tencent/shadow/core/loader/classloaders/PluginClassLoaderTest.kt
================================================
/*
* 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.classloaders
import org.junit.Assert
import org.junit.Test
/**
* @author zby
* @email linxy59@mail2.sysu.edu.cn
* @date 2019-09-06
* @description test String.inPackage(packageNames: Array): Boolean
* @usage click icon on the left of testString_inPackage()
*/
class PluginClassLoaderTest {
@Test
fun case11() {
val packageNames = arrayOf("a.b.c")
val className = "a.b.c.D"
Assert.assertTrue(className.inPackage(packageNames))
}
@Test
fun case12() {
val packageNames = arrayOf("a.b.c")
val className = "a.b.D"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case13() {
val packageNames = arrayOf("a.b.c")
val className = "a.b.c"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case14() {
val packageNames = arrayOf("a.b.c")
val className = "a.b.c.d.E"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case15() {
val packageNames = arrayOf("xxxx", "a.b.c")
val className = "a.b.c.D"
Assert.assertTrue(className.inPackage(packageNames))
}
@Test
fun case16() {
val packageNames = arrayOf("a.b.c", "xxxx")
val className = "a.b.c.D"
Assert.assertTrue(className.inPackage(packageNames))
}
@Test
fun case21() {
val packageNames = arrayOf("")
val className = "A"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case22() {
val packageNames = arrayOf("")
val className = "a.B"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case31() {
val packageNames = arrayOf("b.c")
val className = "a.b.c.D"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case41() {
val packageNames = arrayOf("a.b.c.*")
val className = "a.b.c.D"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case42() {
val packageNames = arrayOf("a.b.c.*")
val className = "a.b.c"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case43() {
val packageNames = arrayOf("a.b.c.*")
val className = "a.b.c.d.E"
Assert.assertTrue(className.inPackage(packageNames))
}
@Test
fun case44() {
val packageNames = arrayOf("a.b.c.*")
val className = "a.b.c.d.e.F"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case51() {
val packageNames = arrayOf(".*")
val className = "a.b.c.d.E"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case52() {
val packageNames = arrayOf(".*")
val className = "A"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case61() {
val packageNames = arrayOf("a.b.c.**")
val className = "a.b.c.D"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case62() {
val packageNames = arrayOf("a.b.c.**")
val className = "a.b.c.d.E"
Assert.assertTrue(className.inPackage(packageNames))
}
@Test
fun case63() {
val packageNames = arrayOf("a.b.c.**")
val className = "a.b.c.d.e.F"
Assert.assertTrue(className.inPackage(packageNames))
}
@Test
fun case64() {
val packageNames = arrayOf("a.b.c.**")
val className = "a.b.C"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case65() {
val packageNames = arrayOf(".**")
val className = "a.b.c.D"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case66() {
val packageNames = arrayOf(".**")
val className = "a.B"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case67() {
val packageNames = arrayOf(".**")
val className = "A"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case68() {
val packageNames = arrayOf("a.b.c.d.**", "a.b.c2.d2.**")
val className = "a.b.c2.d2.e2.F"
Assert.assertTrue(className.inPackage(packageNames))
}
@Test
fun case71() {
val packageNames = arrayOf("com.tencent.**")
val className = "com.tencentshadow.MyClass"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case72() {
val packageNames = arrayOf("com.tencent.**")
val className = "com.tencentshadow.next.MyClass"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case73() {
val packageNames = arrayOf("com.tencent**")
val className = "com.tencentshadow.MyClass"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case74() {
val packageNames = arrayOf("com.tencent**")
val className = "com.tencentshadow.next.MyClass"
Assert.assertFalse(className.inPackage(packageNames))
}
@Test
fun case75() {
//允许retrofit2包中的类和retrofit2包中所有子包中的类
val packageNames = arrayOf("retrofit2", "retrofit2.**")
val className1 = "retrofit2.Retrofit\$Builder"
val className2 = "retrofit2.a.Retrofit\$Builder"
val className3 = "retrofit2.a.b.Retrofit\$Builder"
Assert.assertTrue(className1.inPackage(packageNames))
Assert.assertTrue(className2.inPackage(packageNames))
Assert.assertTrue(className3.inPackage(packageNames))
}
}
================================================
FILE: projects/sdk/core/manager/.gitignore
================================================
/build
*.iml
================================================
FILE: projects/sdk/core/manager/build.gradle
================================================
apply plugin: 'com.tencent.shadow.internal.common-jar-settings'
dependencies {
implementation project(':utils')
compileOnly project(':common')
api project(':load-parameters')
testImplementation "junit:junit:$junit_version"
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/BasePluginManager.java
================================================
/*
* 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.manager;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import com.tencent.shadow.core.common.Logger;
import com.tencent.shadow.core.common.LoggerFactory;
import com.tencent.shadow.core.manager.installplugin.AppCacheFolderManager;
import com.tencent.shadow.core.manager.installplugin.CopySoBloc;
import com.tencent.shadow.core.manager.installplugin.InstallPluginException;
import com.tencent.shadow.core.manager.installplugin.InstalledDao;
import com.tencent.shadow.core.manager.installplugin.InstalledPlugin;
import com.tencent.shadow.core.manager.installplugin.InstalledPluginDBHelper;
import com.tencent.shadow.core.manager.installplugin.InstalledType;
import com.tencent.shadow.core.manager.installplugin.ODexBloc;
import com.tencent.shadow.core.manager.installplugin.PluginConfig;
import com.tencent.shadow.core.manager.installplugin.SafeZipFile;
import com.tencent.shadow.core.manager.installplugin.UnpackManager;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public abstract class BasePluginManager {
private static final Logger mLogger = LoggerFactory.getLogger(BasePluginManager.class);
/*
* 宿主的context对象
*/
public Context mHostContext;
/**
* 从压缩包中将插件解压出来,解析成InstalledPlugin
*/
private UnpackManager mUnpackManager;
/**
* 插件信息查询数据库接口
*/
final private InstalledDao mInstalledDao;
/**
* UI线程的handler
*/
protected Handler mUiHandler = new Handler(Looper.getMainLooper());
public BasePluginManager(Context context) {
this.mHostContext = context.getApplicationContext();
this.mUnpackManager = new UnpackManager(mHostContext.getFilesDir(), getName());
this.mInstalledDao = new InstalledDao(new InstalledPluginDBHelper(mHostContext, getName()));
}
/**
* PluginManager的名字
* 用于和其他PluginManager区分持续化存储的名字
*/
abstract protected String getName();
/**
* 从文件夹中解压插件
*
* @param dir 文件夹路径
* @return PluginConfig
*/
public final PluginConfig installPluginFromDir(File dir) {
throw new UnsupportedOperationException("TODO");
}
/**
* 从压缩包中解压插件
*
* @param zip 压缩包路径
* @param hash 压缩包hash
* @return PluginConfig
*/
public final PluginConfig installPluginFromZip(File zip, String hash) throws IOException, JSONException {
String zipHash;
if (hash != null) {
zipHash = hash;
} else {
zipHash = mUnpackManager.zipHash(zip);
}
File pluginUnpackDir = mUnpackManager.getPluginUnpackDir(zipHash, zip);
JSONObject configJson = mUnpackManager.getConfigJson(zip);
PluginConfig pluginConfig = PluginConfig.parseFromJson(configJson, pluginUnpackDir);
if (!pluginConfig.isUnpacked()) {
mUnpackManager.unpackPlugin(zip, pluginUnpackDir);
}
return pluginConfig;
}
/**
* 安装完成时调用
*
* 将插件信息持久化到数据库
*
* @param pluginConfig 插件配置信息
* @param soDirMap key:type+partKey
*/
public final void onInstallCompleted(PluginConfig pluginConfig,
Map soDirMap) {
File root = mUnpackManager.getAppDir();
String oDexDir = ODexBloc.isEffective() ?
AppCacheFolderManager.getODexDir(root, pluginConfig.UUID).getAbsolutePath() : null;
//在API 33以上的系统上,禁止动态加载文件可写入,满足系统安全限制
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
setWritableFalseForPluginFiles(pluginConfig);
}
mInstalledDao.insert(pluginConfig, soDirMap, oDexDir);
}
private static void setWritableFalseForPluginFiles(PluginConfig pluginConfig) {
List list = new ArrayList<>();
list.add(pluginConfig.pluginLoader);
list.add(pluginConfig.runTime);
list.addAll(pluginConfig.plugins.values());
for (PluginConfig.FileInfo fileInfo : list) {
//noinspection ResultOfMethodCallIgnored
fileInfo.file.setWritable(false);
}
}
protected InstalledPlugin.Part getPluginPartByPartKey(String uuid, String partKey) {
InstalledPlugin installedPlugin = mInstalledDao.getInstalledPluginByUUID(uuid);
if (installedPlugin == null) {
throw new RuntimeException("没有找到uuid:" + uuid);
}
InstalledPlugin.Part part = installedPlugin.getPart(partKey);
if (part == null) {
throw new RuntimeException("没有找到Part partKey:" + partKey);
}
return part;
}
protected InstalledPlugin getInstalledPlugin(String uuid) {
return mInstalledDao.getInstalledPluginByUUID(uuid);
}
protected InstalledPlugin.Part getLoaderOrRunTimePart(String uuid, int type) {
if (type != InstalledType.TYPE_PLUGIN_LOADER && type != InstalledType.TYPE_PLUGIN_RUNTIME) {
throw new RuntimeException("不支持的type:" + type);
}
InstalledPlugin installedPlugin = mInstalledDao.getInstalledPluginByUUID(uuid);
if (type == InstalledType.TYPE_PLUGIN_RUNTIME) {
if (installedPlugin.runtimeFile != null) {
return installedPlugin.runtimeFile;
}
} else if (type == InstalledType.TYPE_PLUGIN_LOADER) {
if (installedPlugin.pluginLoaderFile != null) {
return installedPlugin.pluginLoaderFile;
}
}
throw new RuntimeException("没有找到Part type :" + type);
}
/**
* odex优化
*
* @param uuid 插件包的uuid
* @param partKey 要oDex的插件partkey
*/
public final void oDexPlugin(String uuid, String partKey, File apkFile) throws InstallPluginException {
if (!ODexBloc.isEffective()) {
return;
}
try {
File root = mUnpackManager.getAppDir();
File oDexDir = AppCacheFolderManager.getODexDir(root, uuid);
ODexBloc.oDexPlugin(apkFile, oDexDir, AppCacheFolderManager.getODexCopiedFile(oDexDir, partKey));
} catch (InstallPluginException e) {
if (mLogger.isErrorEnabled()) {
mLogger.error("oDexPlugin exception:", e);
}
throw e;
}
}
/**
* odex优化
*
* @param uuid 插件包的uuid
* @param type 要oDex的插件类型 @class IntalledType loader or runtime
* @param apkFile 插件apk文件
*/
public final void oDexPluginLoaderOrRunTime(String uuid, int type, File apkFile) throws InstallPluginException {
if (!ODexBloc.isEffective()) {
return;
}
try {
File root = mUnpackManager.getAppDir();
File oDexDir = AppCacheFolderManager.getODexDir(root, uuid);
String key = type == InstalledType.TYPE_PLUGIN_LOADER ? "loader" : "runtime";
ODexBloc.oDexPlugin(apkFile, oDexDir, AppCacheFolderManager.getODexCopiedFile(oDexDir, key));
} catch (InstallPluginException e) {
if (mLogger.isErrorEnabled()) {
mLogger.error("oDexPluginLoaderOrRunTime exception:", e);
}
throw e;
}
}
/**
* 解压插件apk中的so。
*
* 插件的ABI和宿主正在使用的保持一致。
* 注意:如果宿主没有打包so,它的ABI会被系统自动设置为设备默认值,
* 默认值可能和插件apk中打包的ABI不一致,导致插件so解压不正确。
*
* @param uuid 插件包的uuid
* @param partKey 要解压so的插件partkey
* @param apkFile 插件apk文件
* @return soDirMap条目
*/
public final Pair extractSo(String uuid, String partKey, File apkFile) throws InstallPluginException {
try {
File root = mUnpackManager.getAppDir();
File soDir = AppCacheFolderManager.getLibDir(root, uuid);
String soDirMapKey = InstalledType.TYPE_PLUGIN + partKey;
String soDirPath = soDir.getAbsolutePath();
String pluginPreferredAbi = getPluginPreferredAbi(getPluginSupportedAbis(), apkFile);
if (pluginPreferredAbi.isEmpty()) {
if (mLogger.isInfoEnabled()) {
mLogger.info("插件没有so");
}
} else {
String filter = "lib/" + pluginPreferredAbi + "/";
// 插件如果设置了android:extractNativeLibs="false",则不需要解压出so
boolean needExtractNativeLibs = needExtractNativeLibs(apkFile, filter);
if (mLogger.isInfoEnabled()) {
mLogger.info("extractSo uuid=={} partKey=={} apkFile=={} soDir=={} filter=={} needExtractNativeLibs=={}",
uuid, partKey, apkFile.getAbsolutePath(), soDir.getAbsolutePath(), filter, needExtractNativeLibs);
}
if (needExtractNativeLibs) {
CopySoBloc.copySo(apkFile, soDir
, AppCacheFolderManager.getLibCopiedFile(soDir, partKey), filter);
} else {
soDirPath = apkFile.getAbsolutePath() + "!/" + filter;
}
}
return new Pair<>(soDirMapKey, soDirPath);
} catch (InstallPluginException e) {
if (mLogger.isErrorEnabled()) {
mLogger.error("extractSo exception:", e);
}
throw e;
}
}
/**
* 插件apk的so解压
*
* @param uuid 插件包的uuid
* @param type 要oDex的插件类型 @class IntalledType loader or runtime
* @param apkFile 插件apk文件
* @return soDirMap条目
*/
public final Pair extractLoaderOrRunTimeSo(String uuid,
int type,
File apkFile)
throws InstallPluginException {
try {
File root = mUnpackManager.getAppDir();
String key = type == InstalledType.TYPE_PLUGIN_LOADER ? "loader" : "runtime";
String pluginPreferredAbi = getPluginPreferredAbi(getPluginSupportedAbis(), apkFile);
String filter = "lib/" + pluginPreferredAbi + "/";
File soDir = AppCacheFolderManager.getLibDir(root, uuid);
if (pluginPreferredAbi.isEmpty()) {
if (mLogger.isInfoEnabled()) {
mLogger.info(key + "没有so");
}
} else {
CopySoBloc.copySo(apkFile, soDir
, AppCacheFolderManager.getLibCopiedFile(soDir, key), filter);
}
String soDirMapKey = Integer.toString(type) + null;// 同InstalledDao.parseConfig
String soDirPath = soDir.getAbsolutePath();
return new Pair<>(soDirMapKey, soDirPath);
} catch (InstallPluginException e) {
if (mLogger.isErrorEnabled()) {
mLogger.error("extractLoaderOrRunTimeSo exception:", e);
}
throw e;
}
}
/**
* 获取已安装的插件,最后安装的排在返回List的最前面
*
* @param limit 最多获取个数
*/
public final List getInstalledPlugins(int limit) {
return mInstalledDao.getLatestPlugins(limit);
}
/**
* 删除指定uuid的插件
*
* @param uuid 插件包的uuid
* @return 是否全部执行成功
*/
public boolean deleteInstalledPlugin(String uuid) {
InstalledPlugin installedPlugin = mInstalledDao.getInstalledPluginByUUID(uuid);
boolean suc = true;
if (installedPlugin.runtimeFile != null) {
if (!deletePart(installedPlugin.runtimeFile)) {
suc = false;
}
}
if (installedPlugin.pluginLoaderFile != null) {
if (!deletePart(installedPlugin.pluginLoaderFile)) {
suc = false;
}
}
for (Map.Entry plugin : installedPlugin.plugins.entrySet()) {
if (!deletePart(plugin.getValue())) {
suc = false;
}
}
if (mInstalledDao.deleteByUUID(uuid) <= 0) {
suc = false;
}
return suc;
}
private boolean deletePart(InstalledPlugin.Part part) {
boolean suc = true;
if (!deleteFileOrDirectory(part.pluginFile)) {
suc = false;
}
if (part.oDexDir != null && !deleteFileOrDirectory(part.oDexDir)) {
suc = false;
}
if (part.libraryDir != null && !deleteFileOrDirectory(part.libraryDir)) {
suc = false;
}
return suc;
}
/**
* 删除文件或目录,递归删除
*
* @param fileOrDirectory 文件或目录
* @return 是否删除成功
*/
private boolean deleteFileOrDirectory(File fileOrDirectory) {
if (fileOrDirectory == null || !fileOrDirectory.exists()) {
return true;
}
if (fileOrDirectory.isDirectory()) {
File[] files = fileOrDirectory.listFiles();
if (files != null) {
for (File file : files) {
if (!deleteFileOrDirectory(file)) {
return false;
}
}
}
}
return fileOrDirectory.delete();
}
/**
* 当前插件希望采用的ABI。
* 子类可以override重新决定。
*
* @param pluginSupportedAbis 从getPluginSupportedAbis方法得到的可选ABI列表
* @param apkFile 插件apk文件
* @return 最终决定的ABI。插件没有so时返回空字符串。
* @throws InstallPluginException 读取apk文件失败时抛出
*/
protected String getPluginPreferredAbi(String[] pluginSupportedAbis, File apkFile)
throws InstallPluginException {
try (ZipFile zipFile = new SafeZipFile(apkFile)) {
//找出插件apk中lib目录下都有哪些子目录
Set subDirsInLib = new LinkedHashSet<>();
Enumeration extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
if (name.startsWith("lib/")) {
String[] split = name.split("/");
if (split.length == 3) {// like "lib/arm64-v8a/libabc.so"
subDirsInLib.add(split[1]);
}
}
}
for (String supportedAbi : pluginSupportedAbis) {
if (subDirsInLib.contains(supportedAbi)) {
return supportedAbi;
}
}
return "";
} catch (IOException e) {
throw new InstallPluginException("读取apk失败,apkFile==" + apkFile, e);
}
}
/**
* 获取可用的ABI列表。
* 和Build.SUPPORTED_ABIS的区别是,这是宿主已经决定了当前进程用32位so还是64位so了,
* 所以可用的ABI只能是其中一部分。
*/
private String[] getPluginSupportedAbis() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
String nativeLibraryDir = mHostContext.getApplicationInfo().nativeLibraryDir;
int nextIndexOfLastSlash = nativeLibraryDir.lastIndexOf('/') + 1;
String instructionSet = nativeLibraryDir.substring(nextIndexOfLastSlash);
if (!isKnownInstructionSet(instructionSet)) {
throw new IllegalStateException("不认识的instructionSet==" + instructionSet);
}
boolean is64Bit = is64BitInstructionSet(instructionSet);
return is64Bit ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS;
} else {
String cpuAbi = Build.CPU_ABI;
String cpuAbi2 = Build.CPU_ABI2;
ArrayList list = new ArrayList<>(2);
if (cpuAbi != null && !cpuAbi.isEmpty()) {
list.add(cpuAbi);
}
if (cpuAbi2 != null && !cpuAbi2.isEmpty()) {
list.add(cpuAbi2);
}
return list.toArray(new String[0]);
}
}
/**
* 根据VMRuntime.ABI_TO_INSTRUCTION_SET_MAP
*/
private static boolean isKnownInstructionSet(String instructionSet) {
return "arm".equals(instructionSet) ||
"mips".equals(instructionSet) ||
"mips64".equals(instructionSet) ||
"x86".equals(instructionSet) ||
"x86_64".equals(instructionSet) ||
"arm64".equals(instructionSet);
}
/**
* Returns whether the given {@code instructionSet} is 64 bits.
*
* @param instructionSet a string representing an instruction set.
* @return true if given {@code instructionSet} is 64 bits, false otherwise.
*
* copy from VMRuntime.java
*/
private static boolean is64BitInstructionSet(String instructionSet) {
return "arm64".equals(instructionSet) ||
"x86_64".equals(instructionSet) ||
"mips64".equals(instructionSet);
}
private static boolean needExtractNativeLibs(File apkFile, String filter) throws InstallPluginException {
//android:extractNativeLibs是API 23引入的
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
try (ZipFile zipFile = new SafeZipFile(apkFile)) {
Enumeration extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
if (name.startsWith(filter)) {
return entry.getMethod() != ZipEntry.STORED;
}
}
return false;
} catch (IOException e) {
throw new InstallPluginException("读取apk失败,apkFile==" + apkFile, e);
}
}
/**
* 释放资源
*/
public void close() {
mInstalledDao.close();
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/AppCacheFolderManager.java
================================================
/*
* 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.manager.installplugin;
import java.io.File;
/**
* 目录各模块的目录关系管理
*/
public class AppCacheFolderManager {
public static File getVersionDir(File root, String appName, String version) {
return new File(getAppDir(root, appName), version);
}
public static File getAppDir(File root, String appName) {
return new File(root, appName);
}
public static File getODexDir(File root, String key) {
return new File(getODexRootDir(root), key + "_odex");
}
public static File getODexCopiedFile(File oDexDir, String key) {
return new File(oDexDir, key + "_oDexed");
}
private static File getODexRootDir(File root) {
return new File(root, "oDex");
}
public static File getLibDir(File root, String key) {
return new File(getLibRootDir(root), key + "_lib");
}
public static File getLibCopiedFile(File soDir, String key) {
return new File(soDir, key + "_copied");
}
private static File getLibRootDir(File root) {
return new File(root, "lib");
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/CopySoBloc.java
================================================
/*
* 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.manager.installplugin;
import android.text.TextUtils;
import com.tencent.shadow.core.common.Logger;
import com.tencent.shadow.core.common.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class CopySoBloc {
private static final Logger mLogger = LoggerFactory.getLogger(CopySoBloc.class);
private static ConcurrentHashMap sLocks = new ConcurrentHashMap<>();
public static void copySo(File apkFile, File soDir, File copiedTagFile, String filter) throws InstallPluginException {
String key = apkFile.getAbsolutePath();
Object lock = sLocks.get(key);
if (lock == null) {
lock = new Object();
sLocks.put(key, lock);
}
synchronized (lock) {
if (TextUtils.isEmpty(filter) || copiedTagFile.exists()) {
return;
}
//如果so目录存在但是个文件,不是目录,那超出预料了。删除了也不一定能工作正常。
if (soDir.exists() && soDir.isFile()) {
throw new InstallPluginException("soDir=" + soDir.getAbsolutePath() + "已存在,但它是个文件,不敢贸然删除");
}
//创建so目录
soDir.mkdirs();
ZipFile zipFile = null;
try {
zipFile = new SafeZipFile(apkFile);
Enumeration extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.getName().startsWith(filter)) {
String fileName = entry.getName().substring(filter.length());
MinFileUtils.writeOutZipEntry(zipFile, entry, soDir, fileName);
}
}
// 外边创建完成标记
try {
copiedTagFile.createNewFile();
} catch (IOException e) {
throw new InstallPluginException("创建so复制完毕 创建tag文件失败:" + copiedTagFile.getAbsolutePath(), e);
}
} catch (Exception e) {
throw new InstallPluginException("解压so 失败 apkFile:" + apkFile.getAbsolutePath() + " abi:" + filter, e);
} finally {
try {
if (zipFile != null) {
zipFile.close();
}
} catch (IOException e) {
mLogger.warn("zip关闭时出错忽略", e);
}
}
}
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/InstallPluginException.java
================================================
/*
* 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.manager.installplugin;
public class InstallPluginException extends Exception {
public InstallPluginException(String message) {
super(message);
}
public InstallPluginException(String message, Throwable cause) {
super(message, cause);
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/InstalledDao.java
================================================
/*
* 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.manager.installplugin;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import org.json.JSONArray;
import org.json.JSONException;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class InstalledDao {
final private InstalledPluginDBHelper mDBHelper;
public InstalledDao(InstalledPluginDBHelper dbHelper) {
mDBHelper = dbHelper;
}
/**
* 根据插件配置信息插入一组数据
*
* @param pluginConfig 插件配置信息
* @param soDirMap key:type+partKey
* @param oDexDir
*/
public void insert(PluginConfig pluginConfig, Map soDirMap, String oDexDir) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
if (soDirMap == null) {
soDirMap = Collections.emptyMap();
}
List contentValuesList = parseConfig(pluginConfig, soDirMap, oDexDir);
db.beginTransaction();
try {
for (ContentValues contentValues : contentValuesList) {
db.replace(InstalledPluginDBHelper.TABLE_NAME_MANAGER, null, contentValues);
}
//把最后一次uuid的插件安装时间作为所有相同uuid的插件的安装时间
ContentValues values = new ContentValues();
values.put(InstalledPluginDBHelper.COLUMN_INSTALL_TIME, pluginConfig.storageDir.lastModified());
db.update(InstalledPluginDBHelper.TABLE_NAME_MANAGER, values, InstalledPluginDBHelper.COLUMN_UUID + " = ?", new String[]{pluginConfig.UUID});
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* 删除UUID相关的数据
*
* @param UUID 插件的发布id
* @return 影响的数据行数
*/
public int deleteByUUID(String UUID) {
int row;
SQLiteDatabase db = mDBHelper.getWritableDatabase();
db.beginTransaction();
try {
row = db.delete(InstalledPluginDBHelper.TABLE_NAME_MANAGER, InstalledPluginDBHelper.COLUMN_UUID + " =?", new String[]{UUID});
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return row;
}
/**
* 根据uuid和APPID获取对应的插件信息
*
* @param uuid 插件的发布id
* @return 插件安装数据
*/
@SuppressLint("Range")
public InstalledPlugin getInstalledPluginByUUID(String uuid) {
SQLiteDatabase db = mDBHelper.getReadableDatabase();
Cursor cursor = db.query(
InstalledPluginDBHelper.TABLE_NAME_MANAGER,
null,
InstalledPluginDBHelper.COLUMN_UUID + " = ?",
new String[]{uuid},
null,
null,
null
);
InstalledPlugin installedPlugin = new InstalledPlugin();
installedPlugin.UUID = uuid;
while (cursor.moveToNext()) {
int type = cursor.getInt(cursor.getColumnIndex(InstalledPluginDBHelper.COLUMN_TYPE));
if (type == InstalledType.TYPE_UUID) {
installedPlugin.UUID_NickName = cursor.getString(cursor.getColumnIndex(InstalledPluginDBHelper.COLUMN_VERSION));
} else {
File pluginFile = new File(cursor.getString(cursor.getColumnIndex(InstalledPluginDBHelper.COLUMN_PATH)));
String oDexPath = cursor.getString(cursor.getColumnIndex(InstalledPluginDBHelper.COLUMN_PLUGIN_ODEX));
File oDexDir = oDexPath == null ? null : new File(oDexPath);
String libPath = cursor.getString(cursor.getColumnIndex(InstalledPluginDBHelper.COLUMN_PLUGIN_LIB));
File libDir = libPath == null ? null : new File(libPath);
if (type == InstalledType.TYPE_PLUGIN_LOADER) {
installedPlugin.pluginLoaderFile = new InstalledPlugin.Part(type, pluginFile, oDexDir, libDir);
} else if (type == InstalledType.TYPE_PLUGIN_RUNTIME) {
installedPlugin.runtimeFile = new InstalledPlugin.Part(type, pluginFile, oDexDir, libDir);
} else {
String businessName = cursor.getString(cursor.getColumnIndex(InstalledPluginDBHelper.COLUMN_BUSINESS_NAME));
String partKey = cursor.getString(cursor.getColumnIndex(InstalledPluginDBHelper.COLUMN_PARTKEY));
if (type == InstalledType.TYPE_PLUGIN) {
String[] dependsOn = getArrayStringByColumnName(InstalledPluginDBHelper.COLUMN_DEPENDSON, cursor);
String[] hostWhiteList = getArrayStringByColumnName(InstalledPluginDBHelper.COLUMN_HOST_WHITELIST, cursor);
installedPlugin.plugins.put(partKey, new InstalledPlugin.PluginPart(type, businessName, pluginFile, oDexDir, libDir, dependsOn, hostWhiteList));
} else {
throw new RuntimeException("出现不认识的type==" + type);
}
}
}
}
cursor.close();
return installedPlugin;
}
private String[] getArrayStringByColumnName(String columnName, Cursor cursor) {
int columnIndex = cursor.getColumnIndex(columnName);
boolean hasColumn = !cursor.isNull(columnIndex);
String[] arrayString;
if (hasColumn) {
String string = cursor.getString(columnIndex);
try {
JSONArray jsonArray = new JSONArray(string);
arrayString = new String[jsonArray.length()];
for (int i = 0; i < jsonArray.length(); i++) {
arrayString[i] = jsonArray.getString(i);
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
} else {
arrayString = null;
}
return arrayString;
}
/**
* @deprecated 方法名拼写错误
*/
@Deprecated
public List getLastPlugins(int limit) {
return getLatestPlugins(limit);
}
/**
* 获取最近的插件列表数据
*
* @param limit 获取的数据数量
* @return 插件列表数据
*/
public List getLatestPlugins(int limit) {
SQLiteDatabase db = mDBHelper.getReadableDatabase();
// 查询所有type为uuid的行,按自增ID主键倒序
// 即自增ID越大的uuid为最新安装的
Cursor cursor = db.query(
InstalledPluginDBHelper.TABLE_NAME_MANAGER,
new String[]{InstalledPluginDBHelper.COLUMN_UUID},
InstalledPluginDBHelper.COLUMN_TYPE + " = ?",
new String[]{String.valueOf(InstalledType.TYPE_UUID)},
null,
null,
InstalledPluginDBHelper.COLUMN_ID + " DESC",
Integer.toString(limit)
);
List uuids = new ArrayList<>();
while (cursor.moveToNext()) {
String uuid = cursor.getString(cursor.getColumnIndex(InstalledPluginDBHelper.COLUMN_UUID));
uuids.add(uuid);
}
cursor.close();
List installedPlugins = new ArrayList<>();
for (String uuid : uuids) {
installedPlugins.add(getInstalledPluginByUUID(uuid));
}
return installedPlugins;
}
private List parseConfig(PluginConfig pluginConfig,
Map soDirMap,// key:type+partKey
String oDexDir
) {
List installedRows = new ArrayList<>();
if (pluginConfig.pluginLoader != null) {
String soDir = soDirMap.get(Integer.toString(InstalledType.TYPE_PLUGIN_LOADER) + null);
installedRows.add(new InstalledRow(pluginConfig.pluginLoader.hash, null, pluginConfig.pluginLoader.file.getAbsolutePath(), InstalledType.TYPE_PLUGIN_LOADER,
soDir, oDexDir));
}
if (pluginConfig.runTime != null) {
String soDir = soDirMap.get(Integer.toString(InstalledType.TYPE_PLUGIN_RUNTIME) + null);
installedRows.add(new InstalledRow(pluginConfig.runTime.hash, null, pluginConfig.runTime.file.getAbsolutePath(), InstalledType.TYPE_PLUGIN_RUNTIME,
soDir, oDexDir));
}
if (pluginConfig.plugins != null) {
Set> plugins = pluginConfig.plugins.entrySet();
for (Map.Entry plugin : plugins) {
PluginConfig.PluginFileInfo fileInfo = plugin.getValue();
String partKey = plugin.getKey();
String soDir = soDirMap.get(InstalledType.TYPE_PLUGIN + partKey);
installedRows.add(
new InstalledRow(
fileInfo.hash,
fileInfo.businessName,
partKey,
fileInfo.dependsOn,
fileInfo.file.getAbsolutePath(),
InstalledType.TYPE_PLUGIN,
fileInfo.hostWhiteList,
soDir, oDexDir
)
);
}
}
InstalledRow uuidRow = new InstalledRow();
uuidRow.type = InstalledType.TYPE_UUID;
uuidRow.filePath = pluginConfig.UUID;
installedRows.add(uuidRow);
List contentValues = new ArrayList<>();
for (InstalledRow row : installedRows) {
row.installedTime = pluginConfig.storageDir.lastModified();
row.UUID = pluginConfig.UUID;
row.version = pluginConfig.UUID_NickName;
contentValues.add(row.toContentValues());
}
return contentValues;
}
/**
* 释放资源
*/
public void close() {
mDBHelper.close();
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/InstalledPlugin.java
================================================
/*
* 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.manager.installplugin;
import java.io.File;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 已安装好的插件.
*
* 这是一个Serializable类,目的是可以将这个类的对象放在Intent中跨进程传递。
* 注意:equals()方法必须重载,并包含全部域变量。
*
* @author owenguo
*/
public class InstalledPlugin implements Serializable {
/**
* 标识一次插件发布的id
*/
public String UUID;
/**
* 标识一次插件发布的id,可以使用自定义格式描述版本信息
*/
public String UUID_NickName;
/**
* pluginLoader文件
*/
public Part pluginLoaderFile;
/**
* runtime文件
*/
public Part runtimeFile;
/**
* 插件文件
*/
public Map plugins = new HashMap<>();
InstalledPlugin() {
}
public boolean hasPart(String partKey) {
return plugins.containsKey(partKey);
}
public PluginPart getPlugin(String partKey) {
return plugins.get(partKey);
}
public Part getPart(String partKey) {
return plugins.get(partKey);
}
static public class Part implements Serializable {
final public int pluginType;
final public File pluginFile;
public File oDexDir;
public File libraryDir;
Part(int pluginType, File file, File oDexDir, File libraryDir) {
this.pluginType = pluginType;
this.oDexDir = oDexDir;
this.libraryDir = libraryDir;
this.pluginFile = file;
}
}
static public class PluginPart extends Part {
final public String businessName;
final public String[] dependsOn;
final public String[] hostWhiteList;
PluginPart(int pluginType, String businessName, File file, File oDexDir, File libraryDir, String[] dependsOn, String[] hostWhiteList) {
super(pluginType, file, oDexDir, libraryDir);
this.businessName = businessName;
this.dependsOn = dependsOn;
this.hostWhiteList = hostWhiteList;
}
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/InstalledPluginDBHelper.java
================================================
/*
* 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.manager.installplugin;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.util.ArrayList;
import java.util.List;
public class InstalledPluginDBHelper extends SQLiteOpenHelper {
/**
* 数据库名称
*/
final static String DB_NAME_PREFIX = "shadow_installed_plugin_db";
/**
* 表名称
*/
public final static String TABLE_NAME_MANAGER = "shadowPluginManager";
/**
* 自增主键
*/
public final static String COLUMN_ID = "id";
/**
* 插件的hash
*/
public final static String COLUMN_HASH = "hash";
/**
* 插件的类型
*/
public final static String COLUMN_TYPE = "type";
/**
* 插件的路径
*/
public final static String COLUMN_PATH = "filePath";
/**
* 插件的businessName
*/
public final static String COLUMN_BUSINESS_NAME = "businessName";
/**
* 插件的名称
*/
public final static String COLUMN_PARTKEY = "partKey";
/**
* 插件的依赖
*/
public final static String COLUMN_DEPENDSON = "dependsOn";
/**
* 插件的uuid
*/
public final static String COLUMN_UUID = "uuid";
/**
* 插件的版本号
*/
public final static String COLUMN_VERSION = "version";
/**
* 插件的安装时间
*/
public final static String COLUMN_INSTALL_TIME = "installedTime";
/**
* 插件的dex目录
*/
public final static String COLUMN_PLUGIN_ODEX = "odexPath";
/**
* 插件的lib目录
*/
public final static String COLUMN_PLUGIN_LIB = "libPath";
/**
* 插件的依赖
*/
public final static String COLUMN_HOST_WHITELIST = "hostWhiteList";
/**
* 数据库的版本号
*/
private final static int VERSION = 4;
public InstalledPluginDBHelper(Context context, String name) {
super(context, DB_NAME_PREFIX + name, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME_MANAGER + " ( "
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ COLUMN_HASH + " VARCHAR , "
+ COLUMN_PATH + " VARCHAR, "
+ COLUMN_TYPE + " INTEGER, "
+ COLUMN_BUSINESS_NAME + " VARCHAR, "
+ COLUMN_PARTKEY + " VARCHAR, "
+ COLUMN_DEPENDSON + " VARCHAR, "
+ COLUMN_UUID + " VARCHAR, "
+ COLUMN_VERSION + " VARCHAR, "
+ COLUMN_INSTALL_TIME + " INTEGER ,"
+ COLUMN_PLUGIN_ODEX + " VARCHAR ,"
+ COLUMN_PLUGIN_LIB + " VARCHAR ,"
+ COLUMN_HOST_WHITELIST + " VARCHAR "
+ ");";
db.execSQL(sql);
}
@Override
@SuppressLint("Range")
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 2) {
db.beginTransaction();
try {
Cursor cursor = db.query(
true,
TABLE_NAME_MANAGER,
new String[]{COLUMN_UUID, COLUMN_TYPE},
COLUMN_TYPE + " = ?",
new String[]{"2"},//Interface Type
null, null, null, null
);
List uuids = new ArrayList<>();
while (cursor.moveToNext()) {
String uuid = cursor.getString(cursor.getColumnIndex(COLUMN_UUID));
uuids.add(uuid);
}
cursor.close();
for (String uuid : uuids) {
db.delete(TABLE_NAME_MANAGER, COLUMN_UUID + " = ?", new String[]{uuid});
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
if (oldVersion < 3) {
db.beginTransaction();
try {
//添加列COLUMN_HOST_WHITELIST
db.execSQL("ALTER TABLE " + TABLE_NAME_MANAGER + " ADD " + COLUMN_HOST_WHITELIST + " VARCHAR");
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
if (oldVersion < 4) {
db.beginTransaction();
try {
//添加列COLUMN_BUSINESS_NAME。所有旧行保持空值即可,表示同宿主相同业务。
db.execSQL("ALTER TABLE " + TABLE_NAME_MANAGER + " ADD " + COLUMN_BUSINESS_NAME + " VARCHAR");
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/InstalledRow.java
================================================
/*
* 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.manager.installplugin;
import android.content.ContentValues;
import org.json.JSONArray;
import java.util.Arrays;
public class InstalledRow {
public String hash;
public long installedTime;
public String partKey;
public String businessName;
public String[] dependsOn;
public String[] hostWhiteList;
public String filePath;
public int type;
public String UUID;
public String version;
public String soDir;
public String oDexDir;
public InstalledRow() {
}
public InstalledRow(String hash, String partKey, String filePath, int type, String soDir, String oDexDir) {
this.hash = hash;
this.partKey = partKey;
this.filePath = filePath;
this.type = type;
this.soDir = soDir;
this.oDexDir = oDexDir;
}
public InstalledRow(String hash, String businessName, String partKey, String[] dependsOn, String filePath, int type, String[] hostWhiteList, String soDir, String oDexDir) {
this(hash, partKey, filePath, type, soDir, oDexDir);
this.businessName = businessName;
this.dependsOn = dependsOn;
this.hostWhiteList = hostWhiteList;
}
public ContentValues toContentValues() {
ContentValues contentValues = new ContentValues();
contentValues.put(InstalledPluginDBHelper.COLUMN_HASH, hash);
contentValues.put(InstalledPluginDBHelper.COLUMN_INSTALL_TIME, installedTime);
if (businessName != null) {
contentValues.put(InstalledPluginDBHelper.COLUMN_BUSINESS_NAME, businessName);
}
if (partKey != null) {
contentValues.put(InstalledPluginDBHelper.COLUMN_PARTKEY, partKey);
}
if (dependsOn != null) {
JSONArray jsonArray = new JSONArray(Arrays.asList(dependsOn));
contentValues.put(InstalledPluginDBHelper.COLUMN_DEPENDSON, jsonArray.toString());
}
if (hostWhiteList != null) {
JSONArray jsonArray = new JSONArray(Arrays.asList(hostWhiteList));
contentValues.put(InstalledPluginDBHelper.COLUMN_HOST_WHITELIST, jsonArray.toString());
}
contentValues.put(InstalledPluginDBHelper.COLUMN_TYPE, type);
contentValues.put(InstalledPluginDBHelper.COLUMN_UUID, UUID);
contentValues.put(InstalledPluginDBHelper.COLUMN_VERSION, version);
contentValues.put(InstalledPluginDBHelper.COLUMN_PATH, filePath);
contentValues.put(InstalledPluginDBHelper.COLUMN_PLUGIN_LIB, soDir);
contentValues.put(InstalledPluginDBHelper.COLUMN_PLUGIN_ODEX, oDexDir);
return contentValues;
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/InstalledType.java
================================================
/*
* 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.manager.installplugin;
public class InstalledType {
public final static int TYPE_PLUGIN = 1;
public final static int TYPE_PLUGIN_LOADER = 3;
public final static int TYPE_PLUGIN_RUNTIME = 4;
public final static int TYPE_UUID = 5;
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/MinFileUtils.java
================================================
/*
* 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.manager.installplugin;
import com.tencent.shadow.core.common.Logger;
import com.tencent.shadow.core.common.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* 复制自
*
* @version $Id: FileUtils.java 1722481 2016-01-01 01:42:04Z dbrosius $
*
* 没有使用完整的commons-io是因为要控制方法数
*/
public class MinFileUtils {
private static final Logger mLogger = LoggerFactory.getLogger(MinFileUtils.class);
/**
* 保证文件的父目录存在,如果不存在,则从不存在的祖先目录开始创建完成路径
*
* @param file 需要
* @throws IOException
*/
public static void ensureParentDirExists(File file) throws IOException {
File parent = file.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
throw new IOException("创建父目录失败,文件目录:" + file.getAbsolutePath() + " parent dir exists=" + parent.exists());
}
}
/**
* Cleans a directory without deleting it.
*
* @param directory directory to clean
* @throws IOException in case cleaning is unsuccessful
* @throws IllegalArgumentException if {@code directory} does not exist or is not a directory
*/
public static void cleanDirectory(final File directory) throws IOException {
final File[] files = verifiedListFiles(directory);
IOException exception = null;
for (final File file : files) {
try {
forceDelete(file);
} catch (final IOException ioe) {
exception = ioe;
}
}
if (null != exception) {
throw exception;
}
}
/**
* Deletes a file. If file is a directory, delete it and all sub-directories.
*
* The difference between File.delete() and this method are:
*
* A directory to be deleted does not have to be empty.
* You get exceptions when a file or directory cannot be deleted.
* (java.io.File methods returns a boolean)
*
*
* @param file file or directory to delete, must not be {@code null}
* @throws NullPointerException if the directory is {@code null}
* @throws FileNotFoundException if the file was not found
* @throws IOException in case deletion is unsuccessful
*/
private static void forceDelete(final File file) throws IOException {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
final boolean filePresent = file.exists();
if (!file.delete()) {
if (!filePresent) {
throw new FileNotFoundException("File does not exist: " + file);
}
final String message =
"Unable to delete file: " + file;
throw new IOException(message);
}
}
}
/**
* Deletes a directory recursively.
*
* @param directory directory to delete
* @throws IOException in case deletion is unsuccessful
* @throws IllegalArgumentException if {@code directory} does not exist or is not a directory
*/
private static void deleteDirectory(final File directory) throws IOException {
if (!directory.exists()) {
return;
}
cleanDirectory(directory);
if (!directory.delete()) {
final String message =
"Unable to delete directory " + directory + ".";
throw new IOException(message);
}
}
/**
* Lists files in a directory, asserting that the supplied directory satisfies exists and is a directory
*
* @param directory The directory to list
* @return The files in the directory, never null.
* @throws IOException if an I/O error occurs
*/
private static File[] verifiedListFiles(File directory) throws IOException {
if (!directory.exists()) {
final String message = directory + " does not exist";
throw new IllegalArgumentException(message);
}
if (!directory.isDirectory()) {
final String message = directory + " is not a directory";
throw new IllegalArgumentException(message);
}
final File[] files = directory.listFiles();
if (files == null) { // null if security restricted
throw new IOException("Failed to list contents of " + directory);
}
return files;
}
public static void writeOutZipEntry(ZipFile zipFile, ZipEntry entry,
File outputDir, String outputFileName) throws IOException {
InputStream inputStream = null;
try {
inputStream = zipFile.getInputStream(entry);
writeOutInputStream(outputDir, outputFileName, inputStream);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
public static void writeOutInputStream(File outputDir, String outputFileName,
InputStream inputStream) throws IOException {
BufferedOutputStream output = null;
try {
File file = new File(outputDir, outputFileName);
output = new BufferedOutputStream(
new FileOutputStream(file));
BufferedInputStream input = new BufferedInputStream(inputStream);
byte b[] = new byte[8192];
int n;
while ((n = input.read(b, 0, 8192)) >= 0) {
output.write(b, 0, n);
}
} finally {
if (output != null) {
output.close();
}
}
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/ODexBloc.java
================================================
/*
* 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.manager.installplugin;
import android.os.Build;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import dalvik.system.DexClassLoader;
public class ODexBloc {
private static ConcurrentHashMap sLocks = new ConcurrentHashMap<>();
/**
* DexClassLoader的optimizedDirectory参数从API 27起就无效了
* 此方法统一判断这一特性是否生效
*
* @return true表示ODexBloc还有作用
*/
public static boolean isEffective() {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1;
}
public static void oDexPlugin(File apkFile, File oDexDir, File copiedTagFile) throws InstallPluginException {
if (!isEffective()) {
return;
}
String key = apkFile.getAbsolutePath();
Object lock = sLocks.get(key);
if (lock == null) {
lock = new Object();
sLocks.put(key, lock);
}
synchronized (lock) {
if (copiedTagFile.exists()) {
return;
}
//如果odex目录存在但是个文件,不是目录,那超出预料了。删除了也不一定能工作正常。
if (oDexDir.exists() && oDexDir.isFile()) {
throw new InstallPluginException("oDexDir=" + oDexDir.getAbsolutePath() + "已存在,但它是个文件,不敢贸然删除");
}
//创建oDex目录
oDexDir.mkdirs();
new DexClassLoader(apkFile.getAbsolutePath(), oDexDir.getAbsolutePath(), null, ODexBloc.class.getClassLoader());
try {
copiedTagFile.createNewFile();
} catch (IOException e) {
throw new InstallPluginException("oDexPlugin完毕 创建tag文件失败:" + copiedTagFile.getAbsolutePath(), e);
}
}
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/PluginConfig.java
================================================
/*
* 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.manager.installplugin;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public class PluginConfig {
/**
* 配置json文件的格式版本号
*/
public int version;
/**
* 配置json文件的格式兼容版本号
*/
public int[] compact_version;
/**
* 标识一次插件发布的id
*/
public String UUID;
/**
* 标识一次插件发布的id,可以使用自定义格式描述版本信息
*/
public String UUID_NickName;
/**
* pluginLoaderAPk 文件信息
*/
public FileInfo pluginLoader;
/**
* runtime 文件信息
*/
public FileInfo runTime;
/**
* 业务插件 key: partKey value:文件信息
*/
public Map plugins = new HashMap<>();
/**
* 插件的存储目录
*/
public File storageDir;
public boolean isUnpacked() {
boolean pluginLoaderUnpacked = true;
if (pluginLoader != null) {
pluginLoaderUnpacked = pluginLoader.file.exists();
}
boolean runtimeUnpacked = true;
if (runTime != null) {
runtimeUnpacked = runTime.file.exists();
}
boolean pluginsUnpacked = true;
for (PluginFileInfo pluginFileInfo : plugins.values()) {
pluginsUnpacked = pluginsUnpacked && pluginFileInfo.file.exists();
}
return pluginLoaderUnpacked && runtimeUnpacked && pluginsUnpacked;
}
public static class FileInfo {
public final File file;
public final String hash;
FileInfo(File file, String hash) {
this.file = file;
this.hash = hash;
}
}
public static class PluginFileInfo extends FileInfo {
final String[] dependsOn;
final String[] hostWhiteList;
final String businessName;
PluginFileInfo(String businessName, FileInfo fileInfo, String[] dependsOn, String[] hostWhiteList) {
this(businessName, fileInfo.file, fileInfo.hash, dependsOn, hostWhiteList);
}
PluginFileInfo(String businessName, File file, String hash, String[] dependsOn, String[] hostWhiteList) {
super(file, hash);
this.businessName = businessName;
this.dependsOn = dependsOn;
this.hostWhiteList = hostWhiteList;
}
}
public static PluginConfig parseFromJson(JSONObject configJson, File storageDir) throws JSONException {
PluginConfig pluginConfig = new PluginConfig();
pluginConfig.version = configJson.getInt("version");
JSONArray compact_version_json = configJson.optJSONArray("compact_version");
if (compact_version_json != null && compact_version_json.length() > 0) {
pluginConfig.compact_version = new int[compact_version_json.length()];
for (int i = 0; i < compact_version_json.length(); i++) {
pluginConfig.compact_version[i] = compact_version_json.getInt(i);
}
}
//todo #27 json的版本检查和不兼容检查
pluginConfig.UUID = configJson.getString("UUID");
pluginConfig.UUID_NickName = configJson.getString("UUID_NickName");
JSONObject loaderJson = configJson.optJSONObject("pluginLoader");
if (loaderJson != null) {
pluginConfig.pluginLoader = getFileInfo(loaderJson, storageDir);
}
JSONObject runtimeJson = configJson.optJSONObject("runtime");
if (runtimeJson != null) {
pluginConfig.runTime = getFileInfo(runtimeJson, storageDir);
}
JSONArray pluginArray = configJson.optJSONArray("plugins");
if (pluginArray != null && pluginArray.length() > 0) {
for (int i = 0; i < pluginArray.length(); i++) {
JSONObject plugin = pluginArray.getJSONObject(i);
String partKey = plugin.getString("partKey");
pluginConfig.plugins.put(partKey, getPluginFileInfo(plugin, storageDir));
}
}
pluginConfig.storageDir = storageDir;
return pluginConfig;
}
private static FileInfo getFileInfo(JSONObject jsonObject, File storageDir) throws JSONException {
String name = jsonObject.getString("apkName");
String hash = jsonObject.getString("hash");
return new FileInfo(new File(storageDir, name), hash);
}
private static PluginFileInfo getPluginFileInfo(JSONObject jsonObject, File storageDir) throws JSONException {
String businessName = jsonObject.optString("businessName", "");
FileInfo fileInfo = getFileInfo(jsonObject, storageDir);
String[] dependsOn = getArrayStringByName(jsonObject, "dependsOn");
String[] hostWhiteList = getArrayStringByName(jsonObject, "hostWhiteList");
return new PluginFileInfo(businessName, fileInfo, dependsOn, hostWhiteList);
}
private static String[] getArrayStringByName(JSONObject jsonObject, String name) throws JSONException {
JSONArray jsonArray = jsonObject.optJSONArray(name);
String[] dependsOn;
if (jsonArray != null) {
dependsOn = new String[jsonArray.length()];
for (int i = 0; i < jsonArray.length(); i++) {
dependsOn[i] = jsonArray.getString(i);
}
} else {
dependsOn = new String[]{};
}
return dependsOn;
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/SafeZipFile.java
================================================
/*
* 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.manager.installplugin;
import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* 避免ZipperDown漏洞
*/
public class SafeZipFile extends ZipFile {
public SafeZipFile(File file) throws IOException {
super(file);
}
@Override
public Enumeration extends ZipEntry> entries() {
return new SafeZipEntryIterator(super.entries());
}
private static class SafeZipEntryIterator implements Enumeration {
final private Enumeration extends ZipEntry> delegate;
private SafeZipEntryIterator(Enumeration extends ZipEntry> delegate) {
this.delegate = delegate;
}
@Override
public boolean hasMoreElements() {
return delegate.hasMoreElements();
}
@Override
public ZipEntry nextElement() {
ZipEntry entry = delegate.nextElement();
if (null != entry) {
String name = entry.getName();
if (null != name && (name.contains("../") || name.contains("..\\"))) {
throw new SecurityException("非法entry路径:" + entry.getName());
}
}
return entry;
}
}
}
================================================
FILE: projects/sdk/core/manager/src/main/java/com/tencent/shadow/core/manager/installplugin/UnpackManager.java
================================================
/*
* 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.manager.installplugin;
import static com.tencent.shadow.core.utils.Md5.md5File;
import com.tencent.shadow.core.common.Logger;
import com.tencent.shadow.core.common.LoggerFactory;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class UnpackManager {
private static final Logger mLogger = LoggerFactory.getLogger(UnpackManager.class);
private static final String CONFIG_FILENAME = "config.json";//todo #28 json的格式需要沉淀文档。
private static final String DEFAULT_STORE_DIR_NAME = "ShadowPluginManager";
private final File mPluginUnpackedDir;
private final String mAppName;
public UnpackManager(File root, String appName) {
File parent = new File(root, DEFAULT_STORE_DIR_NAME);
mPluginUnpackedDir = new File(parent, "UnpackedPlugin");
mPluginUnpackedDir.mkdirs();
mAppName = appName;
}
File getVersionDir(String appHash) {
return AppCacheFolderManager.getVersionDir(mPluginUnpackedDir, mAppName, appHash);
}
public File getAppDir() {
return AppCacheFolderManager.getAppDir(mPluginUnpackedDir, mAppName);
}
/**
* 获取插件解包的目标目录。根据target的文件名决定。
*
* @param target Target
* @return 插件解包的目标目录
*/
public File getPluginUnpackDir(String appHash, File target) {
return new File(getVersionDir(appHash), target.getName());
}
public String zipHash(File zip) {
return md5File(zip);
}
public JSONObject getConfigJson(File zip) {
ZipFile zipFile = null;
try {
zipFile = new SafeZipFile(zip);
ZipEntry entry = zipFile.getEntry(CONFIG_FILENAME);
InputStream inputStream = zipFile.getInputStream(entry);
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder sb = new StringBuilder();
for (String line; (line = br.readLine()) != null; ) {
sb.append(line);
}
return new JSONObject(sb.toString());
} catch (JSONException | IOException e) {
throw new RuntimeException(e);
} finally {
try {
if (zipFile != null) {
zipFile.close();
}
} catch (IOException e) {
mLogger.warn("zip关闭时出错忽略", e);
}
}
}
/**
* 解包一个下载好的插件
*
* @param target 插件包
* @param pluginUnpackDir 解压目录
*/
public void unpackPlugin(File target, File pluginUnpackDir) throws IOException {
pluginUnpackDir.mkdirs();
MinFileUtils.cleanDirectory(pluginUnpackDir);
ZipFile zipFile = null;
try {
zipFile = new SafeZipFile(target);
Enumeration extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory()) {
MinFileUtils.writeOutZipEntry(zipFile, entry, pluginUnpackDir, entry.getName());
}
}
} finally {
try {
if (zipFile != null) {
zipFile.close();
}
} catch (IOException e) {
mLogger.warn("zip关闭时出错忽略", e);
}
}
}
}
================================================
FILE: projects/sdk/core/manager/src/test/java/com/tencent/shadow/core/manager/installplugin/SafeZipFileTest.java
================================================
package com.tencent.shadow.core.manager.installplugin;
import com.tencent.shadow.core.utils.Md5;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
public class SafeZipFileTest {
private File testZipFile;
private ZipOutputStream zipOutputStream;
@Before
public void setUp() throws Exception {
testZipFile = File.createTempFile("SafeZipFileTest", ".zip");
zipOutputStream = new ZipOutputStream(new FileOutputStream(testZipFile));
}
@After
public void tearDown() throws Exception {
zipOutputStream.close();
boolean success = testZipFile.delete();
if (!success) {
throw new RuntimeException("删除临时文件失败");
}
}
@Test
public void testContainsManifest() throws IOException {
//向测试zip中写入一个文件
ZipOutputStream out = this.zipOutputStream;
ZipEntry e = new ZipEntry("META-INF/MANIFEST.MF");
out.putNextEntry(e);
byte[] data = "测试内容".getBytes();
out.write(data, 0, data.length);
out.closeEntry();
out.close();
//测试用SafeZipFile类型遍历zip文件
ZipFile zipFile
= new SafeZipFile(testZipFile);
Enumeration extends ZipEntry> entries = zipFile.entries();
if (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String entryName = entry.getName();
Assert.assertTrue(entryName.length() > 0);
}
zipFile.close();
}
}
================================================
FILE: projects/sdk/core/manager-db-test/.gitignore
================================================
/build
*.iml
================================================
FILE: projects/sdk/core/manager-db-test/build.gradle
================================================
apply plugin: 'com.android.library'
android {
defaultConfig {
compileSdkVersion project.COMPILE_SDK_VERSION
minSdkVersion project.MIN_SDK_VERSION
targetSdkVersion project.TARGET_SDK_VERSION
testInstrumentationRunner "com.tencent.shadow.core.pluginmanager.CustomAndroidJUnitRunner"
}
testOptions {
animationsDisabled = true
}
}
dependencies {
testImplementation "junit:junit:$junit_version"
androidTestImplementation "androidx.test:core:$androidx_test_version"
androidTestImplementation "androidx.test.ext:junit:$androidx_test_junit_version"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-remote:$espresso_version"
implementation "androidx.test.espresso:espresso-idling-resource:$espresso_version"
androidTestImplementation "androidx.test:runner:$androidx_test_version"
androidTestImplementation "commons-io:commons-io:$commons_io_android_version"
androidTestImplementation project(':common')
implementation project(':manager')
}
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/java/com/tencent/shadow/core/manager/installplugin/DbCompatibilityTest.java
================================================
/*
* 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.manager.installplugin;
import static com.tencent.shadow.core.manager.installplugin.InstalledPluginDBHelper.DB_NAME_PREFIX;
import static com.tencent.shadow.core.manager_aar.test.R.raw;
import android.content.Context;
import android.os.Build;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* 数据库兼容性测试
*
* 这项测试在写测试用例前,先回滚代码到旧版本,利用正常的config.json(如res/raw/plugin1.json)通过旧版本的
* InstalledDao安装,生成旧版本的数据库。然后用{@link DbCompatibilityTest#dumpDbToFile(File)}方法
* 将该旧数据库导出成sql文件上库(如res/raw/init_sql_version1.sql)。
* 注意:PRAGMA user_version不能dump出来,需要手工写。
* 再准备一个升级过后数据库dump出来的sql(如res/raw/expect_sql_version1.sql)作为Assert expected。
*/
@RunWith(AndroidJUnit4.class)
public class DbCompatibilityTest {
private static final String TEST_DB_NAME = "DbCompatibilityTest";
private File databasePath;
private Context context;
@Before
public void setUp() {
//因为sqlite3本身的bug和版本不一致导致dump结果不一致,这个单元测试只能在Android P上完整正常运行
//Android 4.4系统在init时会出现内部错误
//Android 7.0系统dump出的sql对表名加了额外的双引号,兼容略麻烦
Assume.assumeTrue(Build.VERSION.SDK_INT == Build.VERSION_CODES.P);
context = ApplicationProvider.getApplicationContext();
databasePath = context.getDatabasePath(
InstalledPluginDBHelper.DB_NAME_PREFIX + TEST_DB_NAME
);
}
@Test
public void testDbNamePrefixNotChanged() {
Assert.assertEquals("DB名前缀不能改",
"shadow_installed_plugin_db",
DB_NAME_PREFIX
);
}
@Test
public void testCompatibleWithVersion1()
throws IOException, InterruptedException, TimeoutException {
testCompatibleWithVersion(
raw.init_sql_version1,
raw.expect_sql_version1
);
}
@Test
public void testCompatibleWithVersion2()
throws IOException, InterruptedException, TimeoutException {
testCompatibleWithVersion(
raw.init_sql_version2,
raw.expect_sql_version2
);
}
@Test
public void testCompatibleWithVersion3()
throws IOException, InterruptedException, TimeoutException {
testCompatibleWithVersion(
raw.init_sql_version3,
raw.expect_sql_version3
);
}
@Test
public void testCompatibleWithVersion4()
throws IOException, InterruptedException, TimeoutException {
testCompatibleWithVersion(
raw.init_sql_version4,
raw.expect_sql_version4
);
}
// @Test //取消注释以生成文件
public void generateCurrentVersionInitSqlFile() throws Exception {
initDbWithConfigJson(raw.plugin1);
}
private void initDbWithConfigJson(int... configJsonResId)
throws IOException, JSONException, TimeoutException, InterruptedException {
List configs = new LinkedList<>();
for (int resId : configJsonResId) {
File configJsonFile = getResRawFile(resId);
String configJson = FileUtils.readFileToString(configJsonFile, Charset.defaultCharset());
FileUtils.forceDelete(configJsonFile);
configs.add(configJson);
}
InstalledPluginDBHelper dbHelper = new InstalledPluginDBHelper(context, TEST_DB_NAME);
InstalledDao installedDao = new InstalledDao(dbHelper);
for (String configJson : configs) {
installedDao.insert(PluginConfig.parseFromJson(new JSONObject(configJson), context.getCacheDir()), null, null);
}
dbHelper.close();
File dumpSqlFile = File.createTempFile("initDbWithConfigJson", ".sql", context.getExternalCacheDir());
dumpDbToFile(dumpSqlFile);
Assert.assertTrue(dumpSqlFile.exists() && dumpSqlFile.length() > 0);
throw new RuntimeException("执行命令复制文件到电脑:adb pull " + dumpSqlFile.getAbsolutePath());
}
private void testCompatibleWithVersion(int initSqlResId, int expectSqlResId)
throws IOException, InterruptedException, TimeoutException {
File initSqlFile = getResRawFile(initSqlResId);
initDb(initSqlFile);
InstalledPluginDBHelper dbHelper = new InstalledPluginDBHelper(context, TEST_DB_NAME);
dbHelper.getReadableDatabase();//只是为了触发onUpgrade方法
dbHelper.close();
File dumpSqlFile = File.createTempFile("dumpDb", ".sql");
dumpDbToFile(dumpSqlFile);
File exceptSqlFile = getResRawFile(expectSqlResId);
try {
Assert.assertEquals(
FileUtils.readLines(exceptSqlFile, Charset.defaultCharset()),
FileUtils.readLines(dumpSqlFile, Charset.defaultCharset())
);
} finally {
FileUtils.forceDelete(dumpSqlFile);
FileUtils.forceDelete(initSqlFile);
FileUtils.forceDelete(exceptSqlFile);
deleteDb();
}
}
private File getResRawFile(int resId) throws IOException {
InputStream is = context.getResources().openRawResource(resId);
File resRawFile = File.createTempFile("getResRawFile", null);
IOUtils.copy(is, new FileOutputStream(resRawFile));
return resRawFile;
}
/**
* 初始化数据库
* 用于将数据库通过sql脚本直接初始化成旧版本数据库。
* 旧版本数据库的sql采用手工执行sqlite3的.dump命令生成的。
*/
private void initDb(File initSqlFile)
throws IOException, InterruptedException, TimeoutException {
String[] cmd = {
"sqlite3",
"-init",
initSqlFile.getAbsolutePath(),
databasePath.getAbsolutePath(),
".exit"
};
Process p = Runtime.getRuntime().exec(cmd);
boolean timeout = !p.waitFor(10, TimeUnit.SECONDS);
if (timeout) {
throw new TimeoutException("exec超时");
}
int exitValue = p.exitValue();
if (exitValue != 0) {
String errorOutput = IOUtils.toString(p.getErrorStream(), Charset.defaultCharset());
throw new Error("exitValue==" + exitValue + " errorOutput==" + errorOutput);
}
}
/**
* 将当前数据库Dump成sql文件
*/
private void dumpDbToFile(File dumpSqlFile)
throws InterruptedException, IOException, TimeoutException {
File tempDumpCmdArgsFile = File.createTempFile("dumpDb", ".cmd");
PrintWriter pw = new PrintWriter(new FileWriter(tempDumpCmdArgsFile));
pw.println(".output " + dumpSqlFile.getAbsolutePath());
pw.println(".dump " + InstalledPluginDBHelper.TABLE_NAME_MANAGER);
pw.println(".exit");
pw.flush();
pw.close();
String[] cmd = {
"sqlite3",
databasePath.getAbsolutePath(),
".read " + tempDumpCmdArgsFile.getAbsolutePath()
};
Process p = Runtime.getRuntime().exec(cmd, null, context.getCacheDir());
boolean timeout = !p.waitFor(10, TimeUnit.SECONDS);
FileUtils.forceDelete(tempDumpCmdArgsFile);
if (timeout) {
throw new TimeoutException("exec超时");
}
int exitValue = p.exitValue();
if (exitValue != 0) {
String errorOutput = IOUtils.toString(p.getErrorStream(), Charset.defaultCharset());
throw new Error("exitValue==" + exitValue + " errorOutput==" + errorOutput);
}
Assert.assertTrue(dumpSqlFile.exists() && dumpSqlFile.length() > 0);
}
/**
* 删除数据库。
* 应该在每一个测试用例最后调用此方法删除数据库,避免对下一个测试用例有影响。
*/
private void deleteDb() throws IOException {
FileUtils.forceDelete(databasePath);
}
}
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/java/com/tencent/shadow/core/pluginmanager/CustomAndroidJUnitRunner.java
================================================
/*
* 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.pluginmanager;
import android.os.Bundle;
import androidx.test.runner.AndroidJUnitRunner;
public class CustomAndroidJUnitRunner extends AndroidJUnitRunner {
@Override
public void onCreate(Bundle arguments) {
//禁止Google收集数据,避免因访问不到在测试结束后等待40秒超时
arguments.putString("disableAnalytics", Boolean.toString(true));
super.onCreate(arguments);
}
}
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/java/common/AndroidLogLoggerFactory.java
================================================
/*
* 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 common;
import android.util.Log;
import com.tencent.shadow.core.common.ILoggerFactory;
import com.tencent.shadow.core.common.Logger;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class AndroidLogLoggerFactory implements ILoggerFactory {
private static final int LOG_LEVEL_TRACE = 5;
private static final int LOG_LEVEL_DEBUG = 4;
private static final int LOG_LEVEL_INFO = 3;
private static final int LOG_LEVEL_WARN = 2;
private static final int LOG_LEVEL_ERROR = 1;
private static AndroidLogLoggerFactory sInstance = new AndroidLogLoggerFactory();
public static ILoggerFactory getInstance() {
return sInstance;
}
final private ConcurrentMap loggerMap = new ConcurrentHashMap();
public Logger getLogger(String name) {
Logger simpleLogger = loggerMap.get(name);
if (simpleLogger != null) {
return simpleLogger;
} else {
Logger newInstance = new IVLogger(name);
Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
class IVLogger implements Logger {
private String name;
IVLogger(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
private void log(int level, String message, Throwable t) {
final String tag = String.valueOf(name);
switch (level) {
case LOG_LEVEL_TRACE:
case LOG_LEVEL_DEBUG:
if (t == null)
Log.d(tag, message);
else
Log.d(tag, message, t);
break;
case LOG_LEVEL_INFO:
if (t == null)
Log.i(tag, message);
else
Log.i(tag, message, t);
break;
case LOG_LEVEL_WARN:
if (t == null)
Log.w(tag, message);
else
Log.w(tag, message, t);
break;
case LOG_LEVEL_ERROR:
if (t == null)
Log.e(tag, message);
else
Log.e(tag, message, t);
break;
default:
break;
}
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public void trace(String msg) {
log(LOG_LEVEL_TRACE, msg, null);
}
@Override
public void trace(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_TRACE, tuple.getMessage(), null);
}
@Override
public void trace(String msg, Throwable throwable) {
log(LOG_LEVEL_TRACE, msg, throwable);
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public void debug(String msg) {
log(LOG_LEVEL_DEBUG, msg, null);
}
@Override
public void debug(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_DEBUG, tuple.getMessage(), null);
}
@Override
public void debug(String msg, Throwable throwable) {
log(LOG_LEVEL_DEBUG, msg, throwable);
}
@Override
public boolean isInfoEnabled() {
return true;
}
@Override
public void info(String msg) {
log(LOG_LEVEL_INFO, msg, null);
}
@Override
public void info(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_INFO, tuple.getMessage(), null);
}
@Override
public void info(String msg, Throwable throwable) {
log(LOG_LEVEL_INFO, msg, throwable);
}
@Override
public boolean isWarnEnabled() {
return true;
}
@Override
public void warn(String msg) {
log(LOG_LEVEL_WARN, msg, null);
}
@Override
public void warn(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_WARN, tuple.getMessage(), null);
}
@Override
public void warn(String msg, Throwable throwable) {
log(LOG_LEVEL_WARN, msg, throwable);
}
@Override
public boolean isErrorEnabled() {
return true;
}
@Override
public void error(String msg) {
log(LOG_LEVEL_ERROR, msg, null);
}
@Override
public void error(String format, Object o) {
FormattingTuple tuple = MessageFormatter.format(format, o);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String format, Object o, Object o1) {
FormattingTuple tuple = MessageFormatter.format(format, o, o1);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String format, Object... objects) {
FormattingTuple tuple = MessageFormatter.arrayFormat(format, objects);
log(LOG_LEVEL_ERROR, tuple.getMessage(), null);
}
@Override
public void error(String msg, Throwable throwable) {
log(LOG_LEVEL_ERROR, msg, throwable);
}
}
}
class FormattingTuple {
static public FormattingTuple NULL = new FormattingTuple(null);
private String message;
private Throwable throwable;
private Object[] argArray;
public FormattingTuple(String message) {
this(message, null, null);
}
public FormattingTuple(String message, Object[] argArray, Throwable throwable) {
this.message = message;
this.throwable = throwable;
this.argArray = argArray;
}
public String getMessage() {
return message;
}
public Object[] getArgArray() {
return argArray;
}
public Throwable getThrowable() {
return throwable;
}
}
final class MessageFormatter {
static final char DELIM_START = '{';
static final char DELIM_STOP = '}';
static final String DELIM_STR = "{}";
private static final char ESCAPE_CHAR = '\\';
/**
* Performs single argument substitution for the 'messagePattern' passed as
* parameter.
*
* For example,
*
*
* MessageFormatter.format("Hi {}.", "there");
*
*
* will return the string "Hi there.".
*
*
* @param messagePattern The message pattern which will be parsed and formatted
* @param arg The argument to be substituted in place of the formatting anchor
* @return The formatted message
*/
final public static FormattingTuple format(String messagePattern, Object arg) {
return arrayFormat(messagePattern, new Object[]{arg});
}
/**
* Performs a two argument substitution for the 'messagePattern' passed as
* parameter.
*
* For example,
*
*
* MessageFormatter.format("Hi {}. My name is {}.", "Alice", "Bob");
*
*
* will return the string "Hi Alice. My name is Bob.".
*
* @param messagePattern The message pattern which will be parsed and formatted
* @param arg1 The argument to be substituted in place of the first formatting
* anchor
* @param arg2 The argument to be substituted in place of the second formatting
* anchor
* @return The formatted message
*/
final public static FormattingTuple format(final String messagePattern, Object arg1, Object arg2) {
return arrayFormat(messagePattern, new Object[]{arg1, arg2});
}
static final Throwable getThrowableCandidate(Object[] argArray) {
if (argArray == null || argArray.length == 0) {
return null;
}
final Object lastEntry = argArray[argArray.length - 1];
if (lastEntry instanceof Throwable) {
return (Throwable) lastEntry;
}
return null;
}
final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) {
Throwable throwableCandidate = getThrowableCandidate(argArray);
Object[] args = argArray;
if (throwableCandidate != null) {
args = trimmedCopy(argArray);
}
return arrayFormat(messagePattern, args, throwableCandidate);
}
private static Object[] trimmedCopy(Object[] argArray) {
if (argArray == null || argArray.length == 0) {
throw new IllegalStateException("non-sensical empty or null argument array");
}
final int trimemdLen = argArray.length - 1;
Object[] trimmed = new Object[trimemdLen];
System.arraycopy(argArray, 0, trimmed, 0, trimemdLen);
return trimmed;
}
final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray, Throwable throwable) {
if (messagePattern == null) {
return new FormattingTuple(null, argArray, throwable);
}
if (argArray == null) {
return new FormattingTuple(messagePattern);
}
int i = 0;
int j;
// use string builder for better multicore performance
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
int L;
for (L = 0; L < argArray.length; L++) {
j = messagePattern.indexOf(DELIM_STR, i);
if (j == -1) {
// no more variables
if (i == 0) { // this is a simple string
return new FormattingTuple(messagePattern, argArray, throwable);
} else { // add the tail string which contains no variables and return
// the result.
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
} else {
if (isEscapedDelimeter(messagePattern, j)) {
if (!isDoubleEscaped(messagePattern, j)) {
L--; // DELIM_START was escaped, thus should not be incremented
sbuf.append(messagePattern, i, j - 1);
sbuf.append(DELIM_START);
i = j + 1;
} else {
// The escape character preceding the delimiter start is
// itself escaped: "abc x:\\{}"
// we have to consume one backward slash
sbuf.append(messagePattern, i, j - 1);
deeplyAppendParameter(sbuf, argArray[L], new HashMap());
i = j + 2;
}
} else {
// normal case
sbuf.append(messagePattern, i, j);
deeplyAppendParameter(sbuf, argArray[L], new HashMap());
i = j + 2;
}
}
}
// append the characters following the last {} pair.
sbuf.append(messagePattern, i, messagePattern.length());
return new FormattingTuple(sbuf.toString(), argArray, throwable);
}
final static boolean isEscapedDelimeter(String messagePattern, int delimeterStartIndex) {
if (delimeterStartIndex == 0) {
return false;
}
char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1);
if (potentialEscape == ESCAPE_CHAR) {
return true;
} else {
return false;
}
}
final static boolean isDoubleEscaped(String messagePattern, int delimeterStartIndex) {
if (delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR) {
return true;
} else {
return false;
}
}
// special treatment of array values was suggested by 'lizongbo'
private static void deeplyAppendParameter(StringBuilder sbuf, Object o, Map seenMap) {
if (o == null) {
sbuf.append("null");
return;
}
if (!o.getClass().isArray()) {
safeObjectAppend(sbuf, o);
} else {
// check for primitive array types because they
// unfortunately cannot be cast to Object[]
if (o instanceof boolean[]) {
booleanArrayAppend(sbuf, (boolean[]) o);
} else if (o instanceof byte[]) {
byteArrayAppend(sbuf, (byte[]) o);
} else if (o instanceof char[]) {
charArrayAppend(sbuf, (char[]) o);
} else if (o instanceof short[]) {
shortArrayAppend(sbuf, (short[]) o);
} else if (o instanceof int[]) {
intArrayAppend(sbuf, (int[]) o);
} else if (o instanceof long[]) {
longArrayAppend(sbuf, (long[]) o);
} else if (o instanceof float[]) {
floatArrayAppend(sbuf, (float[]) o);
} else if (o instanceof double[]) {
doubleArrayAppend(sbuf, (double[]) o);
} else {
objectArrayAppend(sbuf, (Object[]) o, seenMap);
}
}
}
private static void safeObjectAppend(StringBuilder sbuf, Object o) {
try {
String oAsString = o.toString();
sbuf.append(oAsString);
} catch (Throwable t) {
sbuf.append("[FAILED toString()]");
}
}
private static void objectArrayAppend(StringBuilder sbuf, Object[] a, Map seenMap) {
sbuf.append('[');
if (!seenMap.containsKey(a)) {
seenMap.put(a, null);
final int len = a.length;
for (int i = 0; i < len; i++) {
deeplyAppendParameter(sbuf, a[i], seenMap);
if (i != len - 1)
sbuf.append(", ");
}
// allow repeats in siblings
seenMap.remove(a);
} else {
sbuf.append("...");
}
sbuf.append(']');
}
private static void booleanArrayAppend(StringBuilder sbuf, boolean[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void byteArrayAppend(StringBuilder sbuf, byte[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void charArrayAppend(StringBuilder sbuf, char[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void shortArrayAppend(StringBuilder sbuf, short[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void intArrayAppend(StringBuilder sbuf, int[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void longArrayAppend(StringBuilder sbuf, long[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void floatArrayAppend(StringBuilder sbuf, float[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
private static void doubleArrayAppend(StringBuilder sbuf, double[] a) {
sbuf.append('[');
final int len = a.length;
for (int i = 0; i < len; i++) {
sbuf.append(a[i]);
if (i != len - 1)
sbuf.append(", ");
}
sbuf.append(']');
}
}
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/res/raw/expect_sql_version1.sql
================================================
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE shadowPluginManager ( id INTEGER PRIMARY KEY AUTOINCREMENT,hash VARCHAR , filePath VARCHAR, type INTEGER, partKey VARCHAR, dependsOn VARCHAR, uuid VARCHAR, version VARCHAR, installedTime INTEGER ,odexPath VARCHAR ,libPath VARCHAR , hostWhiteList VARCHAR, businessName VARCHAR);
INSERT INTO shadowPluginManager VALUES(1,'B0358A1919582A0FD467A42EEC40A5B7','/data/user/0/com.tencent.shadow.core.manager.test/cache/loader-apk-debug.apk',3,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(2,'51FDE1246F62D17D881493B037795D63','/data/user/0/com.tencent.shadow.core.manager.test/cache/runtime-apk-debug.apk',4,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(3,'DAC0234D1BE7F363A66263A01595DA64','/data/user/0/com.tencent.shadow.core.manager.test/cache/test-plugin-debug.apk',1,'test_main','[]','0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(4,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16',5,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
COMMIT;
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/res/raw/expect_sql_version2.sql
================================================
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE shadowPluginManager ( id INTEGER PRIMARY KEY AUTOINCREMENT,hash VARCHAR , filePath VARCHAR, type INTEGER, partKey VARCHAR, dependsOn VARCHAR, uuid VARCHAR, version VARCHAR, installedTime INTEGER ,odexPath VARCHAR ,libPath VARCHAR , hostWhiteList VARCHAR, businessName VARCHAR);
INSERT INTO shadowPluginManager VALUES(1,'B0358A1919582A0FD467A42EEC40A5B7','/data/user/0/com.tencent.shadow.core.manager.test/cache/loader-apk-debug.apk',3,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(2,'51FDE1246F62D17D881493B037795D63','/data/user/0/com.tencent.shadow.core.manager.test/cache/runtime-apk-debug.apk',4,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(3,'DAC0234D1BE7F363A66263A01595DA64','/data/user/0/com.tencent.shadow.core.manager.test/cache/test-plugin-debug.apk',1,'test_main','[depends1]','0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(4,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16',5,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
COMMIT;
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/res/raw/expect_sql_version3.sql
================================================
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE shadowPluginManager ( id INTEGER PRIMARY KEY AUTOINCREMENT,hash VARCHAR , filePath VARCHAR, type INTEGER, partKey VARCHAR, dependsOn VARCHAR, uuid VARCHAR, version VARCHAR, installedTime INTEGER ,odexPath VARCHAR ,libPath VARCHAR , hostWhiteList VARCHAR, businessName VARCHAR);
INSERT INTO shadowPluginManager VALUES(1,'B0358A1919582A0FD467A42EEC40A5B7','/data/user/0/com.tencent.shadow.core.manager.test/cache/loader-apk-debug.apk',3,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(2,'51FDE1246F62D17D881493B037795D63','/data/user/0/com.tencent.shadow.core.manager.test/cache/runtime-apk-debug.apk',4,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(3,'DAC0234D1BE7F363A66263A01595DA64','/data/user/0/com.tencent.shadow.core.manager.test/cache/test-plugin-debug.apk',1,'test_main','[depends1]','0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,'[host1]',NULL);
INSERT INTO shadowPluginManager VALUES(4,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16',5,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL,NULL);
COMMIT;
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/res/raw/expect_sql_version4.sql
================================================
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE shadowPluginManager ( id INTEGER PRIMARY KEY AUTOINCREMENT,hash VARCHAR , filePath VARCHAR, type INTEGER, businessName VARCHAR, partKey VARCHAR, dependsOn VARCHAR, uuid VARCHAR, version VARCHAR, installedTime INTEGER ,odexPath VARCHAR ,libPath VARCHAR ,hostWhiteList VARCHAR );
INSERT INTO shadowPluginManager VALUES(1,'B0358A1919582A0FD467A42EEC40A5B7','/data/user/0/com.tencent.shadow.core.manager.test/cache/loader-apk-debug.apk',3,NULL,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556448760000,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(2,'51FDE1246F62D17D881493B037795D63','/data/user/0/com.tencent.shadow.core.manager.test/cache/runtime-apk-debug.apk',4,NULL,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556448760000,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(3,'DAC0234D1BE7F363A66263A01595DA64','/data/user/0/com.tencent.shadow.core.manager.test/cache/test-plugin-debug.apk',1,'businesstest','test_main','[]','0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556448760000,NULL,NULL,'[]');
INSERT INTO shadowPluginManager VALUES(4,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16',5,NULL,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556448760000,NULL,NULL,NULL);
COMMIT;
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/res/raw/init_sql_version1.sql
================================================
/*
* version:1的数据库
* 行1-4是没有interface的插件
* 行5-9是有interface的插件
*/
PRAGMA foreign_keys=OFF;
PRAGMA user_version = 1;
BEGIN TRANSACTION;
CREATE TABLE android_metadata (locale TEXT);
INSERT INTO android_metadata VALUES('en_US');
CREATE TABLE shadowPluginManager ( id INTEGER PRIMARY KEY AUTOINCREMENT,hash VARCHAR , filePath VARCHAR, type INTEGER, partKey VARCHAR, dependsOn VARCHAR, uuid VARCHAR, version VARCHAR, installedTime INTEGER ,odexPath VARCHAR ,libPath VARCHAR );
INSERT INTO shadowPluginManager VALUES(1,'B0358A1919582A0FD467A42EEC40A5B7','/data/user/0/com.tencent.shadow.core.manager.test/cache/loader-apk-debug.apk',3,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(2,'51FDE1246F62D17D881493B037795D63','/data/user/0/com.tencent.shadow.core.manager.test/cache/runtime-apk-debug.apk',4,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(3,'DAC0234D1BE7F363A66263A01595DA64','/data/user/0/com.tencent.shadow.core.manager.test/cache/test-plugin-debug.apk',1,'test_main','[]','0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(4,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16',5,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(5,'B0358A1919582A0FD467A42EEC40A5B7','/data/user/0/com.tencent.shadow.core.manager.test/cache/loader-apk-debug.apk',3,NULL,NULL,'1087DACB-3373-4E11-B50B-1B3076BD4F17','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(6,'51FDE1246F62D17D881493B037795D63','/data/user/0/com.tencent.shadow.core.manager.test/cache/runtime-apk-debug.apk',4,NULL,NULL,'1087DACB-3373-4E11-B50B-1B3076BD4F17','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(7,'DAC0234D1BE7F363A66263A01595DA64','/data/user/0/com.tencent.shadow.core.manager.test/cache/test-plugin-debug.apk',1,'test_main','[]','1087DACB-3373-4E11-B50B-1B3076BD4F17','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(8,'test_interface_fake_hash','/data/user/0/com.tencent.shadow.core.manager.test/cache/test_interface.apk',2,'test_interface','[]','1087DACB-3373-4E11-B50B-1B3076BD4F17','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(9,NULL,'1087DACB-3373-4E11-B50B-1B3076BD4F17',5,NULL,NULL,'1087DACB-3373-4E11-B50B-1B3076BD4F17','plugin_config_version1.json',1556258429000,NULL,NULL);
DELETE FROM sqlite_sequence;
INSERT INTO sqlite_sequence VALUES('shadowPluginManager',9);
COMMIT;
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/res/raw/init_sql_version2.sql
================================================
/*
* version:2的数据库
* 行1-4是一个插件
*/
PRAGMA foreign_keys=OFF;
PRAGMA user_version = 2;
BEGIN TRANSACTION;
CREATE TABLE android_metadata (locale TEXT);
INSERT INTO android_metadata VALUES('en_US');
CREATE TABLE shadowPluginManager ( id INTEGER PRIMARY KEY AUTOINCREMENT,hash VARCHAR , filePath VARCHAR, type INTEGER, partKey VARCHAR, dependsOn VARCHAR, uuid VARCHAR, version VARCHAR, installedTime INTEGER ,odexPath VARCHAR ,libPath VARCHAR );
INSERT INTO shadowPluginManager VALUES(1,'B0358A1919582A0FD467A42EEC40A5B7','/data/user/0/com.tencent.shadow.core.manager.test/cache/loader-apk-debug.apk',3,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(2,'51FDE1246F62D17D881493B037795D63','/data/user/0/com.tencent.shadow.core.manager.test/cache/runtime-apk-debug.apk',4,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(3,'DAC0234D1BE7F363A66263A01595DA64','/data/user/0/com.tencent.shadow.core.manager.test/cache/test-plugin-debug.apk',1,'test_main','[depends1]','0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(4,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16',5,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL);
DELETE FROM sqlite_sequence;
INSERT INTO sqlite_sequence VALUES('shadowPluginManager',4);
COMMIT;
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/res/raw/init_sql_version3.sql
================================================
/*
* version:3的数据库
* 行1-4是一个插件
*/
PRAGMA foreign_keys=OFF;
PRAGMA user_version = 3;
BEGIN TRANSACTION;
CREATE TABLE android_metadata (locale TEXT);
INSERT INTO android_metadata VALUES('en_US');
CREATE TABLE shadowPluginManager ( id INTEGER PRIMARY KEY AUTOINCREMENT,hash VARCHAR , filePath VARCHAR, type INTEGER, partKey VARCHAR, dependsOn VARCHAR, uuid VARCHAR, version VARCHAR, installedTime INTEGER ,odexPath VARCHAR ,libPath VARCHAR , hostWhiteList VARCHAR);
INSERT INTO shadowPluginManager VALUES(1,'B0358A1919582A0FD467A42EEC40A5B7','/data/user/0/com.tencent.shadow.core.manager.test/cache/loader-apk-debug.apk',3,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(2,'51FDE1246F62D17D881493B037795D63','/data/user/0/com.tencent.shadow.core.manager.test/cache/runtime-apk-debug.apk',4,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(3,'DAC0234D1BE7F363A66263A01595DA64','/data/user/0/com.tencent.shadow.core.manager.test/cache/test-plugin-debug.apk',1,'test_main','[depends1]','0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,'[host1]');
INSERT INTO shadowPluginManager VALUES(4,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16',5,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556258429000,NULL,NULL,NULL);
DELETE FROM sqlite_sequence;
INSERT INTO sqlite_sequence VALUES('shadowPluginManager',4);
COMMIT;
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/res/raw/init_sql_version4.sql
================================================
/*
* version:4的数据库
* 行1-4是一个插件
*/
PRAGMA foreign_keys=OFF;
PRAGMA user_version = 4;
BEGIN TRANSACTION;
CREATE TABLE shadowPluginManager ( id INTEGER PRIMARY KEY AUTOINCREMENT,hash VARCHAR , filePath VARCHAR, type INTEGER, businessName VARCHAR, partKey VARCHAR, dependsOn VARCHAR, uuid VARCHAR, version VARCHAR, installedTime INTEGER ,odexPath VARCHAR ,libPath VARCHAR ,hostWhiteList VARCHAR );
INSERT INTO shadowPluginManager VALUES(1,'B0358A1919582A0FD467A42EEC40A5B7','/data/user/0/com.tencent.shadow.core.manager.test/cache/loader-apk-debug.apk',3,NULL,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556448760000,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(2,'51FDE1246F62D17D881493B037795D63','/data/user/0/com.tencent.shadow.core.manager.test/cache/runtime-apk-debug.apk',4,NULL,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556448760000,NULL,NULL,NULL);
INSERT INTO shadowPluginManager VALUES(3,'DAC0234D1BE7F363A66263A01595DA64','/data/user/0/com.tencent.shadow.core.manager.test/cache/test-plugin-debug.apk',1,'businesstest','test_main','[]','0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556448760000,NULL,NULL,'[]');
INSERT INTO shadowPluginManager VALUES(4,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16',5,NULL,NULL,NULL,'0087DACB-3373-4E11-B50B-1B3076BD4F16','plugin_config_version1.json',1556448760000,NULL,NULL,NULL);
COMMIT;
================================================
FILE: projects/sdk/core/manager-db-test/src/androidTest/res/raw/plugin1.json
================================================
{
"compact_version": [
1
],
"pluginLoader": {
"apkName": "loader-apk-debug.apk",
"hash": "B0358A1919582A0FD467A42EEC40A5B7"
},
"plugins": [
{
"partKey": "test_main",
"apkName": "test-plugin-debug.apk",
"hash": "DAC0234D1BE7F363A66263A01595DA64"
}
],
"runtime": {
"apkName": "runtime-apk-debug.apk",
"hash": "51FDE1246F62D17D881493B037795D63"
},
"UUID": "0087DACB-3373-4E11-B50B-1B3076BD4F16",
"version": 1,
"UUID_NickName": "plugin_config_version1.json"
}
================================================
FILE: projects/sdk/core/manager-db-test/src/main/AndroidManifest.xml
================================================
================================================
FILE: projects/sdk/core/manifest-parser/.gitignore
================================================
/build
================================================
FILE: projects/sdk/core/manifest-parser/build.gradle
================================================
apply plugin: 'kotlin'
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.squareup:javapoet:$javapoet_version"
implementation project(':runtime')
testImplementation "junit:junit:$junit_version"
testImplementation "commons-io:commons-io:$commons_io_jvm_version"
testImplementation 'com.tencent.shadow.coding:java-build-config'
}
compileKotlin {
kotlinOptions {
jvmTarget = "1.6"
apiVersion = "1.3"// 兼容低版本Gradle和https://youtrack.jetbrains.com/issue/KT-39389
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "1.6"
}
}
================================================
FILE: projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestKeys.kt
================================================
package com.tencent.shadow.core.manifest_parser
sealed class AndroidManifestKeys {
companion object {
const val appComponentFactory = "android:appComponentFactory"
const val `package` = "package"
const val name = "android:name"
const val theme = "android:theme"
const val configChanges = "android:configChanges"
const val screenOrientation = "android:screenOrientation"
const val windowSoftInputMode = "android:windowSoftInputMode"
const val authorities = "android:authorities"
const val `intent-filter` = "intent-filter"
const val action = "action"
const val manifest = "manifest"
const val application = "application"
const val activity = "activity"
const val service = "service"
const val provider = "provider"
const val receiver = "receiver"
const val grantUriPermissions = "android:grantUriPermissions"
}
}
typealias ComponentMapKey = String
typealias ComponentMapValue = Any
typealias ComponentMap = Map
typealias MutableComponentMap = MutableMap
typealias ManifestValueParser = (String) -> String
================================================
FILE: projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestReader.kt
================================================
package com.tencent.shadow.core.manifest_parser
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
typealias ManifestMap = Map
/**
* 读取xml格式的Manifest到内存Map中
*/
class AndroidManifestReader {
/**
* 读取入口方法
*
* @param xmlFile com.android.build.gradle.tasks.ManifestProcessorTask任务的输出文件,
* 一般位于apk工程的build/intermediates/merged_manifest目录中。
*/
fun read(xmlFile: File): ManifestMap {
val manifest = readXml(xmlFile).documentElement
val application = readApplication(manifest)
val globalAttributes = readGlobalAttributes(manifest, application)
val components = readComponents(application)
return globalAttributes.plus(components)
}
private fun readXml(xmlFile: File): Document {
try {
val documentBuilderFactory = DocumentBuilderFactory.newDefaultInstance()
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
return documentBuilder.parse(xmlFile)!!
} catch (e: Exception) {
throw RuntimeException("xml应该是AGP生成的合法文件,所以不兼容任何xml读取错误", e)
}
}
private fun readApplication(manifest: Element): Element? {
val elements = manifest.getElementsByTagName(AndroidManifestKeys.application)
return if (elements.length == 1) {
val node = elements.item(0)
assert(node.nodeType == Node.ELEMENT_NODE)
elements.item(0) as Element
} else {
null
}
}
/**
* 读取Manifest中那些唯一的属性
*/
private fun readGlobalAttributes(manifest: Element, application: Element?): Map {
val globalAttributes = mutableMapOf()
fun manifestAttribute(name: String) {
globalAttributes[name] = manifest.getAttribute(name)
}
fun applicationAttribute(name: String) {
if (application != null) {
val attribute = application.getAttribute(name)
if (attribute.isNotEmpty()) {
globalAttributes[name] = attribute
}
}
}
manifestAttribute(AndroidManifestKeys.`package`)
listOf(
AndroidManifestKeys.name,
AndroidManifestKeys.theme,
AndroidManifestKeys.appComponentFactory,
).forEach(::applicationAttribute)
return globalAttributes
}
private fun readComponents(application: Element?) =
listOf(
AndroidManifestKeys.activity to ::parseActivity,
AndroidManifestKeys.service to ::parseService,
AndroidManifestKeys.receiver to ::parseReceiver,
AndroidManifestKeys.provider to ::parseProvider,
).map { (componentKey, parseMethod) ->
val componentArray = parseComponents(application, componentKey, parseMethod)
componentKey to componentArray
}
private fun parseComponents(
application: Element?,
componentKey: String,
parseFunction: (Element) -> ComponentMap
): Array {
if (application == null) {
return emptyArray()
}
val nodeList = application.getElementsByTagName(componentKey)
val length = nodeList.length
val collectionList = mutableListOf()
for (i in 0 until length) {
val node = nodeList.item(i)
assert(node.nodeType == Node.ELEMENT_NODE)
val map = parseFunction(node as Element)
collectionList.add(map)
}
return collectionList.toTypedArray()
}
private fun parseActivity(element: Element): ComponentMap {
val activityMap = parseComponent(element).toMutableMap()
listOf(
AndroidManifestKeys.theme,
AndroidManifestKeys.configChanges,
AndroidManifestKeys.windowSoftInputMode,
AndroidManifestKeys.screenOrientation,
).forEach { attributeKey ->
activityMap.putAttributeIfNotNull(element, attributeKey)
}
return activityMap
}
private fun parseService(element: Element): ComponentMap {
return parseComponent(element)
}
private fun parseReceiver(element: Element): ComponentMap {
val receiverMap = parseComponent(element).toMutableMap()
val receiverActions = parseReceiverActions(element)
if (receiverActions.isNotEmpty()) {
receiverMap[AndroidManifestKeys.action] = receiverActions
}
return receiverMap
}
private fun parseReceiverActions(receiverElement: Element): List {
val intentFilters =
receiverElement.getElementsByTagName(AndroidManifestKeys.`intent-filter`)
val collectionList = mutableListOf()
for (i in 0 until intentFilters.length) {
val intentFilter = intentFilters.item(i)
assert(intentFilter.nodeType == Node.ELEMENT_NODE)
val actions = (intentFilter as Element).getElementsByTagName(AndroidManifestKeys.action)
for (j in 0 until actions.length) {
val action = actions.item(j)
assert(action.nodeType == Node.ELEMENT_NODE)
val actionName = (action as Element).getAttribute(AndroidManifestKeys.name)
collectionList.add(actionName)
}
}
return collectionList
}
private fun parseProvider(element: Element): ComponentMap {
val providerMap = parseComponent(element).toMutableMap()
providerMap.putAttributeIfNotNull(element, AndroidManifestKeys.authorities)
providerMap.putAttributeIfNotNull(element, AndroidManifestKeys.grantUriPermissions)
return providerMap
}
private fun parseComponent(element: Element): ComponentMap {
val componentName = element.getAttribute(AndroidManifestKeys.name)
return mapOf(
AndroidManifestKeys.name to componentName
)
}
private fun MutableComponentMap.putAttributeIfNotNull(
componentElement: Element,
attributeKey: String
) {
if (componentElement.hasAttribute(attributeKey)) {
this[attributeKey] = componentElement.getAttribute(attributeKey)
}
}
}
================================================
FILE: projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/ManifestParser.kt
================================================
package com.tencent.shadow.core.manifest_parser
import java.io.File
import java.util.Collections
/**
* manifest-parser的入口方法
*
* @param xmlFile com.android.build.gradle.tasks.ManifestProcessorTask任务的输出文件,
* 一般位于apk工程的build/intermediates/merged_manifest目录中。
* @param outputDir 生成文件的输出目录
* @param packageName 生成类的包名
* @param manifestValueParser 资源解析器
*/
fun generatePluginManifest(
xmlFile: File,
outputDir: File,
packageName: String,
manifestValueParser: ManifestValueParser? = null
) {
val androidManifest = AndroidManifestReader().read(xmlFile)
val generator = PluginManifestGenerator()
generator.generate(androidManifest, outputDir, packageName, manifestValueParser)
}
/**
* 创建资源解析器。
*
* @param rTxt R.txt文件
* @return 资源解析器
*/
fun createManifestValueParser(rTxt: File): ManifestValueParser {
val rTxtMap = parseRTxt(rTxt)
return { resName ->
if (resName.startsWith("@android:")) {
// @android:style/Theme.NoTitleBar -> android.R.style.Theme_NoTitleBar
val parts = resName.substringAfter("@android:").split("/")
val type = parts[0]
val name = parts[1].replace(".", "_")
"android.R.$type.$name"
} else {
// @[package:]type/name -> id 值
var raw = resName.substringAfter("@")
if (raw.contains(":")) {
raw = raw.substringAfter(":")
}
val parts = raw.split("/")
val type = parts[0]
val name = parts[1].replace('.', '_')
val key = "@$type/$name"
rTxtMap[key]
?: throw IllegalArgumentException("Resource not found in R.txt: $resName (normalized: $key)")
}
}
}
/**
* 解析 R.txt 文件并生成资源 ID 映射表。 R.txt 包含项目引用的所有资源 ID。
*
* @param rTxtFile R.txt 文件对象
* @return 资源全称(如 @string/app_name)到 ID 的映射
*/
fun parseRTxt(rTxtFile: File): Map {
if (!rTxtFile.exists()) return Collections.emptyMap()
val map = mutableMapOf()
rTxtFile.useLines {
it.forEach { line ->
if (!(line.startsWith("int "))) {
return@forEach
}
val parts = line.split(Regex("\\s+")).filter { it.isNotBlank() }
if (parts.size == 4 && parts[0] == "int") {
val type = parts[1]
val name = parts[2]
val idStr = parts[3]
map["@$type/$name"] = idStr
}
}
}
return map
}
================================================
FILE: projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/PluginManifestGenerator.kt
================================================
package com.tencent.shadow.core.manifest_parser
import com.squareup.javapoet.*
import com.tencent.shadow.core.runtime.PluginManifest
import java.io.File
import javax.lang.model.element.Modifier
/**
* PluginManifest.java生成器
*
* 将Loader所需的插件Manifest信息生成为Java文件,
* 添加runtime中PluginManifest接口的实现方法
*/
class PluginManifestGenerator {
/**
* 生成器入口方法
*
* 根据AndroidManifestReader输出的Map生成PluginManifest.java到outputDir目录中。
*
* @param manifestMap AndroidManifestReader#read的输出Map
* @param outputDir 生成文件的输出目录
* @param packageName 生成类的包名
* @param manifestValueParser 资源值解析器。用于将资源名称解析为资源 ID 值
*/
fun generate(
manifestMap: ManifestMap,
outputDir: File,
packageName: String,
manifestValueParser: ManifestValueParser? = null
) {
val pluginManifestBuilder = PluginManifestBuilder(manifestMap, manifestValueParser)
val pluginManifest = pluginManifestBuilder.build()
JavaFile.builder(packageName, pluginManifest)
.build()
.writeTo(outputDir)
}
}
private class PluginManifestBuilder(
val manifestMap: ManifestMap,
val manifestValueParser: ManifestValueParser? = null
) {
val classBuilder: TypeSpec.Builder =
TypeSpec.classBuilder("PluginManifest")
.addSuperinterface(ClassName.get(PluginManifest::class.java))
.addModifiers(Modifier.PUBLIC)!!
fun build(): TypeSpec {
listOf(
*buildApplicationFields(),
buildActivityInfoArrayField(),
buildServiceInfoArrayField(),
buildReceiverInfoArrayField(),
buildProviderInfoArrayField(),
).forEach { fieldSpec ->
val getterMethod = buildGetterMethod(fieldSpec)
classBuilder.addField(fieldSpec)
classBuilder.addMethod(getterMethod)
}
return classBuilder.build()
}
private fun buildApplicationFields(): Array {
val stringFields = mapOf(
"applicationPackageName" to AndroidManifestKeys.`package`,
"applicationClassName" to AndroidManifestKeys.name,
"appComponentFactory" to AndroidManifestKeys.appComponentFactory,
).map { (fieldName, key) ->
buildStringField(fieldName, key)
}
val resIdFields = mapOf(
"applicationTheme" to AndroidManifestKeys.theme,
).map { (fieldName, key) ->
buildResIdField(fieldName, key)
}
return (stringFields + resIdFields).toTypedArray()
}
private fun buildActivityInfoArrayField() = buildComponentArrayField(
AndroidManifestKeys.activity,
"ActivityInfo",
"activities",
::toNewActivityInfo,
)
private fun buildServiceInfoArrayField() = buildComponentArrayField(
AndroidManifestKeys.service,
"ServiceInfo",
"services",
::toNewServiceInfo,
)
private fun buildReceiverInfoArrayField() = buildComponentArrayField(
AndroidManifestKeys.receiver,
"ReceiverInfo",
"receivers",
::toNewReceiverInfo,
)
private fun buildProviderInfoArrayField() = buildComponentArrayField(
AndroidManifestKeys.provider,
"ProviderInfo",
"providers",
::toNewProviderInfo,
)
private fun buildComponentArrayField(
key: String,
subClassName: String,
fieldName: String,
transform: (ComponentMap) -> String
): FieldSpec {
@Suppress("UNCHECKED_CAST")
val componentMapArray = manifestMap[key] as Array
val literal = componentMapArray.joinToString(
separator = ",\n",
prefix = "{\n",
postfix = "\n}",
transform = transform
)
val componentInfoArrayTypeName = ArrayTypeName.of(
ClassName.get(
"com.tencent.shadow.core.runtime",
"PluginManifest",
subClassName
)
)
val codeBlock = if (componentMapArray.isNotEmpty()) {
CodeBlock.of("new \$1T \$2L", componentInfoArrayTypeName, literal)
} else {
nullCodeBlock()
}
return privateStaticFinalFieldBuilder(
componentInfoArrayTypeName,
fieldName,
).initializer(
codeBlock
).build()
}
private fun buildStringField(fieldName: String, key: String): FieldSpec {
val value = manifestMap[key]
val codeBlock = if (value != null) {
CodeBlock.of("\"$1L\"", value)
} else {
nullCodeBlock()
}
return privateStaticFinalStringFieldBuilder(fieldName)
.initializer(codeBlock).build()
}
private fun buildGetterMethod(fieldSpec: FieldSpec): MethodSpec =
MethodSpec.methodBuilder("get${fieldSpec.name.capitalize()}")
.addModifiers(
Modifier.PUBLIC,
Modifier.FINAL,
)
.returns(fieldSpec.type)
.addStatement(CodeBlock.of("return ${fieldSpec.name}"))
.build()
private fun buildResIdField(fieldName: String, key: String): FieldSpec {
val manifestValue = manifestMap[key]
return if (manifestValue != null) {
buildResIdFieldWithValue(fieldName, manifestValue)
} else {
privateStaticFinalIntFieldBuilder(fieldName)
.initializer(
CodeBlock.of("$1L", "0")
).build()
}
}
private fun buildResIdFieldWithValue(
fieldName: String,
manifestValue: Any,
): FieldSpec {
val resIdLiteral = themeStringToResId(manifestValue, manifestValueParser)
return privateStaticFinalIntFieldBuilder(fieldName)
.initializer(
CodeBlock.of("$1L", resIdLiteral)
).build()
}
private fun toNewActivityInfo(componentMap: ComponentMap): String {
fun makeResIdLiteral(
key: String,
defaultValue: String = "0",
valueToResId: (value: String) -> String
): String {
val value = componentMap[key] as String?
val literal = if (value != null) {
valueToResId(value)
} else {
defaultValue
}
return literal
}
val themeLiteral = makeResIdLiteral(AndroidManifestKeys.theme) {
themeStringToResId(it, manifestValueParser)
}
val configChangesLiteral = makeResIdLiteral(AndroidManifestKeys.configChanges) {
parseConfigChanges(it)
}
val softInputModeLiteral = makeResIdLiteral(AndroidManifestKeys.windowSoftInputMode) {
parseSoftInputMode(it)
}
val screenOrientation = makeResIdLiteral(AndroidManifestKeys.screenOrientation, "-1") {
parseScreenOrientation(it)
}
return "new com.tencent.shadow.core.runtime.PluginManifest" +
".ActivityInfo(" +
"\"${componentMap[AndroidManifestKeys.name]}\", " +
"$themeLiteral ," +
"$configChangesLiteral ," +
"$softInputModeLiteral ," +
screenOrientation +
")"
}
private fun toNewServiceInfo(componentMap: ComponentMap): String {
return "new com.tencent.shadow.core.runtime.PluginManifest" +
".ServiceInfo(\"${componentMap[AndroidManifestKeys.name]}\")"
}
private fun toNewReceiverInfo(componentMap: ComponentMap): String {
@Suppress("UNCHECKED_CAST")
val actions = componentMap[AndroidManifestKeys.action] as List?
val actionsLiteral =
actions?.joinToString(
prefix = "new String[]{\"",
separator = "\", \"",
postfix = "\"}"
) ?: "null"
return "new com.tencent.shadow.core.runtime.PluginManifest" +
".ReceiverInfo(\"${componentMap[AndroidManifestKeys.name]}\", " +
actionsLiteral +
")"
}
private fun toNewProviderInfo(componentMap: ComponentMap): String {
val authoritiesValue = componentMap[AndroidManifestKeys.authorities]
//如果未传值使用android.content.pm.ProviderInfo.grantUriPermissions的默认值false
val grantUriPermissions = componentMap[AndroidManifestKeys.grantUriPermissions] ?: false
val authoritiesLiteral =
if (authoritiesValue != null) {
"\"${authoritiesValue}\""
} else {
"null"
}
return "new com.tencent.shadow.core.runtime.PluginManifest" +
".ProviderInfo(\"${componentMap[AndroidManifestKeys.name]}\", $authoritiesLiteral,$grantUriPermissions)"
}
companion object {
fun privateStaticFinalFieldBuilder(type: TypeName, fieldName: String) = FieldSpec.builder(
type,
fieldName,
Modifier.PRIVATE,
Modifier.STATIC,
Modifier.FINAL,
)!!
fun privateStaticFinalStringFieldBuilder(fieldName: String) =
privateStaticFinalFieldBuilder(
ClassName.get(String::class.java),
fieldName,
)
fun privateStaticFinalIntFieldBuilder(fieldName: String) =
privateStaticFinalFieldBuilder(
TypeName.INT,
fieldName,
)
fun nullCodeBlock() = CodeBlock.of("null")!!
fun themeStringToResId(
manifestValue: Any,
manifestValueParser: ManifestValueParser? = null
): String {
val formatValue = manifestValue as String // for example: @ref/0x7e0b009e
if (formatValue.startsWith("@ref/")) {
return formatValue.removePrefix("@ref/")
} else if (formatValue.startsWith("@")) {
// 对于使用 merged manifest 的场景,manifestValueParser 不会为 null 。
// 对于使用 ap_ 文件中的 AndroidManifest.xml 的场景,不会出现以 @ 打头却不是 @ref 的情况。
if (manifestValueParser != null) {
// @style/Theme.AppCompat --> id 值
return manifestValueParser.invoke(formatValue)
}
}
// 其余格式:https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:apkparser/analyzer/src/main/java/com/android/tools/apk/analyzer/BinaryXmlParser.java;l=193
throw TODO("不支持其他格式: $formatValue")
}
fun parseConfigChanges(value: String): String {
if (value.startsWith("0x") || value.toIntOrNull() != null) return value
return value.split("|").joinToString("|") {
val constant = when (it) {
"mcc" -> "CONFIG_MCC"
"mnc" -> "CONFIG_MNC"
"locale" -> "CONFIG_LOCALE"
"touchscreen" -> "CONFIG_TOUCHSCREEN"
"keyboard" -> "CONFIG_KEYBOARD"
"keyboardHidden" -> "CONFIG_KEYBOARD_HIDDEN"
"navigation" -> "CONFIG_NAVIGATION"
"orientation" -> "CONFIG_ORIENTATION"
"screenLayout" -> "CONFIG_SCREEN_LAYOUT"
"uiMode" -> "CONFIG_UI_MODE"
"screenSize" -> "CONFIG_SCREEN_SIZE"
"smallestScreenSize" -> "CONFIG_SMALLEST_SCREEN_SIZE"
"density" -> "CONFIG_DENSITY"
"layoutDirection" -> "CONFIG_LAYOUT_DIRECTION"
"fontScale" -> "CONFIG_FONT_SCALE"
"colorMode" -> "CONFIG_COLOR_MODE" // Added in API 26
else -> throw IllegalArgumentException("Unknown configChanges: $it")
}
"android.content.pm.ActivityInfo.$constant"
}
}
fun parseSoftInputMode(value: String): String {
if (value.startsWith("0x") || value.toIntOrNull() != null) return value
return value.split("|").joinToString("|") {
val constant = when (it) {
"stateUnspecified" -> "SOFT_INPUT_STATE_UNSPECIFIED"
"stateUnchanged" -> "SOFT_INPUT_STATE_UNCHANGED"
"stateHidden" -> "SOFT_INPUT_STATE_HIDDEN"
"stateAlwaysHidden" -> "SOFT_INPUT_STATE_ALWAYS_HIDDEN"
"stateVisible" -> "SOFT_INPUT_STATE_VISIBLE"
"stateAlwaysVisible" -> "SOFT_INPUT_STATE_ALWAYS_VISIBLE"
"adjustUnspecified" -> "SOFT_INPUT_ADJUST_UNSPECIFIED"
"adjustResize" -> "SOFT_INPUT_ADJUST_RESIZE"
"adjustPan" -> "SOFT_INPUT_ADJUST_PAN"
"adjustNothing" -> "SOFT_INPUT_ADJUST_NOTHING"
"isForwardNavigation" -> "SOFT_INPUT_IS_FORWARD_NAVIGATION"
else -> throw IllegalArgumentException("Unknown windowSoftInputMode: $it")
}
"android.view.WindowManager.LayoutParams.$constant"
}
}
fun parseScreenOrientation(value: String): String {
if (value.startsWith("0x") || value.toIntOrNull() != null) return value
val constant = when (value) {
"unspecified" -> "SCREEN_ORIENTATION_UNSPECIFIED"
"landscape" -> "SCREEN_ORIENTATION_LANDSCAPE"
"portrait" -> "SCREEN_ORIENTATION_PORTRAIT"
"user" -> "SCREEN_ORIENTATION_USER"
"behind" -> "SCREEN_ORIENTATION_BEHIND"
"sensor" -> "SCREEN_ORIENTATION_SENSOR"
"nosensor" -> "SCREEN_ORIENTATION_NOSENSOR"
"sensorLandscape" -> "SCREEN_ORIENTATION_SENSOR_LANDSCAPE"
"sensorPortrait" -> "SCREEN_ORIENTATION_SENSOR_PORTRAIT"
"reverseLandscape" -> "SCREEN_ORIENTATION_REVERSE_LANDSCAPE"
"reversePortrait" -> "SCREEN_ORIENTATION_REVERSE_PORTRAIT"
"fullSensor" -> "SCREEN_ORIENTATION_FULL_SENSOR"
"userLandscape" -> "SCREEN_ORIENTATION_USER_LANDSCAPE"
"userPortrait" -> "SCREEN_ORIENTATION_USER_PORTRAIT"
"fullUser" -> "SCREEN_ORIENTATION_FULL_USER"
"locked" -> "SCREEN_ORIENTATION_LOCKED"
else -> throw IllegalArgumentException("Unknown screenOrientation: $value")
}
return "android.content.pm.ActivityInfo.$constant"
}
}
}
================================================
FILE: projects/sdk/core/manifest-parser/src/test/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestReaderTest.kt
================================================
package com.tencent.shadow.core.manifest_parser
import org.junit.Assert
import org.junit.Test
import java.io.File
class AndroidManifestReaderTest {
@Test
fun testReadXml() {
val testFile = File(javaClass.classLoader.getResource("sample-app.xml")!!.toURI())
val androidManifest = AndroidManifestReader().read(testFile)
Assert.assertEquals(
"com.tencent.shadow.sample.host",
androidManifest[AndroidManifestKeys.`package`]
)
Assert.assertEquals(
"com.tencent.shadow.sample.plugin.app.lib.UseCaseApplication",
androidManifest[AndroidManifestKeys.name]
)
Assert.assertEquals(
"com.tencent.shadow.test.plugin.androidx_cases.lib.TestComponentFactory",
androidManifest[AndroidManifestKeys.appComponentFactory]
)
Assert.assertEquals(
"@ref/0x01030006",
androidManifest[AndroidManifestKeys.theme]
)
}
}
================================================
FILE: projects/sdk/core/manifest-parser/src/test/kotlin/com/tencent/shadow/core/manifest_parser/PluginManifestGeneratorTest.kt
================================================
package com.tencent.shadow.core.manifest_parser
import org.junit.Assert
import org.junit.Test
import java.io.File
class PluginManifestGeneratorTest {
@Test
fun testCompileCaseAsLittleAsPossible() {
testCompile("case_as_little_as_possible.xml")
}
@Test
fun testNoAppComponentFactory() {
testCompile("noAppComponentFactory.xml")
}
@Test
fun testCompileSampleApp() {
testCompile("sample-app.xml")
}
private fun testCompile(case: String) {
val testFile = File(javaClass.classLoader.getResource(case)!!.toURI())
val androidManifest = AndroidManifestReader().read(testFile)
val generator = PluginManifestGenerator()
val tempBuildDir = File("build", "PluginManifestGeneratorTest")
val outputDir = File(tempBuildDir, case)
println("outputDir==$outputDir")
generator.generate(androidManifest, outputDir, "test")
val cmd = "javac -cp ../runtime/build/classes/java/main:build/classes/java/test" +
" ${outputDir.absolutePath}/test/PluginManifest.java"
val process = Runtime.getRuntime().exec(cmd)
val ret = process.waitFor()
Assert.assertEquals(cmd, 0, ret)
}
}
================================================
FILE: projects/sdk/core/manifest-parser/src/test/resources/case_as_little_as_possible.xml
================================================
================================================
FILE: projects/sdk/core/manifest-parser/src/test/resources/noAppComponentFactory.xml
================================================
================================================
FILE: projects/sdk/core/manifest-parser/src/test/resources/sample-app.xml
================================================
================================================
FILE: projects/sdk/core/runtime/.gitignore
================================================
/build
================================================
FILE: projects/sdk/core/runtime/build.gradle
================================================
/**
* shadow-runtime是运行时Shadow App所依赖的类。
*
* 这里面的类混淆意义不大,对体积缩减需求也不强。
* shadow-loader的Debug版依赖这个模块的Debug版
* shadow-loader的Release版依赖这个模块的Release版
* shadow-transform依赖这个模块的Release版
* 因此暂定这个模块不混淆,保持Debug版和Release版无差别。
*/
import com.tencent.shadow.coding.code_generator.ActivityCodeGenerator
buildscript {
dependencies {
classpath 'com.tencent.shadow.coding:android-jar'
classpath 'com.tencent.shadow.coding:code-generator'
}
}
apply plugin: 'com.tencent.shadow.internal.common-jar-settings'
java {
sourceSets {
main.java.srcDirs += 'build/generated/sources/code-generator'
}
}
dependencies{
compileOnly project(':activity-container')
}
def generateCode = tasks.register('generateCode') {
def outputDir = layout.buildDirectory.dir('generated/sources/code-generator')
outputs.dir(outputDir)
.withPropertyName('outputDir')
doLast {
ActivityCodeGenerator codeGenerator = new ActivityCodeGenerator()
codeGenerator.generate(outputDir.get().getAsFile(), "runtime")
}
}
compileJava.dependsOn(generateCode)
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/ActivityOptionsSupport.java
================================================
/*
* 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.runtime;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityOptions;
import android.util.Pair;
import android.view.View;
/**
* @see com.tencent.shadow.core.transform.specific.ActivityOptionsSupportTransform
*/
@SuppressLint("NewApi")
public class ActivityOptionsSupport {
public static ActivityOptions makeSceneTransitionAnimation(
ShadowActivity shadowActivity,
View sharedElement,
String sharedElementName) {
Activity activity = shadowActivity.hostActivityDelegator
.getHostActivity().getImplementActivity();
return ActivityOptions.makeSceneTransitionAnimation(
activity,
sharedElement,
sharedElementName
);
}
@SafeVarargs
public static ActivityOptions makeSceneTransitionAnimation(
ShadowActivity shadowActivity,
Pair... sharedElements) {
Activity activity = shadowActivity.hostActivityDelegator
.getHostActivity().getImplementActivity();
return ActivityOptions.makeSceneTransitionAnimation(
activity,
sharedElements
);
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/FixedContextLayoutInflater.java
================================================
/*
* 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.runtime;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
/**
* 在HostActivityDelegate.getLayoutInflater返回的LayoutInflater虽然已经被替换为ShadowActivity作为Context了.
* 但是Fragment在创建时还是会通过这个LayoutInflater的cloneInContext方法,传入宿主Activity作为新的Context.
* 这里通过覆盖cloneInContext方法,避免Context被替换.
* 见onGetLayoutInflater() of Activity$HostCallbacks in Activity.java
*
* @author cubershi
*/
public abstract class FixedContextLayoutInflater extends LayoutInflater {
/**
* 复制自
* com.android.internal.policy.PhoneLayoutInflater#sClassPrefixList
*/
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
public FixedContextLayoutInflater(Context context) {
super(context);
}
public FixedContextLayoutInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
}
@Override
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
//模仿com.android.internal.policy.PhoneLayoutInflater#onCreateView实现
//xml中一些系统view省略了包名,这里在尝试拼上包名
for (String prefix : sClassPrefixList) {
try {
Pair afterChange = changeViewNameAndPrefix(name, prefix);
name = afterChange.first;
prefix = afterChange.second;
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
@Override
public LayoutInflater cloneInContext(Context newContext) {
return createNewContextLayoutInflater(newContext);
}
abstract LayoutInflater createNewContextLayoutInflater(Context context);
abstract Pair changeViewNameAndPrefix(String name, String prefix);
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PackageManagerInvokeRedirect.java
================================================
/*
* 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.runtime;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.VersionedPackage;
import android.os.Build;
import java.util.List;
/**
* PackageManagerTransform必须把对PackageManager的调用转到到这个处于Runtime层不被Transform作用的类上来,
* 而不能把这个类实现的方法直接写在Transform生成的方法体中,是为了避免这个类实现的代码再次被Transform,
* 形成循环调用。
*/
public class PackageManagerInvokeRedirect {
public static PluginPackageManager getPluginPackageManager(ClassLoader classLoaderOfInvokeCode) {
return PluginPartInfoManager.getPluginInfo(classLoaderOfInvokeCode).packageManager;
}
public static ApplicationInfo getApplicationInfo(ClassLoader classLoaderOfInvokeCode, String packageName, int flags) throws PackageManager.NameNotFoundException {
return getPluginPackageManager(classLoaderOfInvokeCode).getApplicationInfo(packageName, flags);
}
public static ActivityInfo getActivityInfo(ClassLoader classLoaderOfInvokeCode, ComponentName component, int flags) throws PackageManager.NameNotFoundException {
return getPluginPackageManager(classLoaderOfInvokeCode).getActivityInfo(component, flags);
}
public static ServiceInfo getServiceInfo(ClassLoader classLoaderOfInvokeCode, ComponentName component, int flags) throws PackageManager.NameNotFoundException {
return getPluginPackageManager(classLoaderOfInvokeCode).getServiceInfo(component, flags);
}
public static ProviderInfo getProviderInfo(ClassLoader classLoaderOfInvokeCode, ComponentName component, int flags) throws PackageManager.NameNotFoundException {
return getPluginPackageManager(classLoaderOfInvokeCode).getProviderInfo(component, flags);
}
public static PackageInfo getPackageInfo(ClassLoader classLoaderOfInvokeCode, String packageName, int flags) throws PackageManager.NameNotFoundException {
return getPluginPackageManager(classLoaderOfInvokeCode).getPackageInfo(packageName, flags);
}
@TargetApi(Build.VERSION_CODES.O)
public static PackageInfo getPackageInfo(ClassLoader classLoaderOfInvokeCode, VersionedPackage versionedPackage,
int flags) throws PackageManager.NameNotFoundException {
return getPluginPackageManager(classLoaderOfInvokeCode).getPackageInfo(versionedPackage, flags);
}
@TargetApi(Build.VERSION_CODES.TIRAMISU)
public static PackageInfo getPackageInfo(ClassLoader classLoaderOfInvokeCode, String packageName,
PackageManager.PackageInfoFlags flags) throws PackageManager.NameNotFoundException {
return getPluginPackageManager(classLoaderOfInvokeCode).getPackageInfo(packageName, flags);
}
@TargetApi(Build.VERSION_CODES.TIRAMISU)
public static PackageInfo getPackageInfo(ClassLoader classLoaderOfInvokeCode, VersionedPackage versionedPackage,
PackageManager.PackageInfoFlags flags) throws PackageManager.NameNotFoundException {
return getPluginPackageManager(classLoaderOfInvokeCode).getPackageInfo(versionedPackage, flags);
}
public static ProviderInfo resolveContentProvider(ClassLoader classLoaderOfInvokeCode, String name, int flags) {
return getPluginPackageManager(classLoaderOfInvokeCode).resolveContentProvider(name, flags);
}
public static List queryContentProviders(ClassLoader classLoaderOfInvokeCode, String processName, int uid, int flags) {
return getPluginPackageManager(classLoaderOfInvokeCode).queryContentProviders(processName, uid, flags);
}
public static ResolveInfo resolveActivity(ClassLoader classLoaderOfInvokeCode, Intent intent, int flags) {
return getPluginPackageManager(classLoaderOfInvokeCode).resolveActivity(intent, flags);
}
public static ResolveInfo resolveService(ClassLoader classLoaderOfInvokeCode, Intent intent, int flags) {
return getPluginPackageManager(classLoaderOfInvokeCode).resolveService(intent, flags);
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginActivity.java
================================================
/*
* 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.runtime;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.Window;
import com.tencent.shadow.core.runtime.container.HostActivityDelegator;
import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
public abstract class PluginActivity extends GeneratedPluginActivity {
static PluginActivity get(PluginContainerActivity pluginContainerActivity) {
Object o = pluginContainerActivity.getPluginActivity();
if (o != null) {
return (PluginActivity) o;
} else {
//在遇到IllegalIntent时hostActivityDelegate==null。需要返回一个空的Activity避免Crash。
return new ShadowActivity();
}
}
HostActivityDelegator hostActivityDelegator;
ShadowApplication mPluginApplication;
ComponentName mCallingActivity;
public void registerActivityLifecycleCallbacks(
ShadowActivityLifecycleCallbacks callback) {
mPluginApplication.mActivityLifecycleCallbacksHolder.registerActivityLifecycleCallbacks(
callback, this, hostActivityDelegator
);
}
public void unregisterActivityLifecycleCallbacks(
ShadowActivityLifecycleCallbacks callback) {
mPluginApplication.mActivityLifecycleCallbacksHolder.unregisterActivityLifecycleCallbacks(
callback, this, hostActivityDelegator
);
}
public final void setHostContextAsBase(Context context) {
attachBaseContext(context);
}
public void setHostActivityDelegator(HostActivityDelegator delegator) {
super.hostActivityDelegator = delegator;
hostActivityDelegator = delegator;
}
public void setPluginApplication(ShadowApplication pluginApplication) {
mPluginApplication = pluginApplication;
}
public boolean onCreatePanelMenu(int featureId, Menu menu) {
if (featureId == Window.FEATURE_OPTIONS_PANEL) {
return onCreateOptionsMenu(menu);
} else {
return hostActivityDelegator.superOnCreatePanelMenu(featureId, menu);
}
}
public LayoutInflater getLayoutInflater() {
return LayoutInflater.from(this);
}
//TODO: 对齐原手工代码,这个方法签名实际上不对,应该传入ShadowActivity
public void onChildTitleChanged(Activity childActivity, CharSequence title) {
hostActivityDelegator.superOnChildTitleChanged(childActivity, title);
}
@Override
public boolean onNavigateUpFromChild(ShadowActivity arg0) {
throw new UnsupportedOperationException("Unsupported Yet");
}
@Override
public void onChildTitleChanged(ShadowActivity arg0, CharSequence arg1) {
throw new UnsupportedOperationException("Unsupported Yet");
}
public void setCallingActivity(ComponentName callingActivity) {
mCallingActivity = callingActivity;
}
@Override
public void setTheme(int resid) {
super.setTheme(resid);
hostActivityDelegator.setTheme(resid);
}
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
return super.getSystemService(name);
} else {
return hostActivityDelegator.getHostActivity().getImplementActivity()
.getSystemService(name);
}
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginManifest.java
================================================
package com.tencent.shadow.core.runtime;
import android.os.Parcel;
import android.os.Parcelable;
public interface PluginManifest {
/**
* same as android.content.pm.PackageItemInfo#packageName
*/
String getApplicationPackageName();
/**
* same as android.content.pm.ApplicationInfo#className
*/
String getApplicationClassName();
/**
* same as android.content.pm.ApplicationInfo#appComponentFactory
*/
String getAppComponentFactory();
/**
* same as android.content.pm.ApplicationInfo#theme
*/
int getApplicationTheme();
ActivityInfo[] getActivities();
ServiceInfo[] getServices();
ReceiverInfo[] getReceivers();
ProviderInfo[] getProviders();
abstract class ComponentInfo implements Parcelable {
public final String className;
public ComponentInfo(String className) {
this.className = className;
}
protected ComponentInfo(Parcel in) {
className = in.readString();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(className);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator CREATOR = new Creator() {
@Override
public ComponentInfo createFromParcel(Parcel in) {
throw new UnsupportedOperationException();
}
@Override
public ComponentInfo[] newArray(int size) {
return new ComponentInfo[size];
}
};
}
final class ActivityInfo extends ComponentInfo implements Parcelable {
public final int theme;
public final int configChanges;
public final int softInputMode;
public final int screenOrientation;
public ActivityInfo(String className,
int theme,
int configChanges,
int softInputMode,
int screenOrientation) {
super(className);
this.theme = theme;
this.configChanges = configChanges;
this.softInputMode = softInputMode;
this.screenOrientation = screenOrientation;
}
protected ActivityInfo(Parcel in) {
super(in);
theme = in.readInt();
configChanges = in.readInt();
softInputMode = in.readInt();
screenOrientation = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(theme);
dest.writeInt(configChanges);
dest.writeInt(softInputMode);
dest.writeInt(screenOrientation);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator CREATOR = new Creator() {
@Override
public ActivityInfo createFromParcel(Parcel in) {
return new ActivityInfo(in);
}
@Override
public ActivityInfo[] newArray(int size) {
return new ActivityInfo[size];
}
};
}
final class ServiceInfo extends ComponentInfo {
public ServiceInfo(String className) {
super(className);
}
}
final class ReceiverInfo extends ComponentInfo {
public final String[] actions;
public ReceiverInfo(String className, String[] actions) {
super(className);
this.actions = actions;
}
}
final class ProviderInfo extends ComponentInfo {
public final String authorities;
public final boolean grantUriPermissions;
public ProviderInfo(String className, String authorities, boolean grantUriPermissions) {
super(className);
this.authorities = authorities;
this.grantUriPermissions = grantUriPermissions;
}
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginPackageManager.java
================================================
package com.tencent.shadow.core.runtime;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.VersionedPackage;
import android.os.Build;
import java.util.List;
public interface PluginPackageManager {
ApplicationInfo getApplicationInfo(String packageName, int flags);
ActivityInfo getActivityInfo(ComponentName component, int flags);
ServiceInfo getServiceInfo(ComponentName component, int flags);
ProviderInfo getProviderInfo(ComponentName component, int flags);
PackageInfo getPackageInfo(String packageName, int flags);
PackageInfo getPackageInfo(VersionedPackage versionedPackage, int flags);
@TargetApi(Build.VERSION_CODES.TIRAMISU)
PackageInfo getPackageInfo(VersionedPackage versionedPackage, PackageManager.PackageInfoFlags flags);
@TargetApi(Build.VERSION_CODES.TIRAMISU)
PackageInfo getPackageInfo(String packageName, PackageManager.PackageInfoFlags flags);
ProviderInfo resolveContentProvider(String name, int flags);
List queryContentProviders(String processName, int uid, int flags);
ResolveInfo resolveActivity(Intent intent, int flags);
ResolveInfo resolveService(Intent intent, int flags);
String getArchiveFilePath();
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginPartInfo.java
================================================
/*
* 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.runtime;
import android.content.res.Resources;
public class PluginPartInfo {
public ShadowApplication application;
public Resources resources;
public ClassLoader classLoader;
PluginPackageManager packageManager;
public PluginPartInfo(ShadowApplication application, Resources resources, ClassLoader classLoader, PluginPackageManager packageManager) {
this.application = application;
this.resources = resources;
this.classLoader = classLoader;
this.packageManager = packageManager;
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginPartInfoManager.java
================================================
/*
* 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.runtime;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class PluginPartInfoManager {
private static Map sPluginInfos = new HashMap<>();
public static void addPluginInfo(ClassLoader classLoader, PluginPartInfo pluginPartInfo) {
sPluginInfos.put(classLoader, pluginPartInfo);
}
public static PluginPartInfo getPluginInfo(ClassLoader classLoader) {
PluginPartInfo pluginPartInfo = sPluginInfos.get(classLoader);
if (pluginPartInfo == null) {
throw new RuntimeException("没有找到classLoader对应的pluginInfo classLoader:" + classLoader);
}
return pluginPartInfo;
}
public static Collection getAllPluginInfo() {
return sPluginInfos.values();
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/ResolverHook.java
================================================
/*
* 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.runtime;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
public class ResolverHook {
public static Uri insert(ContentResolver resolver, Uri url, ContentValues values) {
Uri containerUri = UriConverter.parse(url.toString());
return resolver.insert(containerUri, values);
}
public static int delete(ContentResolver resolver, Uri url, String where, String[] selectionArgs) {
Uri containerUri = UriConverter.parse(url.toString());
return resolver.delete(containerUri, where, selectionArgs);
}
public static int update(ContentResolver resolver, Uri uri, ContentValues values, String where, String[] selectionArgs) {
Uri containerUri = UriConverter.parse(uri.toString());
return resolver.update(containerUri, values, where, selectionArgs);
}
@TargetApi(Build.VERSION_CODES.O)
public static Cursor query(ContentResolver resolver, Uri uri, String[] projection, Bundle queryArgs,
CancellationSignal cancellationSignal) {
Uri containerUri = UriConverter.parse(uri.toString());
return resolver.query(containerUri, projection, queryArgs, cancellationSignal);
}
public static Cursor query(ContentResolver resolver, Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Uri containerUri = UriConverter.parse(uri.toString());
return resolver.query(containerUri, projection, selection, selectionArgs, sortOrder);
}
public static Cursor query(ContentResolver resolver, Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder,
CancellationSignal cancellationSignal) {
Uri containerUri = UriConverter.parse(uri.toString());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return resolver.query(containerUri, projection, selection, selectionArgs, sortOrder, cancellationSignal);
} else {
return null;
}
}
public static Bundle call(ContentResolver resolver, Uri uri, String method, String arg, Bundle extras) {
if (extras == null) {
extras = new Bundle();
}
Uri containerUri = UriConverter.parseCall(uri.toString(), extras);
return resolver.call(containerUri, method, arg, extras);
}
public static int bulkInsert(ContentResolver resolver, Uri url, ContentValues[] values) {
Uri containerUri = UriConverter.parse(url.toString());
return resolver.bulkInsert(containerUri, values);
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/ShadowActivity.java
================================================
/*
* 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.runtime;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentSender;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class ShadowActivity extends PluginActivity {
@Override
public void setContentView(int layoutResID) {
if ("merge".equals(XmlPullParserUtil.getLayoutStartTagName(getResources(), layoutResID))) {
//如果传进来的xml文件的根tag是merge时,需要特殊处理
View decorView = hostActivityDelegator.getWindow().getDecorView();
ViewGroup viewGroup = decorView.findViewById(android.R.id.content);
LayoutInflater.from(this).inflate(layoutResID, viewGroup);
} else {
View inflate = LayoutInflater.from(this).inflate(layoutResID, null);
hostActivityDelegator.setContentView(inflate);
}
}
@Override
public final ShadowApplication getApplication() {
return mPluginApplication;
}
@Override
public final ShadowActivity getParent() {
return null;
}
@Override
public void overridePendingTransition(int enterAnim, int exitAnim) {
//如果使用的资源不是系统资源,我们无法支持这个特性。
if ((enterAnim & 0xFF000000) != 0x01000000) {
enterAnim = 0;
}
if ((exitAnim & 0xFF000000) != 0x01000000) {
exitAnim = 0;
}
hostActivityDelegator.overridePendingTransition(enterAnim, exitAnim);
}
@Override
public void startActivityForResult(Intent intent, int requestCode) {
startActivityForResult(intent, requestCode, null);
}
@Override
public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
final Intent pluginIntent = new Intent(intent);
pluginIntent.setExtrasClassLoader(mPluginClassLoader);
ComponentName callingActivity = new ComponentName(getPackageName(), getClass().getName());
final boolean success = mPluginComponentLauncher.startActivityForResult(hostActivityDelegator, pluginIntent, requestCode, options, callingActivity);
if (!success) {
hostActivityDelegator.startActivityForResult(intent, requestCode, options);
}
}
@Override
public SharedPreferences getPreferences(int mode) {
return super.getSharedPreferences(getLocalClassName(), mode);
}
@Override
public String getLocalClassName() {
return this.getClass().getName();
}
@Override
public boolean shouldUpRecreateTask(Intent targetIntent) {
Intent intent = mPluginComponentLauncher.convertPluginActivityIntent(targetIntent);
return hostActivityDelegator.shouldUpRecreateTask(intent);
}
@Override
public boolean navigateUpTo(Intent upIntent) {
Intent intent = mPluginComponentLauncher.convertPluginActivityIntent(upIntent);
return hostActivityDelegator.navigateUpTo(intent);
}
@Override
public final T requireViewById(int id) {
T view = findViewById(id);
if (view == null) {
throw new IllegalArgumentException("ID does not reference a View inside this Activity");
}
return view;
}
@Override
public void startIntentSenderFromChild(ShadowActivity arg0, IntentSender arg1, int arg2, Intent arg3, int arg4, int arg5, int arg6) throws IntentSender.SendIntentException {
throw new UnsupportedOperationException("Unsupported Yet");
}
@Override
public void startIntentSenderFromChild(ShadowActivity arg0, IntentSender arg1, int arg2, Intent arg3, int arg4, int arg5, int arg6, Bundle arg7) throws IntentSender.SendIntentException {
throw new UnsupportedOperationException("Unsupported Yet");
}
@Override
public boolean navigateUpToFromChild(ShadowActivity arg0, Intent arg1) {
throw new UnsupportedOperationException("Unsupported Yet");
}
@Override
public void finishFromChild(ShadowActivity arg0) {
throw new UnsupportedOperationException("Unsupported Yet");
}
@Override
public void finishActivityFromChild(ShadowActivity arg0, int arg1) {
throw new UnsupportedOperationException("Unsupported Yet");
}
@Override
public ComponentName getCallingActivity() {
return mCallingActivity;
}
/**
* https://developer.android.com/reference/android/app/Activity#startActivityFromChild(android.app.Activity,%20android.content.Intent,%20int,%20android.os.Bundle)
*
* This method was deprecated in API level R.
* Use androidx.fragment.app.FragmentActivity#startActivityFromFragment( androidx.fragment.app.Fragment,Intent,int,Bundle)
*
* 不计划支持这个方法了。
*/
@Override
public void startActivityFromChild(ShadowActivity arg0, Intent arg1, int arg2) {
throw new UnsupportedOperationException("Unsupported");
}
/**
* https://developer.android.com/reference/android/app/Activity#startActivityFromChild(android.app.Activity,%20android.content.Intent,%20int,%20android.os.Bundle)
*
* This method was deprecated in API level R.
* Use androidx.fragment.app.FragmentActivity#startActivityFromFragment( androidx.fragment.app.Fragment,Intent,int,Bundle)
*
* 不计划支持这个方法了。
*/
@Override
public void startActivityFromChild(ShadowActivity arg0, Intent arg1, int arg2, Bundle arg3) {
throw new UnsupportedOperationException("Unsupported");
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/ShadowActivityLifecycleCallbacks.java
================================================
/*
* 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.runtime;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import com.tencent.shadow.core.runtime.container.HostActivityDelegator;
import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
public interface ShadowActivityLifecycleCallbacks {
void onActivityPreCreated(ShadowActivity activity, Bundle savedInstanceState);
void onActivityCreated(ShadowActivity activity, Bundle savedInstanceState);
void onActivityPostCreated(ShadowActivity activity, Bundle savedInstanceState);
void onActivityPreStarted(ShadowActivity activity);
void onActivityStarted(ShadowActivity activity);
void onActivityPostStarted(ShadowActivity activity);
void onActivityPreResumed(ShadowActivity activity);
void onActivityResumed(ShadowActivity activity);
void onActivityPostResumed(ShadowActivity activity);
void onActivityPrePaused(ShadowActivity activity);
void onActivityPaused(ShadowActivity activity);
void onActivityPostPaused(ShadowActivity activity);
void onActivityPreStopped(ShadowActivity activity);
void onActivityStopped(ShadowActivity activity);
void onActivityPostStopped(ShadowActivity activity);
void onActivityPreSaveInstanceState(ShadowActivity activity, Bundle outState);
void onActivitySaveInstanceState(ShadowActivity activity, Bundle outState);
void onActivityPostSaveInstanceState(ShadowActivity activity, Bundle outState);
void onActivityPreDestroyed(ShadowActivity activity);
void onActivityDestroyed(ShadowActivity activity);
void onActivityPostDestroyed(ShadowActivity activity);
class Wrapper implements Application.ActivityLifecycleCallbacks {
final ShadowActivityLifecycleCallbacks shadowActivityLifecycleCallbacks;
final Object runtimeObject;
private boolean isRegistered;
public Wrapper(ShadowActivityLifecycleCallbacks shadowActivityLifecycleCallbacks, Object runtimeObject) {
this.shadowActivityLifecycleCallbacks = shadowActivityLifecycleCallbacks;
this.runtimeObject = runtimeObject;
}
private ShadowActivity getPluginActivity(Activity activity) {
if (activity instanceof PluginContainerActivity) {
return (ShadowActivity) PluginActivity.get((PluginContainerActivity) activity);
} else {
return null;
}
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
shadowActivityLifecycleCallbacks.onActivityCreated(pluginActivity, savedInstanceState);
}
}
@Override
public void onActivityStarted(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
shadowActivityLifecycleCallbacks.onActivityStarted(pluginActivity);
}
}
@Override
public void onActivityResumed(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
shadowActivityLifecycleCallbacks.onActivityResumed(pluginActivity);
}
}
@Override
public void onActivityPaused(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
shadowActivityLifecycleCallbacks.onActivityPaused(pluginActivity);
}
}
@Override
public void onActivityStopped(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
shadowActivityLifecycleCallbacks.onActivityStopped(pluginActivity);
}
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
shadowActivityLifecycleCallbacks.onActivitySaveInstanceState(pluginActivity, outState);
}
}
@Override
public void onActivityDestroyed(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
shadowActivityLifecycleCallbacks.onActivityDestroyed(pluginActivity);
}
}
@Override
public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {
//此时PluginActivity尚未构造。改由onPluginActivityPreCreated通知。
}
public void onPluginActivityPreCreated(ShadowActivity pluginActivity, Bundle savedInstanceState) {
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPreCreated(pluginActivity, savedInstanceState);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPostCreated(pluginActivity, savedInstanceState);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPreStarted(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPreStarted(pluginActivity);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPostStarted(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPostStarted(pluginActivity);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPreResumed(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPreResumed(pluginActivity);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPostResumed(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPostResumed(pluginActivity);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPrePaused(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPrePaused(pluginActivity);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPostPaused(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPostPaused(pluginActivity);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPreStopped(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPreStopped(pluginActivity);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPostStopped(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPostStopped(pluginActivity);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPreSaveInstanceState(Activity activity, Bundle outState) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPreSaveInstanceState(pluginActivity, outState);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPostSaveInstanceState(Activity activity, Bundle outState) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPostSaveInstanceState(pluginActivity, outState);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPreDestroyed(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPreDestroyed(pluginActivity);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
@Override
public void onActivityPostDestroyed(Activity activity) {
final ShadowActivity pluginActivity = getPluginActivity(activity);
if (checkOwnerActivity(pluginActivity)) {
try {
shadowActivityLifecycleCallbacks.onActivityPostDestroyed(pluginActivity);
} catch (AbstractMethodError ignored) {
//兼容Java8接口default方法
}
}
}
/**
* 检测Activity是否属于当前Application所在的插件
*
* @param activity 插件Activity
* @return 是否属于当前Application所在的插件 true属于
*/
private boolean checkOwnerActivity(PluginActivity activity) {
if (activity == null) {
return false;
} else if (runtimeObject instanceof ShadowApplication) {
return activity.mPluginApplication == runtimeObject;
} else {
return activity == runtimeObject;
}
}
}
class Holder {
final private Map>
mShadowActivityLifecycleCallbacksWrapperMap = new WeakHashMap<>();
/**
* 针对业务代码自己不持有ActivityLifecycleCallbacks的情况,
* 无法通过mShadowActivityLifecycleCallbacksWrapperMap获取所有wrapper,
* 需要特别记录所有wrapper。
*
* 采用弱引用以便不影响Wrapper原本的GC时机,Wrapper至少被系统持有。
*
* GuardedBy mShadowActivityLifecycleCallbacksWrapperMap
*/
final private Map
mAllShadowActivityLifecycleCallbackWrappers = new WeakHashMap<>();
public void notifyPluginActivityPreCreated(ShadowActivity pluginActivity,
Bundle savedInstanceState) {
synchronized (mShadowActivityLifecycleCallbacksWrapperMap) {
//onPluginActivityPreCreated中可能会再次调用registerActivityLifecycleCallbacks,
//进而修改mAllShadowActivityLifecycleCallbackWrappers,
//因此需要先复制出待通知的callback,再通知。
List copiedWrappers = new LinkedList<>();
Set wrappers
= mAllShadowActivityLifecycleCallbackWrappers.keySet();
for (ShadowActivityLifecycleCallbacks.Wrapper wrapper : wrappers) {
// wrapper是弱引用持有的,需要二次验证其是否处于register状态
if (wrapper != null && wrapper.isRegistered) {
copiedWrappers.add(wrapper);
}
}
for (ShadowActivityLifecycleCallbacks.Wrapper wrapper : copiedWrappers) {
wrapper.onPluginActivityPreCreated(pluginActivity, savedInstanceState);
}
}
}
private ShadowActivityLifecycleCallbacks.Wrapper shadowActivityLifecycleCallbacksToWrapper(
ShadowActivityLifecycleCallbacks callbacks,
Object caller
) {
if (callbacks == null) {
return null;
}
synchronized (mShadowActivityLifecycleCallbacksWrapperMap) {
ShadowActivityLifecycleCallbacks.Wrapper wrapper;
WeakReference weakReference
= mShadowActivityLifecycleCallbacksWrapperMap.get(callbacks);
wrapper = weakReference == null ? null : weakReference.get();
if (wrapper == null) {
wrapper = new ShadowActivityLifecycleCallbacks.Wrapper(callbacks, caller);
mShadowActivityLifecycleCallbacksWrapperMap.put(callbacks,
new WeakReference<>(wrapper));
mAllShadowActivityLifecycleCallbackWrappers.put(wrapper, null);
}
return wrapper;
}
}
void registerActivityLifecycleCallbacks(ShadowActivityLifecycleCallbacks callback,
Object caller,
Object hostActivityDelegatorOrApplication) {
Wrapper wrapper = shadowActivityLifecycleCallbacksToWrapper(callback, caller);
wrapper.isRegistered = true;
if (hostActivityDelegatorOrApplication instanceof HostActivityDelegator) {
((HostActivityDelegator) hostActivityDelegatorOrApplication)
.registerActivityLifecycleCallbacks(wrapper);
} else {
((Application) hostActivityDelegatorOrApplication)
.registerActivityLifecycleCallbacks(wrapper);
}
}
void unregisterActivityLifecycleCallbacks(ShadowActivityLifecycleCallbacks callback,
Object caller,
Object hostActivityDelegatorOrApplication) {
Wrapper wrapper = shadowActivityLifecycleCallbacksToWrapper(callback, caller);
wrapper.isRegistered = false;
if (hostActivityDelegatorOrApplication instanceof HostActivityDelegator) {
((HostActivityDelegator) hostActivityDelegatorOrApplication)
.unregisterActivityLifecycleCallbacks(wrapper);
} else {
((Application) hostActivityDelegatorOrApplication)
.unregisterActivityLifecycleCallbacks(wrapper);
}
}
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/ShadowAppComponentFactory.java
================================================
package com.tencent.shadow.core.runtime;
import android.content.BroadcastReceiver;
import android.content.ContentProvider;
import android.content.Intent;
public class ShadowAppComponentFactory {
public ShadowApplication instantiateApplication(ClassLoader cl,
String className)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (ShadowApplication) cl.loadClass(className).newInstance();
}
public ShadowActivity instantiateActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (ShadowActivity) cl.loadClass(className).newInstance();
}
public BroadcastReceiver instantiateReceiver(ClassLoader cl,
String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (BroadcastReceiver) cl.loadClass(className).newInstance();
}
public ShadowService instantiateService(ClassLoader cl,
String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (ShadowService) cl.loadClass(className).newInstance();
}
public ContentProvider instantiateProvider(ClassLoader cl,
String className)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (ContentProvider) cl.loadClass(className).newInstance();
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/ShadowApplication.java
================================================
/*
* 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.runtime;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.os.Build;
import java.util.HashMap;
import java.util.Map;
/**
* 用于在plugin-loader中调用假的Application方法的接口
*/
public class ShadowApplication extends ShadowContext {
private Application mHostApplication;
private Map mBroadcasts;
private ShadowAppComponentFactory mAppComponentFactory;
final public ShadowActivityLifecycleCallbacks.Holder mActivityLifecycleCallbacksHolder
= new ShadowActivityLifecycleCallbacks.Holder();
public boolean isCallOnCreate;
@Override
public Context getApplicationContext() {
return this;
}
public void registerActivityLifecycleCallbacks(
ShadowActivityLifecycleCallbacks callback) {
mActivityLifecycleCallbacksHolder.registerActivityLifecycleCallbacks(
callback, this, mHostApplication
);
}
public void unregisterActivityLifecycleCallbacks(
ShadowActivityLifecycleCallbacks callback) {
mActivityLifecycleCallbacksHolder.unregisterActivityLifecycleCallbacks(
callback, this, mHostApplication
);
}
public void onCreate() {
isCallOnCreate = true;
for (Map.Entry entry : mBroadcasts.entrySet()) {
try {
String receiverClassname = entry.getKey();
BroadcastReceiver receiver = mAppComponentFactory.instantiateReceiver(
mPluginClassLoader,
receiverClassname,
null);
IntentFilter intentFilter = new IntentFilter();
String[] receiverActions = entry.getValue();
if (receiverActions != null) {
for (String action : receiverActions) {
intentFilter.addAction(action);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED);
} else {
registerReceiver(receiver, intentFilter);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
mHostApplication.registerComponentCallbacks(new ComponentCallbacks2() {
@Override
public void onTrimMemory(int level) {
ShadowApplication.this.onTrimMemory(level);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
ShadowApplication.this.onConfigurationChanged(newConfig);
}
@Override
public void onLowMemory() {
ShadowApplication.this.onLowMemory();
}
});
}
public void onTerminate() {
throw new UnsupportedOperationException();
}
public void onConfigurationChanged(Configuration newConfig) {
//do nothing.
}
public void onLowMemory() {
//do nothing.
}
public void onTrimMemory(int level) {
//do nothing.
}
public void registerComponentCallbacks(ComponentCallbacks callback) {
mHostApplication.registerComponentCallbacks(callback);
}
public void unregisterComponentCallbacks(ComponentCallbacks callback) {
mHostApplication.unregisterComponentCallbacks(callback);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public void registerOnProvideAssistDataListener(Application.OnProvideAssistDataListener callback) {
mHostApplication.registerOnProvideAssistDataListener(callback);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public void unregisterOnProvideAssistDataListener(Application.OnProvideAssistDataListener callback) {
mHostApplication.unregisterOnProvideAssistDataListener(callback);
}
public void setHostApplicationContextAsBase(Context hostAppContext) {
super.attachBaseContext(hostAppContext);
mHostApplication = (Application) hostAppContext;
}
public void setBroadcasts(PluginManifest.ReceiverInfo[] receiverInfos) {
Map classNameToActions = new HashMap<>();
if (receiverInfos != null) {
for (PluginManifest.ReceiverInfo receiverInfo : receiverInfos) {
classNameToActions.put(receiverInfo.className, receiverInfo.actions);
}
}
mBroadcasts = classNameToActions;
}
public void attachBaseContext(Context base) {
//do nothing.
}
public void setAppComponentFactory(ShadowAppComponentFactory factory) {
mAppComponentFactory = factory;
}
@SuppressLint("NewApi")
public static String getProcessName() {
return Application.getProcessName();
}
}
================================================
FILE: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/ShadowContext.java
================================================
/*
* 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.runtime;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Pair;
import android.view.LayoutInflater;
import com.tencent.shadow.core.runtime.container.GeneratedHostActivityDelegator;
public class ShadowContext extends SubDirContextThemeWrapper {
PluginComponentLauncher mPluginComponentLauncher;
ClassLoader mPluginClassLoader;
ShadowApplication mShadowApplication;
Resources mPluginResources;
LayoutInflater mLayoutInflater;
ApplicationInfo mApplicationInfo;
protected String mPartKey;
private String mBusinessName;
public ShadowContext() {
}
public ShadowContext(Context base, int themeResId) {
super(base, themeResId);
}
public final void setPluginResources(Resources resources) {
mPluginResources = resources;
}
public final void setPluginClassLoader(ClassLoader classLoader) {
mPluginClassLoader = classLoader;
}
public void setPluginComponentLauncher(PluginComponentLauncher pluginComponentLauncher) {
mPluginComponentLauncher = pluginComponentLauncher;
}
public void setShadowApplication(ShadowApplication shadowApplication) {
mShadowApplication = shadowApplication;
}
public void setApplicationInfo(ApplicationInfo applicationInfo) {
ApplicationInfo copy = new ApplicationInfo(applicationInfo);
copy.metaData = null;//正常通过Context获得的ApplicationInfo就没有metaData
mApplicationInfo = copy;
}
public void setBusinessName(String businessName) {
if (TextUtils.isEmpty(businessName)) {
businessName = null;
}
this.mBusinessName = businessName;
}
public void setPluginPartKey(String partKey) {
this.mPartKey = partKey;
}
@Override
public Context getApplicationContext() {
return mShadowApplication;
}
@Override
public Resources getResources() {
return mPluginResources;
}
@Override
public AssetManager getAssets() {
return mPluginResources.getAssets();
}
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mLayoutInflater == null) {
LayoutInflater inflater = (LayoutInflater) super.getSystemService(name);
mLayoutInflater = ShadowLayoutInflater.build(inflater, this, mPartKey);
}
return mLayoutInflater;
}
return super.getSystemService(name);
}
@Override
public ClassLoader getClassLoader() {
return mPluginClassLoader;
}
public interface PluginComponentLauncher {
/**
* 启动Activity
*
* @param shadowContext 启动context
* @param intent 插件内传来的Intent.
* @return true表示该Intent是为了启动插件内Activity的,已经被正确消费了.
* false表示该Intent不是插件内的Activity.
*/
boolean startActivity(ShadowContext shadowContext, Intent intent, Bundle options);
/**
* 启动Activity
*
* @param delegator 发起启动的activity的delegator
* @param intent 插件内传来的Intent.
* @param callingActivity 调用者
* @return true表示该Intent是为了启动插件内Activity的,已经被正确消费了.
* false表示该Intent不是插件内的Activity.
*/
boolean startActivityForResult(GeneratedHostActivityDelegator delegator, Intent intent, int requestCode, Bundle option, ComponentName callingActivity);
Pair startService(ShadowContext context, Intent service);
Pair