├── .github
└── workflows
│ └── android.yml
├── .gitignore
├── .idea
├── encodings.xml
├── misc.xml
└── vcs.xml
├── Plugin
├── .gitignore
├── AutoTrackPlugin
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── wallstreetcn
│ │ └── autotrack
│ │ ├── AarTransform.java
│ │ ├── AutoTrackConfig.java
│ │ ├── AutoTrackPlugin.kt
│ │ ├── AutoTrackPluginProvider.kt
│ │ ├── DataScanTransform.kt
│ │ ├── NewAutoTackTransform.kt
│ │ ├── TransformActionDemo.kt
│ │ ├── helper
│ │ ├── AutoTrackHelper.kt
│ │ ├── ClassFilterVisitor.java
│ │ ├── FieldAnnotationVisitor.java
│ │ ├── FragmentTrackHelper.kt
│ │ ├── MethodCell.java
│ │ ├── MethodHelper.java
│ │ ├── ModifyUtils.java
│ │ └── RecyclerViewHolderImp.kt
│ │ └── scan
│ │ ├── DataClassManager.kt
│ │ └── DataScanAsmHelper.kt
├── BasePlugin
│ ├── .gitignore
│ ├── build.gradle
│ ├── nexus.properties
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── kronos
│ │ └── plugin
│ │ └── base
│ │ ├── AsmHelper.java
│ │ ├── BaseTransform.kt
│ │ ├── ClassNameFilter.kt
│ │ ├── ClassUtils.kt
│ │ ├── DefaultClassNameFilter.kt
│ │ ├── DeleteCallBack.kt
│ │ ├── JarUtils.kt
│ │ ├── Log.kt
│ │ ├── PluginProvider.kt
│ │ ├── SetDiff.java
│ │ ├── TransformBuilder.kt
│ │ ├── TransformCallBack.kt
│ │ ├── asm
│ │ ├── AsmExtensions.kt
│ │ ├── Java9ClassAdapter.kt
│ │ └── LambdaNodeHelper.kt
│ │ └── utils
│ │ ├── DigestUtils.kt
│ │ └── FileUtils.kt
├── build.gradle
├── double_tap_plugin
│ ├── .gitignore
│ ├── build.gradle
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── kronos
│ │ └── doubletap
│ │ ├── DoubleTabConfig.kt
│ │ ├── DoubleTapAppTransform.kt
│ │ ├── DoubleTapLibraryTransform.kt
│ │ ├── DoubleTapPlugin.java
│ │ ├── DoubleTapProvider.kt
│ │ ├── DoubleTapTransform.kt
│ │ └── helper
│ │ ├── CheckVisitor.java
│ │ ├── ClassFilterVisitor.java
│ │ ├── DoubleTapAsmHelper.java
│ │ ├── DoubleTapClassNodeHelper.kt
│ │ ├── InitBlockVisitor.java
│ │ ├── MethodCell.java
│ │ ├── MethodHelper.java
│ │ └── ModifyUtils.java
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── multiPlugin
│ ├── .gitignore
│ ├── build.gradle
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── kronos
│ │ └── plugin
│ │ └── multi
│ │ ├── MultiPlugin.kt
│ │ └── graph
│ │ ├── Analyzer.kt
│ │ ├── ModuleBfsHelper.kt
│ │ └── ModuleNode.kt
├── settings.gradle
└── thread_hook_plugin
│ ├── .gitignore
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── com
│ └── kronos
│ └── plugin
│ └── thread
│ ├── HookTransform.kt
│ ├── PoolEntity.kt
│ ├── PrivacyClassVisitorFactory.kt
│ ├── ThreadHookPlugin.kt
│ ├── ThreadHookProvider.kt
│ ├── task
│ └── ManifestTask.kt
│ └── visitor
│ ├── PrivacyAsmHelper.kt
│ ├── ThreadAsmHelper.kt
│ ├── ThreadPoolCreator.kt
│ ├── ThreadPoolMethodVisitor.java
│ └── privacy
│ ├── PrivacyAsmEntity.kt
│ ├── PrivacyClassNode.kt
│ └── PrivacyHelper.kt
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── wallstreetcn
│ │ └── sample
│ │ ├── App.kt
│ │ ├── DoubleTapCheck.kt
│ │ ├── Entity.java
│ │ ├── Fragment2.kt
│ │ ├── JavaFragment.java
│ │ ├── MainActivity.java
│ │ ├── NewEntity.java
│ │ ├── NewTest.java
│ │ ├── SecondActivity.kt
│ │ ├── ToastHelper.java
│ │ ├── adapter
│ │ ├── OtherTestViewHolder.java
│ │ ├── Test.kt
│ │ └── TestAdapter.kt
│ │ ├── utils
│ │ ├── PrivacyUtils.kt
│ │ ├── ReflectUtil.java
│ │ ├── ResourceUtils.java
│ │ ├── TestIOThreadExecutor.kt
│ │ └── TestThreadFactory.kt
│ │ └── viewexpose
│ │ ├── AutoExposeImp.kt
│ │ ├── ExposeChecker.kt
│ │ ├── ExposeConstant.kt
│ │ ├── ExposeViewDelegate.kt
│ │ ├── ItemViewExtension.kt
│ │ └── OnExposeListener.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── layout
│ ├── activity_main.xml
│ ├── activity_second.xml
│ └── recycler_item_view.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
├── testmodule
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── kronos
│ │ └── testmodule
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── wallstreetcn
│ │ │ └── testmodule
│ │ │ ├── KronosContext.kt
│ │ │ ├── ModuleActivity.kt
│ │ │ ├── OtherTestViewHolder.java
│ │ │ └── TestFragment.kt
│ └── res
│ │ ├── layout
│ │ └── activity_module.xml
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── kronos
│ └── testmodule
│ └── ExampleUnitTest.java
├── upload.gradle
└── upload_bintray.gradle
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: set up JDK 11
17 | uses: actions/setup-java@v2
18 | with:
19 | java-version: '11'
20 | distribution: 'adopt'
21 |
22 | - name: Grant execute permission for gradlew
23 | run: chmod +x gradlew
24 | - name: publish
25 | run: ./gradlew wrapper; ./gradlew publish
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /.idea/assetWizardSettings.xml
4 | /build
5 | /captures
6 | .externalNativeBuild
7 | .gradle/
8 | .idea/
9 | .DS_Store/
10 | .externalNativeBuild/
11 | **/build/
12 | **/captures/
13 | **/*.iml
14 | **/local.properties
15 | local_repo.xml
16 | .repo/
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Plugin/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'groovy'
2 | apply plugin: 'kotlin'
3 | apply plugin: 'java-gradle-plugin'
4 | apply plugin: 'kotlin-kapt'
5 |
6 | dependencies {
7 | implementation gradleApi()
8 | implementation localGroovy()
9 | implementation project(":BasePlugin")
10 | kapt "com.google.auto.service:auto-service:1.0"
11 | implementation 'com.google.auto.service:auto-service:1.0'
12 | }
13 |
14 | gradlePlugin {
15 | plugins {
16 | version {
17 | // 在 app 模块需要通过 id 引用这个插件
18 | id = 'auto-track'
19 | // 实现这个插件的类的路径
20 | implementationClass = 'com.wallstreetcn.autotrack.AutoTrackPlugin'
21 | }
22 | }
23 | }
24 |
25 | java {
26 | sourceCompatibility = JavaVersion.VERSION_11
27 | targetCompatibility = JavaVersion.VERSION_11
28 | }
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/AarTransform.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack;
2 |
3 | import com.android.build.gradle.internal.dependency.ExtractProGuardRulesTransform;
4 | import com.android.build.gradle.internal.dependency.GenericTransformParameters;
5 | import com.android.build.gradle.internal.publishing.AndroidArtifacts;
6 | import com.android.build.gradle.internal.tasks.AarMetadataTask;
7 |
8 | import org.checkerframework.checker.nullness.qual.NonNull;
9 | import org.gradle.api.artifacts.transform.InputArtifact;
10 | import org.gradle.api.artifacts.transform.TransformAction;
11 | import org.gradle.api.artifacts.transform.TransformOutputs;
12 | import org.gradle.api.file.FileSystemLocation;
13 | import org.gradle.api.provider.Property;
14 | import org.gradle.api.provider.Provider;
15 | import org.gradle.api.tasks.Classpath;
16 | import org.gradle.api.tasks.Input;
17 |
18 | import java.io.File;
19 |
20 | /**
21 | * @Author LiABao
22 | * @Since 2021/9/18
23 | */
24 | public abstract class AarTransform {} /*implements TransformAction {
25 |
26 | public interface Parameters extends GenericTransformParameters {
27 | @Input
28 | Property getTargetType();
29 |
30 | @Input
31 | Property getSharedLibSupport();
32 | }
33 |
34 | @Classpath
35 | @InputArtifact
36 | public abstract Provider getInputArtifact();
37 |
38 | @NonNull
39 | public static AndroidArtifacts.ArtifactType[] getTransformTargets() {
40 | return new AndroidArtifacts.ArtifactType[] {
41 | // For CLASSES, this transform is ues for runtime, and AarCompileClassesTransform is
42 | // used for compile
43 | AndroidArtifacts.ArtifactType.SHARED_CLASSES,
44 | AndroidArtifacts.ArtifactType.JAVA_RES,
45 | AndroidArtifacts.ArtifactType.SHARED_JAVA_RES,
46 | AndroidArtifacts.ArtifactType.PROCESSED_JAR,
47 | AndroidArtifacts.ArtifactType.MANIFEST,
48 | AndroidArtifacts.ArtifactType.ANDROID_RES,
49 | AndroidArtifacts.ArtifactType.ASSETS,
50 | AndroidArtifacts.ArtifactType.SHARED_ASSETS,
51 | AndroidArtifacts.ArtifactType.JNI,
52 | AndroidArtifacts.ArtifactType.SHARED_JNI,
53 | AndroidArtifacts.ArtifactType.AIDL,
54 | AndroidArtifacts.ArtifactType.RENDERSCRIPT,
55 | AndroidArtifacts.ArtifactType.UNFILTERED_PROGUARD_RULES,
56 | AndroidArtifacts.ArtifactType.LINT,
57 | AndroidArtifacts.ArtifactType.ANNOTATIONS,
58 | AndroidArtifacts.ArtifactType.PUBLIC_RES,
59 | AndroidArtifacts.ArtifactType.COMPILE_SYMBOL_LIST,
60 | AndroidArtifacts.ArtifactType.DATA_BINDING_ARTIFACT,
61 | AndroidArtifacts.ArtifactType.DATA_BINDING_BASE_CLASS_LOG_ARTIFACT,
62 | AndroidArtifacts.ArtifactType.RES_STATIC_LIBRARY,
63 | AndroidArtifacts.ArtifactType.RES_SHARED_STATIC_LIBRARY,
64 | AndroidArtifacts.ArtifactType.PREFAB_PACKAGE,
65 | AndroidArtifacts.ArtifactType.AAR_METADATA,
66 | AndroidArtifacts.ArtifactType.ART_PROFILE,
67 | };
68 | }
69 |
70 | @Override
71 | public void transform(@NonNull TransformOutputs transformOutputs) {
72 | File input = getInputArtifact().get().getAsFile();
73 | AndroidArtifacts.ArtifactType targetType = getParameters().getTargetType().get();
74 | switch (targetType) {
75 | case CLASSES_JAR:
76 | case JAVA_RES:
77 | case PROCESSED_JAR:
78 | // even though resources are supposed to only be in the main jar of the AAR, this
79 | // is not necessarily enforced by all build systems generating AAR so it's safer to
80 | // read all jars from the manifest.
81 | // For shared libraries, these are provided via SHARED_CLASSES and SHARED_JAVA_RES.
82 | if (!isShared(input)) {
83 | AarTransformUtil.getJars(input).forEach(transformOutputs::file);
84 | }
85 | break;
86 | case SHARED_CLASSES:
87 | case SHARED_JAVA_RES:
88 | if (isShared(input)) {
89 | AarTransformUtil.getJars(input).forEach(transformOutputs::file);
90 | }
91 | break;
92 | case LINT:
93 | outputIfExists(FileUtils.join(input, FD_JARS, FN_LINT_JAR), transformOutputs);
94 | break;
95 | case MANIFEST:
96 | // Return both the manifest and the extra snippet for the shared library.
97 | outputIfExists(new File(input, FN_ANDROID_MANIFEST_XML), transformOutputs);
98 | if (isShared(input)) {
99 | outputIfExists(
100 | new File(input, FN_SHARED_LIBRARY_ANDROID_MANIFEST_XML),
101 | transformOutputs);
102 | }
103 | break;
104 | case ANDROID_RES:
105 | outputIfExists(new File(input, FD_RES), transformOutputs);
106 | break;
107 | case ASSETS:
108 | outputIfExists(new File(input, FD_ASSETS), transformOutputs);
109 | break;
110 | case JNI:
111 | outputIfExists(new File(input, FD_JNI), transformOutputs);
112 | break;
113 | case AIDL:
114 | outputIfExists(new File(input, FD_AIDL), transformOutputs);
115 | break;
116 | case RENDERSCRIPT:
117 | outputIfExists(new File(input, FD_RENDERSCRIPT), transformOutputs);
118 | break;
119 | case UNFILTERED_PROGUARD_RULES:
120 | if (!ExtractProGuardRulesTransform.performTransform(
121 | FileUtils.join(input, FD_JARS, FN_CLASSES_JAR), transformOutputs, false)) {
122 | outputIfExists(new File(input, FN_PROGUARD_TXT), transformOutputs);
123 | }
124 | break;
125 | case ANNOTATIONS:
126 | outputIfExists(new File(input, FN_ANNOTATIONS_ZIP), transformOutputs);
127 | break;
128 | case PUBLIC_RES:
129 | outputIfExists(new File(input, FN_PUBLIC_TXT), transformOutputs);
130 | break;
131 | case COMPILE_SYMBOL_LIST:
132 | outputIfExists(new File(input, FN_RESOURCE_TEXT), transformOutputs);
133 | break;
134 | case RES_STATIC_LIBRARY:
135 | if (!isShared(input)) {
136 | outputIfExists(new File(input, FN_RESOURCE_STATIC_LIBRARY), transformOutputs);
137 | }
138 | break;
139 | case RES_SHARED_STATIC_LIBRARY:
140 | if (isShared(input)) {
141 | outputIfExists(
142 | new File(input, SdkConstants.FN_RESOURCE_SHARED_STATIC_LIBRARY),
143 | transformOutputs);
144 | }
145 | break;
146 | case DATA_BINDING_ARTIFACT:
147 | outputIfExists(
148 | new File(input, DataBindingBuilder.DATA_BINDING_ROOT_FOLDER_IN_AAR),
149 | transformOutputs);
150 | break;
151 | case DATA_BINDING_BASE_CLASS_LOG_ARTIFACT:
152 | outputIfExists(
153 | new File(
154 | input,
155 | DataBindingBuilder.DATA_BINDING_CLASS_LOG_ROOT_FOLDER_IN_AAR),
156 | transformOutputs);
157 | break;
158 | case PREFAB_PACKAGE:
159 | outputIfExists(new File(input, FD_PREFAB_PACKAGE), transformOutputs);
160 | break;
161 | case AAR_METADATA:
162 | outputIfExists(
163 | FileUtils.join(input, AarMetadataTask.AAR_METADATA_ENTRY_PATH.split("/")),
164 | transformOutputs);
165 | break;
166 | case ART_PROFILE:
167 | outputIfExists(
168 | FileUtils.join(
169 | input,
170 | SdkConstants.FN_ART_PROFILE),
171 | transformOutputs);
172 | break;
173 | default:
174 | throw new RuntimeException("Unsupported type in AarTransform: " + targetType);
175 | }
176 | }
177 |
178 | private boolean isShared(@NonNull File explodedAar) {
179 | return getParameters().getSharedLibSupport().get()
180 | && new File(explodedAar, FN_SHARED_LIBRARY_ANDROID_MANIFEST_XML).exists();
181 | }
182 |
183 | private static void outputIfExists(@NonNull File file, @NonNull TransformOutputs outputs) {
184 | if (file.isDirectory()) {
185 | outputs.dir(file);
186 | } else if (file.isFile()) {
187 | outputs.file(file);
188 | }
189 | }
190 | }
191 | */
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/AutoTrackConfig.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack;
2 |
3 | public class AutoTrackConfig {
4 | public String[] packageList = {};
5 | }
6 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/AutoTrackPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack
2 |
3 | import com.android.build.api.variant.Component
4 | import com.android.build.gradle.AppExtension
5 | import com.android.build.gradle.AppPlugin
6 | import com.android.build.gradle.internal.dependency.AarTransform
7 | import com.android.build.gradle.internal.dependency.GenericTransformParameters
8 | import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
9 | import com.android.build.gradle.internal.publishing.AndroidArtifacts
10 | import com.android.build.gradle.internal.utils.setDisallowChanges
11 | import com.android.build.gradle.options.BooleanOption
12 | import com.kronos.plugin.base.Log
13 | import com.kronos.plugin.base.utils.filterTest
14 | import org.gradle.api.Plugin
15 | import org.gradle.api.Project
16 | import org.gradle.api.artifacts.transform.TransformAction
17 | import org.gradle.api.artifacts.transform.TransformSpec
18 | import org.gradle.api.attributes.Attribute
19 | import org.gradle.api.internal.artifacts.ArtifactAttributes
20 |
21 | class AutoTrackPlugin : Plugin {
22 |
23 | override fun apply(project: Project) {
24 | val isApp = project.plugins.hasPlugin(AppPlugin::class.java)
25 | if (isApp) {
26 | val appExtension = project.extensions.getByType(BaseAppModuleExtension::class.java)
27 | val scanTransform = DataScanTransform()
28 | appExtension.registerTransform(scanTransform)
29 | appExtension.registerTransform(NewAutoTackTransform())
30 | }
31 |
32 | // val artifactType = Attribute.of("artifactType", String::class.java)
33 | //val transformed = Attribute.of("TransformActionDemo", Boolean::class.java)
34 | //val transformTarget = AndroidArtifacts.ArtifactType.AAR
35 | //BooleanOption.CONSUME_DEPENDENCIES_AS_SHARED_LIBRARIES
36 | /* project.registerTransform(
37 | TransformActionDemo::class.java,
38 | AndroidArtifacts.ArtifactType.AAR,
39 | transformTarget
40 | ) { params ->
41 | params.getTargetType().setDisallowChanges(transformTarget)
42 | params.getSharedLibSupport().setDisallowChanges(false)
43 | }*/
44 |
45 | }
46 |
47 | private fun Project.registerTransform(
48 | transformClass: Class>,
49 | fromArtifactType: AndroidArtifacts.ArtifactType,
50 | toArtifactType: AndroidArtifacts.ArtifactType,
51 | parametersSetter: ((T) -> Unit)? = null
52 | ) {
53 | registerTransform(
54 | transformClass,
55 | fromArtifactType.type,
56 | toArtifactType.type,
57 | parametersSetter
58 | )
59 | }
60 |
61 |
62 | private fun Project.registerTransform(
63 | transformClass: Class>,
64 | fromArtifactType: String,
65 | toArtifactType: String,
66 | parametersSetter: ((T) -> Unit)? = null
67 | ) {
68 | dependencies.registerTransform(
69 | transformClass
70 | ) { spec: TransformSpec ->
71 | spec.from.attribute(ArtifactAttributes.ARTIFACT_FORMAT, fromArtifactType)
72 | spec.to.attribute(ArtifactAttributes.ARTIFACT_FORMAT, toArtifactType)
73 | spec.parameters.projectName.setDisallowChanges(name)
74 | parametersSetter?.let { it(spec.parameters) }
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/AutoTrackPluginProvider.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack
2 |
3 | import com.google.auto.service.AutoService
4 | import com.kronos.plugin.base.PluginProvider
5 | import org.gradle.api.Plugin
6 | import org.gradle.api.Project
7 |
8 | /**
9 | * @Author LiABao
10 | * @Since 2021/1/26
11 | */
12 | @AutoService(value = [PluginProvider::class])
13 | class AutoTrackPluginProvider : PluginProvider {
14 |
15 | override fun getPlugin(): Class> {
16 | return AutoTrackPlugin::class.java
17 | }
18 |
19 | override fun dependOn(): List {
20 | return arrayListOf().apply {
21 | // add("com.kronos.doubletap.DoubleTapProvider")
22 | // add("com.kronos.plugin.thread.ThreadHookProvider")
23 | }
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/DataScanTransform.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack
2 |
3 | import com.android.build.api.transform.QualifiedContent
4 | import com.android.build.api.transform.Transform
5 | import com.android.build.api.transform.TransformException
6 | import com.android.build.api.transform.TransformInvocation
7 | import com.android.build.gradle.internal.pipeline.TransformManager
8 | import com.kronos.plugin.base.BaseTransform
9 | import com.kronos.plugin.base.ClassUtils
10 | import com.kronos.plugin.base.TransformCallBack
11 | import com.wallstreetcn.autotrack.scan.DataScanAsmHelper
12 | import java.io.IOException
13 |
14 | /**
15 | * @Author LiABao
16 | * @Since 2021/1/9
17 | */
18 | class DataScanTransform : Transform() {
19 | override fun getName(): String {
20 | return "DataScanTransform"
21 | }
22 |
23 | override fun getInputTypes(): Set {
24 | return TransformManager.CONTENT_JARS
25 | }
26 |
27 | override fun getScopes(): MutableSet? {
28 | return TransformManager.SCOPE_FULL_PROJECT
29 | }
30 |
31 | override fun isIncremental(): Boolean {
32 | return true
33 | }
34 |
35 | @Throws(TransformException::class, InterruptedException::class, IOException::class)
36 | override fun transform(transformInvocation: TransformInvocation) {
37 | val injectHelper = DataScanAsmHelper()
38 | val baseTransform = BaseTransform(transformInvocation, object : TransformCallBack {
39 | override fun process(className: String, bytes: ByteArray?): ByteArray? {
40 | if (ClassUtils.checkClassName(className)) {
41 | try {
42 | return injectHelper.modifyClass(bytes)
43 | } catch (e: Exception) {
44 | e.printStackTrace()
45 | }
46 | }
47 | return null
48 | }
49 | })
50 | baseTransform.startTransform()
51 | }
52 | }
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/NewAutoTackTransform.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack
2 |
3 | import com.android.build.api.transform.QualifiedContent
4 | import com.android.build.api.transform.Transform
5 | import com.android.build.api.transform.TransformException
6 | import com.android.build.api.transform.TransformInvocation
7 | import com.android.build.gradle.internal.pipeline.TransformManager
8 | import com.kronos.plugin.base.BaseTransform
9 | import com.kronos.plugin.base.ClassUtils
10 | import com.kronos.plugin.base.TransformCallBack
11 | import com.wallstreetcn.autotrack.helper.AutoTrackHelper
12 | import java.io.IOException
13 |
14 | class NewAutoTackTransform : Transform() {
15 | override fun getName(): String {
16 | return "NewAutoTackTransform"
17 | }
18 |
19 | override fun getInputTypes(): Set {
20 | return TransformManager.CONTENT_JARS
21 | }
22 |
23 | override fun getScopes(): MutableSet? {
24 | return TransformManager.SCOPE_FULL_PROJECT
25 | }
26 |
27 | override fun isIncremental(): Boolean {
28 | return true
29 | }
30 |
31 | @Throws(TransformException::class, InterruptedException::class, IOException::class)
32 | override fun transform(transformInvocation: TransformInvocation) {
33 | val injectHelper = AutoTrackHelper()
34 | val baseTransform = BaseTransform(transformInvocation, object : TransformCallBack {
35 | override fun process(className: String, classBytes: ByteArray?): ByteArray? {
36 | if (ClassUtils.checkClassName(className)) {
37 | try {
38 | return injectHelper.modifyClass(classBytes!!)
39 | } catch (e: IOException) {
40 | e.printStackTrace()
41 | }
42 | }
43 | return null
44 | }
45 | })
46 | baseTransform.startTransform()
47 | }
48 | }
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/TransformActionDemo.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack
2 |
3 | import com.android.build.gradle.internal.dependency.GenericTransformParameters
4 | import com.android.build.gradle.internal.publishing.AndroidArtifacts
5 | import com.kronos.plugin.base.Log
6 | import org.gradle.api.artifacts.transform.*
7 | import org.gradle.api.file.ConfigurableFileCollection
8 | import org.gradle.api.file.FileSystemLocation
9 | import org.gradle.api.provider.Property
10 | import org.gradle.api.provider.Provider
11 | import org.gradle.api.tasks.Classpath
12 | import org.gradle.api.tasks.Input
13 |
14 | import org.gradle.api.tasks.InputFiles
15 |
16 |
17 | /**
18 | *
19 | * @Author LiABao
20 | * @Since 2021/9/17
21 | *
22 | */
23 |
24 | abstract class TransformActionDemo : TransformAction {
25 |
26 | override fun transform(outputs: TransformOutputs) {
27 | val input = getInputArtifact().get().asFile
28 | Log.info("TransformActionDemo: input:$input")
29 | if (!input.exists()) {
30 | input.parentFile.mkdirs()
31 | // input.bytes = []
32 | outputs.file(getInputArtifact())
33 | return
34 | }
35 | }
36 |
37 | @Classpath
38 | @InputArtifact
39 | abstract fun getInputArtifact(): Provider
40 |
41 |
42 | interface Parameters : GenericTransformParameters {
43 | @Input
44 | fun getTargetType(): Property
45 |
46 | @Input
47 | fun getSharedLibSupport(): Property
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/helper/AutoTrackHelper.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack.helper
2 |
3 | import com.kronos.plugin.base.AsmHelper
4 | import com.kronos.plugin.base.Log
5 | import com.kronos.plugin.base.asm.lambdaHelper
6 | import com.wallstreetcn.autotrack.scan.DataClassManager
7 | import org.objectweb.asm.ClassReader
8 | import org.objectweb.asm.ClassWriter
9 | import org.objectweb.asm.Opcodes
10 | import org.objectweb.asm.Opcodes.*
11 | import org.objectweb.asm.tree.*
12 | import java.io.IOException
13 |
14 | class AutoTrackHelper : AsmHelper {
15 |
16 | private val classNodeMap = hashMapOf()
17 |
18 | @Throws(IOException::class)
19 | override fun modifyClass(srcClass: ByteArray): ByteArray {
20 | val classNode = ClassNode(ASM5)
21 | val classReader = ClassReader(srcClass)
22 | //1 将读入的字节转为classNode
23 | classReader.accept(classNode, 0)
24 | classNodeMap[classNode.name] = classNode
25 | RecyclerViewHolderImp(classNode)
26 | // 判断当前类是否实现了OnClickListener接口
27 | classNode.interfaces?.forEach {
28 | if (it == "android/view/View\$OnClickListener") {
29 | val field = classNode.getField()
30 | classNode.methods?.forEach { method ->
31 | // 找到onClick 方法
32 | if (method.name == "onClick" && method.desc == "(Landroid/view/View;)V") {
33 | insertTrack(classNode, method, field)
34 | }
35 | }
36 | }
37 | }
38 | classNode.lambdaHelper(true) {
39 | (it.name == "onClick" && it.desc.contains(")Landroid/view/View\$OnClickListener;"))
40 | }.forEach { method ->
41 | val field = classNode.getField()
42 | insertLambda(classNode, method, field)
43 | }
44 | //调用Fragment的onHiddenChange方法
45 | visitFragment(classNode)
46 | val classWriter = ClassWriter(0)
47 | //3 将classNode转为字节数组
48 | classNode.accept(classWriter)
49 | return classWriter.toByteArray()
50 | }
51 |
52 | private fun visitFragment(classNode: ClassNode) {
53 | if (isFragment(classNode.superName)) {
54 | var hasHiddenChange = false
55 | classNode.methods?.forEach { methodNode ->
56 | if (methodNode.name == "onHiddenChanged" && methodNode.desc == "(Z)V" && methodNode.access == Opcodes.ACC_PUBLIC) {
57 | hasHiddenChange = true
58 | classNode.insert(methodNode)
59 | }
60 | }
61 | if (!hasHiddenChange) {
62 | val methodVisitor = classNode.visitMethod(
63 | ACC_PUBLIC, "onHiddenChanged", "(Z)V",
64 | classNode.signature, null
65 | )
66 | methodVisitor.visitCode()
67 | methodVisitor.visitVarInsn(ALOAD, 0);
68 | methodVisitor.visitVarInsn(ILOAD, 1);
69 | methodVisitor.visitMethodInsn(
70 | INVOKESPECIAL, classNode.superName,
71 | "onHiddenChanged", "(Z)V", false
72 | )
73 | methodVisitor.visitInsn(RETURN)
74 | methodVisitor.visitMaxs(2, 2)
75 | methodVisitor.visitEnd()
76 | classNode.methods.last()?.let { methodNode ->
77 | if (methodNode.name == "onHiddenChanged" && methodNode.desc == "(Z)V" && methodNode.access == Opcodes.ACC_PUBLIC) {
78 | classNode.insert(methodNode)
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
85 | private fun insertLambda(node: ClassNode, method: MethodNode, field: FieldNode?) {
86 | // 判断方法名和方法描述
87 | val className = node.outerClass
88 | val parentNode = classNodeMap[className]
89 | // 根据outClassName 获取到外部类的Node
90 | val parentField = field ?: parentNode?.getField()
91 | val instructions = method.instructions
92 | instructions?.iterator()?.forEach {
93 | // 判断是不是代码的截止点
94 | if ((it.opcode >= Opcodes.IRETURN && it.opcode <= Opcodes.RETURN) || it.opcode == Opcodes.ATHROW) {
95 | instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 0))
96 | instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 0))
97 | // 获取到数据参数
98 | if (parentField != null) {
99 | parentField.apply {
100 | instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 0))
101 | instructions.insertBefore(
102 | it, FieldInsnNode(Opcodes.GETFIELD, node.name, parentField.name, parentField.desc)
103 | )
104 | }
105 | } else {
106 | instructions.insertBefore(it, LdcInsnNode("1234"))
107 | }
108 | instructions.insertBefore(
109 | it, MethodInsnNode(
110 | Opcodes.INVOKESTATIC,
111 | "com/wallstreetcn/sample/ToastHelper",
112 | "toast",
113 | "(Ljava/lang/Object;Landroid/view/View;Ljava/lang/Object;)V",
114 | false)
115 | )
116 | }
117 | }
118 | }
119 |
120 |
121 | private fun insertTrack(node: ClassNode, method: MethodNode, field: FieldNode?) {
122 | // 判断方法名和方法描述
123 | val className = node.outerClass
124 | val parentNode = classNodeMap[className]
125 | // 根据outClassName 获取到外部类的Node
126 | val parentField = field ?: parentNode?.getField()
127 | val instructions = method.instructions
128 | var dataField: FieldInsnNode? = null
129 | instructions?.iterator()?.forEach {
130 | if (it is FieldInsnNode && dataField == null) {
131 | if (it.opcode == GETFIELD) {
132 | Log.info(" instructions filedName:${it.name} fieldOwner:${it.owner} desc:${it.desc}")
133 | val className = it.desc.subSequence(1, it.desc.length - 1)
134 | if (DataClassManager.INSTANCE.datList.contains(className)) {
135 | dataField = it
136 | Log.info(" match track filedName:${it.name} fieldOwner:${it.owner} desc:${it.desc}")
137 | }
138 | }
139 | }
140 | // 判断是不是代码的截止点
141 | if ((it.opcode >= Opcodes.IRETURN && it.opcode <= Opcodes.RETURN) || it.opcode == Opcodes.ATHROW) {
142 | instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 1))
143 | instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 1))
144 | // 获取到数据参数
145 | if (dataField != null) {
146 | dataField?.let { dataField ->
147 | instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 0))
148 | instructions.insertBefore(it, FieldInsnNode(dataField.opcode, dataField.owner, dataField.name, dataField.desc))
149 | }
150 | } else if (parentField != null) {
151 | parentField.apply {
152 | instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 0))
153 | instructions.insertBefore(
154 | it, FieldInsnNode(Opcodes.GETFIELD, node.name, parentField.name, parentField.desc)
155 | )
156 | }
157 | } else {
158 | instructions.insertBefore(it, LdcInsnNode("1234"))
159 | }
160 | instructions.insertBefore(
161 | it, MethodInsnNode(
162 | Opcodes.INVOKESTATIC,
163 | "com/wallstreetcn/sample/ToastHelper",
164 | "toast",
165 | "(Ljava/lang/Object;Landroid/view/View;Ljava/lang/Object;)V",
166 | false)
167 | )
168 | }
169 | }
170 | }
171 | }
172 |
173 | // 判断Field是否包含注解
174 | private fun ClassNode.getField(): FieldNode? {
175 | return fields?.firstOrNull { field ->
176 | var hasAnnotation = false
177 | field?.visibleAnnotations?.forEach { annotation ->
178 | if (annotation.desc == "Lcom/wallstreetcn/sample/adapter/Test;") {
179 | hasAnnotation = true
180 | }
181 | }
182 | hasAnnotation
183 | }
184 | }
185 |
186 |
187 | fun isFragment(superName: String?): Boolean {
188 | return superName == "androidx/fragment/app/Fragment" || superName == "android/support/v4/app/Fragment"
189 | }
190 |
191 |
192 | fun isRecyclerViewHolder(superName: String?): Boolean {
193 | return superName == "androidx/recyclerview/widget/RecyclerView\$ViewHolder"
194 | }
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/helper/ClassFilterVisitor.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack.helper;
2 |
3 |
4 | import org.objectweb.asm.ClassVisitor;
5 | import org.objectweb.asm.FieldVisitor;
6 | import org.objectweb.asm.MethodVisitor;
7 | import org.objectweb.asm.Opcodes;
8 | import org.objectweb.asm.Type;
9 |
10 |
11 | //其实我已经不用了 但是保留下历史吧 毕竟以前菜过
12 | public class ClassFilterVisitor extends ClassVisitor {
13 |
14 | private String[] interfaces;
15 | private final String objectText = "java/lang/Object";
16 |
17 |
18 | ClassFilterVisitor(ClassVisitor classVisitor) {
19 | super(Opcodes.ASM5, classVisitor);
20 | }
21 |
22 | @Override
23 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
24 | super.visit(version, access, name, signature, superName, interfaces);
25 | this.interfaces = interfaces;
26 | if (!superName.equals(objectText)) {
27 | int index = name.lastIndexOf("/");
28 | index = index < 0 ? 0 : index + 1;
29 | String nameShort = name.substring(index);
30 | cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
31 | "mTargetClassName", Type.getDescriptor(String.class),
32 | null, nameShort);
33 | }
34 |
35 | }
36 |
37 | @Override
38 | public void visitOuterClass(String owner, String name, String desc) {
39 | super.visitOuterClass(owner, name, desc);
40 | }
41 |
42 |
43 | @Override
44 | public void visitEnd() {
45 | super.visitEnd();
46 | }
47 |
48 | @Override
49 | public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
50 | FieldVisitor fieldVisitor = super.visitField(access, name, desc, signature, value);
51 | return new FieldAnnotationVisitor(Opcodes.ASM5, fieldVisitor, name, desc) {
52 |
53 | @Override
54 | void hasAnnotation(String fieldName, String fieldDesc) {
55 | this.fieldName = name;
56 | this.fieldDesc = desc;
57 | }
58 | };
59 | }
60 |
61 | @Override
62 | public MethodVisitor visitMethod(int access, String name,
63 | String desc, String signature, String[] exceptions) {
64 | if (interfaces != null && interfaces.length > 0) {
65 | try {
66 | MethodCell cell = MethodHelper.sInterfaceMethods.get(name + desc);
67 | for (String interfaceName : interfaces) {
68 | if (cell != null && interfaceName.equals(cell.parent)) {
69 | MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
70 | return new MethodVisitor(Opcodes.ASM5, methodVisitor) {
71 |
72 | @Override
73 | public void visitInsn(int opcode) {
74 | if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
75 | || opcode == Opcodes.ATHROW) {
76 | visitMethodWithLoadedParams(methodVisitor, Opcodes.INVOKESTATIC, MethodHelper.INJECT_CLASS_NAME,
77 | cell.agentName, cell.agentDesc, cell.paramsStart,
78 | cell.paramsCount, cell.opcodes);
79 | }
80 | methodVisitor.visitInsn(opcode);
81 | }
82 |
83 | };
84 | }
85 | }
86 |
87 | } catch (Exception e) {
88 | e.printStackTrace();
89 | }
90 | }
91 | return super.visitMethod(access, name, desc, signature, exceptions);
92 | }
93 |
94 |
95 | void visitMethodWithLoadedParams(MethodVisitor methodVisitor, int opcode, String owner, String methodName, String methodDesc,
96 | int start, int count, int[] paramOpcodes) {
97 | methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
98 |
99 | for (int i = start; i < start + count; i++) {
100 | methodVisitor.visitVarInsn(paramOpcodes[(i - start)], i);
101 | }
102 | methodVisitor.visitInsn(Opcodes.ACONST_NULL);
103 | methodVisitor.visitMethodInsn(opcode, owner, methodName, methodDesc, false);
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/helper/FieldAnnotationVisitor.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack.helper;
2 |
3 | import org.objectweb.asm.AnnotationVisitor;
4 | import org.objectweb.asm.FieldVisitor;
5 |
6 | //其实我已经不用了 但是保留下历史吧 毕竟以前菜过
7 | public abstract class FieldAnnotationVisitor extends FieldVisitor {
8 |
9 | private final String annotationName = "Lcom/wallstreetcn/sample/adapter/Test;";
10 | public String fieldName, fieldDesc;
11 |
12 | public FieldAnnotationVisitor(int i, FieldVisitor fieldVisitor, String fieldName, String fieldDesc) {
13 | super(i, fieldVisitor);
14 | this.fieldName = fieldName;
15 | this.fieldDesc = fieldDesc;
16 | }
17 |
18 |
19 | @Override
20 | public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
21 | if (desc.equals(annotationName)) {
22 | hasAnnotation(fieldName, fieldDesc);
23 | }
24 | return super.visitAnnotation(desc, visible);
25 | }
26 |
27 | abstract void hasAnnotation(String fieldName, String fieldDesc);
28 | }
29 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/helper/FragmentTrackHelper.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack.helper
2 |
3 | import org.objectweb.asm.Opcodes
4 | import org.objectweb.asm.Opcodes.INVOKESTATIC
5 | import org.objectweb.asm.Opcodes.INVOKEVIRTUAL
6 | import org.objectweb.asm.tree.*
7 |
8 | /**
9 | * @Author LiABao
10 | * @Since 2021/1/6
11 | */
12 | fun ClassNode.insert(methodNode: MethodNode) {
13 | val instructions = methodNode.instructions
14 | instructions?.iterator()?.forEach {
15 | if ((it.opcode >= Opcodes.IRETURN && it.opcode <= Opcodes.RETURN) || it.opcode == Opcodes.ATHROW) {
16 | instructions.insertBefore(
17 | it, FieldInsnNode(Opcodes.GETSTATIC, "com/wallstreetcn/testmodule/KronosContext",
18 | "INSTANCE", "Lcom/wallstreetcn/testmodule/KronosContext;")
19 | )
20 | instructions.insertBefore(
21 | it, MethodInsnNode(Opcodes.INVOKEVIRTUAL, "com/wallstreetcn/testmodule/KronosContext",
22 | "getApp", "()Landroid/app/Application;", false)
23 | )
24 | instructions.insertBefore(
25 | it, TypeInsnNode(Opcodes.NEW, "java/lang/StringBuilder")
26 | )
27 | instructions.insertBefore(
28 | it, InsnNode(Opcodes.DUP)
29 | )
30 | instructions.insertBefore(
31 | it, MethodInsnNode(
32 | Opcodes.INVOKESPECIAL, "java/lang/StringBuilder",
33 | "", "()V", false)
34 | )
35 | instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 0))
36 | instructions.insertBefore(
37 | it, MethodInsnNode(INVOKEVIRTUAL, "java/lang/Object",
38 | "getClass", "()Ljava/lang/Class;", false)
39 | )
40 | instructions.insertBefore(
41 | it, MethodInsnNode(
42 | INVOKEVIRTUAL, "java/lang/Class", "getName",
43 | "()Ljava/lang/String;", false
44 | )
45 | )
46 | instructions.insertBefore(
47 | it, MethodInsnNode(
48 | INVOKEVIRTUAL, "java/lang/StringBuilder",
49 | "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false
50 | )
51 | )
52 | instructions.insertBefore(it, VarInsnNode(Opcodes.ILOAD, 1))
53 | instructions.insertBefore(
54 | it, MethodInsnNode(
55 | INVOKEVIRTUAL, "java/lang/StringBuilder",
56 | "append", "(Z)Ljava/lang/StringBuilder;", false
57 | )
58 | )
59 | instructions.insertBefore(
60 | it, MethodInsnNode(
61 | INVOKEVIRTUAL, "java/lang/StringBuilder",
62 | "toString", "()Ljava/lang/String;", false
63 | )
64 | )
65 | instructions.insertBefore(
66 | it, MethodInsnNode(
67 | INVOKESTATIC, "com/wallstreetcn/testmodule/KronosContextKt",
68 | "show", "(Landroid/app/Application;Ljava/lang/String;)V", false
69 | )
70 | )
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/helper/MethodCell.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack.helper;
2 |
3 | public class MethodCell {
4 | // 原方法名
5 | public String name;
6 | // 原方法描述
7 | public String desc;
8 | // 方法所在的接口或类
9 | public String parent;
10 | // 采集数据的方法名
11 | public String agentName;
12 | // 采集数据的方法描述
13 | public String agentDesc;
14 | // 采集数据的方法参数起始索引( 0:this,1+:普通参数 )
15 | public int paramsStart;
16 | // 采集数据的方法参数个数
17 | public int paramsCount;
18 | // 参数类型对应的ASM指令,加载不同类型的参数需要不同的指令
19 | /*
20 | ('I',Opcodes.ILOAD);// I: int , retrieve integer from local variable
21 | ('Z',Opcodes.ILOAD);// Z: bool , retrieve integer from local variable
22 | ('J',Opcodes.LLOAD);// J: long , retrieve long from local variable
23 | ('F',Opcodes.FLOAD);// F: float , retrieve float from local variable
24 | ('D',Opcodes.DLOAD);// D: double , retrieve double from local variable
25 | */
26 | public int[] opcodes;
27 |
28 | public MethodCell(String name, String desc, String parent, String agentName, String agentDesc, int paramsStart,
29 | int paramsCount, int[] opcodes) {
30 | this.name = name;
31 | this.desc = desc;
32 | this.parent = parent;
33 | this.agentName = agentName;
34 | this.agentDesc = agentDesc;
35 | this.paramsStart = paramsStart;
36 | this.paramsCount = paramsCount;
37 | this.opcodes = opcodes;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/helper/MethodHelper.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack.helper;
2 |
3 | import org.objectweb.asm.Opcodes;
4 |
5 | import java.util.HashMap;
6 |
7 | class MethodHelper {
8 | public final static String INJECT_CLASS_NAME = "com/wallstreetcn/sample/ToastHelper";
9 | public final static HashMap sInterfaceMethods = new HashMap<>();
10 |
11 | static {
12 | sInterfaceMethods.put("onClick(Landroid/view/View;)V", new MethodCell(
13 | "onClick",
14 | "(Landroid/view/View;)V",
15 | "android/view/View$OnClickListener",
16 | "toast",
17 | "(Ljava/lang/Object;Landroid/view/View;Ljava/lang/Object;)V",
18 | 1, 1,
19 | new int[]{Opcodes.ALOAD}));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/helper/ModifyUtils.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack.helper;
2 |
3 |
4 | import org.objectweb.asm.ClassReader;
5 | import org.objectweb.asm.ClassVisitor;
6 | import org.objectweb.asm.ClassWriter;
7 |
8 | import java.io.IOException;
9 |
10 |
11 | class ModifyUtils {
12 |
13 | public static byte[] modifyClass(byte[] srcClass) throws IOException {
14 | ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
15 | ClassVisitor methodFilterCV = new ClassFilterVisitor(classWriter);
16 | ClassReader cr = new ClassReader(srcClass);
17 | cr.accept(methodFilterCV, ClassReader.SKIP_DEBUG);
18 | return classWriter.toByteArray();
19 | }
20 |
21 |
22 | private static boolean instanceOfFragment(String superName) {
23 | return superName.equals("android/app/Fragment") || superName.equals("android/support/v4/app/Fragment");
24 | }
25 |
26 |
27 | static boolean isRecyclerViewHolder(String superName) {
28 | return superName.equals("androidx/recyclerview/widget/RecyclerView$ViewHolder");
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/helper/RecyclerViewHolderImp.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack.helper
2 |
3 | import com.kronos.plugin.base.asm.methodEnd
4 | import org.objectweb.asm.Opcodes
5 | import org.objectweb.asm.tree.*
6 |
7 | /**
8 | *
9 | * @Author LiABao
10 | * @Since 2021/8/29
11 | *
12 | */
13 | class RecyclerViewHolderImp(classNode: ClassNode) {
14 |
15 | init {
16 | if (isRecyclerViewHolder(classNode.superName)) {
17 | classNode.methods.firstOrNull {
18 | it.name == ""
19 | }?.apply {
20 | instructions.forEach {
21 | if (it.methodEnd()) {
22 | instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 1))
23 | instructions.insertBefore(it, TypeInsnNode(Opcodes.NEW,
24 | "com/wallstreetcn/sample/viewexpose/AutoExposeImp"))
25 | instructions.insertBefore(it, InsnNode(Opcodes.DUP))
26 | instructions.insertBefore(it, VarInsnNode(Opcodes.ALOAD, 0))
27 | instructions.insertBefore(it, MethodInsnNode(Opcodes.INVOKESPECIAL, "com/wallstreetcn/sample/viewexpose/AutoExposeImp",
28 | "", "(Landroidx/recyclerview/widget/RecyclerView\$ViewHolder;)V", false))
29 | instructions.insertBefore(it, MethodInsnNode(Opcodes.INVOKESTATIC, "com/wallstreetcn/sample/viewexpose/ItemViewExtensionKt",
30 | "addExposeListener", "(Landroid/view/View;Lcom/wallstreetcn/sample/viewexpose/OnExposeListener;)V", false))
31 | }
32 | }
33 | }
34 | }
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/scan/DataClassManager.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack.scan
2 |
3 | /**
4 | * @Author LiABao
5 | * @Since 2021/4/22
6 | */
7 | class DataClassManager private constructor() {
8 |
9 | val datList = mutableListOf()
10 |
11 | companion object {
12 | val INSTANCE = DataClassManager()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Plugin/AutoTrackPlugin/src/main/java/com/wallstreetcn/autotrack/scan/DataScanAsmHelper.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.autotrack.scan
2 |
3 | import com.kronos.plugin.base.AsmHelper
4 | import com.kronos.plugin.base.Log
5 | import com.wallstreetcn.autotrack.scan.DataClassManager.Companion.INSTANCE
6 | import org.objectweb.asm.ClassReader
7 | import org.objectweb.asm.Opcodes
8 | import org.objectweb.asm.tree.ClassNode
9 |
10 | /**
11 | * @Author LiABao
12 | * @Since 2021/1/9
13 | */
14 | class DataScanAsmHelper : AsmHelper {
15 |
16 | override fun modifyClass(srcClass: ByteArray?): ByteArray {
17 | val classNode = ClassNode(Opcodes.ASM5)
18 | val classReader = ClassReader(srcClass)
19 | //1 将读入的字节转为classNode
20 | classReader.accept(classNode, 0)
21 | if (classNode.superName == "java/lang/Object" || classNode.interfaces.contains(PARCEL)) {
22 | INSTANCE.datList.add(classNode.name)
23 | }
24 |
25 | return requireNotNull(srcClass)
26 | }
27 |
28 | companion object {
29 | const val PARCEL = "android/os/Parcelable"
30 | }
31 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/Plugin/BasePlugin/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("kotlin")
3 | id("kotlin-kapt")
4 | }
5 |
6 | dependencies {
7 | implementation gradleApi()
8 | api "com.android.tools.build:gradle-api:$apgVersion"
9 | api "com.android.tools.build:gradle:$apgVersion"
10 | implementation 'commons-io:commons-io:2.11.0'
11 | implementation "commons-codec:commons-codec:1.15"
12 | api 'org.ow2.asm:asm:9.2'
13 | api 'org.ow2.asm:asm-tree:9.2'
14 | }
15 |
16 |
17 | Properties config = new Properties()
18 | config.load(project.file("nexus.properties").newDataInputStream())
19 | group = config.getProperty('nexus_groupId')
20 |
21 |
22 | java {
23 | sourceCompatibility = JavaVersion.VERSION_11
24 | targetCompatibility = JavaVersion.VERSION_11
25 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/nexus.properties:
--------------------------------------------------------------------------------
1 | nexus_versionName=0.2.1
2 | nexus_artifactId=BasePlugin
3 | nexus_groupId=com.kronos.plugin
4 | nexus_description=basePlugin
5 | nexus_fileName=basePlugin
6 | nexus_type=library
7 | nexus_url=https://github.com/Leifzhang/AndroidAutoTrack
8 |
9 |
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/AsmHelper.java:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base;
2 |
3 | import java.io.IOException;
4 |
5 | public interface AsmHelper {
6 | byte[] modifyClass(byte[] srcClass) throws IOException;
7 | }
8 |
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/ClassNameFilter.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base
2 |
3 | interface ClassNameFilter {
4 | fun filter(className: String): Boolean
5 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/ClassUtils.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base
2 |
3 | import java.io.File
4 | import java.io.FileOutputStream
5 |
6 | object ClassUtils {
7 | fun path2Classname(entryName: String): String {
8 | // 感谢大佬 dingshaoran
9 | //ClassUtils.path2Classname(className); File.separator donot match jar entryName on windows
10 | return entryName.replace(".class", "")
11 | .replace('\\', '.')
12 | .replace('/', '.')
13 | }
14 |
15 | fun checkClassName(className: String): Boolean {
16 | if (className.contains("R\$")) {
17 | return false
18 | }
19 | if (className.endsWith("R.class")) {
20 | return false
21 | }
22 | return (!className.contains("R\\$") && !className.endsWith("R")
23 | && !className.endsWith("BuildConfig"))
24 | }
25 |
26 | fun saveFile(mTempDir: File?, modifiedClassBytes: ByteArray?): File? {
27 | val modified: File? = mTempDir
28 | modifiedClassBytes?.apply {
29 | if (mTempDir!!.exists()) {
30 | mTempDir.delete()
31 | }
32 | mTempDir.createNewFile()
33 | val stream = FileOutputStream(mTempDir)
34 | stream.use {
35 | stream.write(modifiedClassBytes)
36 | }
37 | }
38 | return modified
39 | }
40 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/DefaultClassNameFilter.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base
2 |
3 | class DefaultClassNameFilter : ClassNameFilter {
4 |
5 | override fun filter(className: String): Boolean {
6 | whiteList.forEach {
7 | if (className.contains(it)) {
8 | return true
9 | }
10 | }
11 | return false
12 | }
13 |
14 | companion object {
15 | val whiteList = mutableListOf().apply {
16 | add("kotlin")
17 | add("org")
18 | add("androidx")
19 | add("android")
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/DeleteCallBack.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base
2 |
3 | interface DeleteCallBack {
4 | fun delete(className: String, classBytes: ByteArray)
5 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/JarUtils.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base
2 |
3 | import com.kronos.plugin.base.utils.DigestUtils
4 | import java.io.File
5 | import java.io.FileOutputStream
6 | import java.util.*
7 | import java.util.jar.JarFile
8 | import java.util.jar.JarOutputStream
9 | import java.util.zip.ZipEntry
10 |
11 | internal object JarUtils {
12 |
13 | fun modifyJarFile(jarFile: File, tempDir: File?, transform: BaseTransform): File {
14 | /** 设置输出到的jar */
15 | val hexName = DigestUtils.md5Hex(jarFile.absolutePath).substring(0, 8)
16 | val optJar = File(tempDir, hexName + jarFile.name)
17 | val jarOutputStream = JarOutputStream(FileOutputStream(optJar))
18 | jarOutputStream.use {
19 | val file = JarFile(jarFile)
20 | val enumeration = file.entries()
21 | enumeration.iterator().forEach { jarEntry ->
22 | val inputStream = file.getInputStream(jarEntry)
23 | val entryName = jarEntry.name
24 | if (entryName.contains("module-info.class") && !entryName.contains("META-INF")) {
25 | Log.info("jar file module-info:$entryName jarFileName:${jarFile.path}")
26 | } else {
27 | val zipEntry = ZipEntry(entryName)
28 | jarOutputStream.putNextEntry(zipEntry)
29 | var modifiedClassBytes: ByteArray? = null
30 | val sourceClassBytes = inputStream.readBytes()
31 | if (entryName.endsWith(".class")) {
32 | try {
33 | modifiedClassBytes = transform.process(entryName, sourceClassBytes)
34 | } catch (ignored: Exception) {
35 | }
36 | }
37 | /**
38 | * 读取原jar
39 | */
40 | if (modifiedClassBytes == null) {
41 | jarOutputStream.write(sourceClassBytes)
42 | } else {
43 | jarOutputStream.write(modifiedClassBytes)
44 | }
45 | jarOutputStream.closeEntry()
46 | }
47 | }
48 | }
49 | return optJar
50 | }
51 |
52 | fun scanJarFile(jarFile: File): HashSet {
53 | val hashSet = HashSet()
54 | val file = JarFile(jarFile)
55 | file.use {
56 | val enumeration = file.entries()
57 | while (enumeration.hasMoreElements()) {
58 | val jarEntry = enumeration.nextElement()
59 | val entryName = jarEntry.name
60 | if (entryName.contains("module-info.class")) {
61 | Log.info("module-info:$entryName")
62 | continue
63 | }
64 | if (entryName.endsWith(".class")) {
65 | hashSet.add(entryName)
66 | }
67 | }
68 | }
69 | return hashSet
70 | }
71 |
72 | fun deleteJarScan(jarFile: File?, removeClasses: List, callBack: DeleteCallBack?) {
73 | /**
74 | * 读取原jar
75 | */
76 | val file = JarFile(jarFile)
77 | file.use {
78 | val enumeration = file.entries()
79 | while (enumeration.hasMoreElements()) {
80 | val jarEntry = enumeration.nextElement()
81 | val entryName = jarEntry.name
82 | if (entryName.endsWith(".class") && removeClasses.contains(entryName)) {
83 | val inputStream = file.getInputStream(jarEntry)
84 | val sourceClassBytes = inputStream.readBytes()
85 | try {
86 | callBack?.delete(entryName, sourceClassBytes)
87 | } catch (ignored: Exception) {
88 | }
89 | }
90 | }
91 | }
92 | }
93 |
94 | fun deleteJarScan(jarFile: File?, callBack: DeleteCallBack?) {
95 | /**
96 | * 读取原jar
97 | */
98 | val file = JarFile(jarFile)
99 | file.use {
100 | val enumeration = file.entries()
101 | while (enumeration.hasMoreElements()) {
102 | val jarEntry = enumeration.nextElement()
103 | val inputStream = file.getInputStream(jarEntry)
104 | val entryName = jarEntry.name
105 | val sourceClassBytes = inputStream.readBytes()
106 | if (entryName.endsWith(".class")) {
107 | try {
108 | callBack?.delete(entryName, sourceClassBytes)
109 | } catch (ignored: Exception) {
110 | }
111 | }
112 | }
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/Log.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base
2 |
3 |
4 | object Log {
5 | @JvmStatic
6 | fun info(msg: Any) {
7 | try {
8 | println((String.format("{%s}", msg.toString())))
9 | } catch (e: Exception) {
10 | e.printStackTrace()
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/PluginProvider.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base
2 |
3 | import org.gradle.api.Plugin
4 | import org.gradle.api.Project
5 |
6 | /**
7 | * @Author LiABao
8 | * @Since 2021/1/26
9 | */
10 | interface PluginProvider {
11 |
12 | fun getPlugin(): Class>
13 |
14 |
15 | fun dependOn(): List
16 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/SetDiff.java:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.Set;
6 |
7 | public class SetDiff {
8 |
9 | private final List addedList = new ArrayList<>();
10 | private final List unchangedList = new ArrayList<>();
11 | private final List removedList = new ArrayList<>();
12 |
13 | public SetDiff(Set beforeList, Set afterList) {
14 | addedList.addAll(afterList); // Will contain only new elements when all elements in the Before-list are removed.
15 | beforeList.forEach(e -> {
16 | boolean b = addedList.remove(e) ? unchangedList.add(e) : removedList.add(e);
17 | });
18 | }
19 |
20 | public List getAddedList() {
21 | return addedList;
22 | }
23 |
24 | public List getUnchangedList() {
25 | return unchangedList;
26 | }
27 |
28 | public List getRemovedList() {
29 | return removedList;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/TransformBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base
2 |
3 | import com.android.build.api.transform.TransformInvocation
4 |
5 | /**
6 | *
7 | * @Author LiABao
8 | * @Since 2021/6/30
9 | *
10 | */
11 | class TransformBuilder(val callBack: TransformCallBack) {
12 |
13 | var transformInvocation: TransformInvocation? = null
14 |
15 | var single: Boolean = false
16 |
17 | var filter: ClassNameFilter? = null
18 |
19 | var simpleScan = false
20 |
21 | var deleteCallBack: DeleteCallBack? = null
22 |
23 | fun build(): BaseTransform {
24 | val transform = BaseTransform(transformInvocation, callBack, single)
25 | transform.also {
26 | it.filter = filter
27 | }
28 | if (simpleScan) {
29 | transform.openSimpleScan()
30 | }
31 | deleteCallBack?.apply {
32 | transform.setDeleteCallBack(this)
33 | }
34 | return transform
35 | }
36 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/TransformCallBack.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base
2 |
3 | interface TransformCallBack {
4 | fun process(className: String, classBytes: ByteArray?): ByteArray?
5 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/asm/AsmExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base.asm
2 |
3 | import org.objectweb.asm.Opcodes
4 | import org.objectweb.asm.tree.AbstractInsnNode
5 |
6 | /**
7 | *
8 | * @Author LiABao
9 | * @Since 2021/8/29
10 | *
11 | */
12 | fun AbstractInsnNode.methodEnd(): Boolean {
13 | return ((this.opcode >= Opcodes.IRETURN && this.opcode <= Opcodes.RETURN) || this.opcode == Opcodes.ATHROW)
14 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/asm/Java9ClassAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base.asm
2 |
3 | import com.kronos.plugin.base.Log
4 | import org.apache.commons.io.FileUtils
5 | import java.io.File
6 |
7 | /**
8 | * @Author LiABao
9 | * @Since 2021/7/21
10 | */
11 |
12 | fun copyIfLegal(srcFile: File?, destFile: File) {
13 | if (srcFile?.name?.contains("module-info") != true) {
14 | try {
15 | srcFile?.apply {
16 | FileUtils.copyFile(srcFile, destFile)
17 | }
18 | } catch (e: Exception) {
19 | e.printStackTrace()
20 | }
21 | } else {
22 | Log.info("copyIfLegal module-info:" + srcFile.name)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/asm/LambdaNodeHelper.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base.asm
2 |
3 | import org.objectweb.asm.Handle
4 | import org.objectweb.asm.Opcodes.ACC_STATIC
5 | import org.objectweb.asm.tree.ClassNode
6 | import org.objectweb.asm.tree.InvokeDynamicInsnNode
7 | import org.objectweb.asm.tree.MethodNode
8 |
9 | /**
10 | *
11 | * @Author LiABao
12 | * @Since 2021/1/13
13 | *
14 | */
15 |
16 | fun ClassNode.lambdaHelper(isStatic: Boolean = false, block: (InvokeDynamicInsnNode) -> Boolean): MutableList {
17 | val lambdaMethodNodes = mutableListOf()
18 | methods?.forEach { method ->
19 | method?.instructions?.iterator()?.forEach {
20 | if (it is InvokeDynamicInsnNode) {
21 | if (block.invoke(it)) {
22 | // Log.info("dynamicName:${it.name} dynamicDesc:${it.desc}")
23 | val args = it.bsmArgs
24 | args.forEach { arg ->
25 | if (arg is Handle) {
26 | val methodNode = findMethodByNameAndDesc(arg.name, arg.desc)
27 | methodNode?.apply {
28 | val hasStatic = access and ACC_STATIC != 0
29 | if (isStatic) {
30 | if (hasStatic) {
31 | lambdaMethodNodes.add(this)
32 | }
33 | } else {
34 | if (!hasStatic) {
35 | lambdaMethodNodes.add(this)
36 | }
37 | }
38 |
39 | }
40 | }
41 | }
42 | }
43 | }
44 | }
45 | }
46 | lambdaMethodNodes.forEach {
47 |
48 | // Log.info("lambdaName:${it.name} lambdaDesc:${it.desc} lambdaAccess:${it.access}")
49 | }
50 | return lambdaMethodNodes
51 |
52 | }
53 |
54 | fun ClassNode.findMethodByNameAndDesc(name: String, desc: String): MethodNode? {
55 | return methods?.firstOrNull {
56 | it.name == name && it.desc == desc
57 | }
58 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/utils/DigestUtils.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base.utils
2 |
3 | import org.apache.commons.codec.binary.Hex
4 | import org.apache.commons.codec.digest.DigestUtils.getMd5Digest
5 | import java.nio.charset.Charset
6 | import kotlin.text.Charsets.UTF_8
7 |
8 | /**
9 | * @Author LiABao
10 | * @Since 2021/7/31
11 | */
12 | object DigestUtils {
13 |
14 | fun md5Hex(data: String?): String {
15 | return Hex.encodeHexString(md5(data)) ?: ""
16 | }
17 |
18 | fun md5(data: String?): ByteArray? {
19 | return md5(data?.getBytes())
20 | }
21 |
22 | fun md5(data: ByteArray?): ByteArray? {
23 | return getMd5Digest().digest(data)
24 | }
25 |
26 | private fun String.getBytes(charset: Charset = UTF_8): ByteArray = this.toByteArray(charset)
27 |
28 | }
--------------------------------------------------------------------------------
/Plugin/BasePlugin/src/main/java/com/kronos/plugin/base/utils/FileUtils.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.base.utils
2 |
3 | import java.io.File
4 |
5 | /**
6 | * @Author LiABao
7 | * @Since 2021/1/4
8 | */
9 |
10 | fun File.filterTest(nameReg: String): Array? {
11 | return listFiles { p0 -> p0?.name == nameReg }
12 | }
13 |
14 | fun File.deleteAll() {
15 | delFolder(path)
16 | }
17 |
18 |
19 | private fun delAllFile(path: String): Boolean {
20 | var flag = false
21 | val file = File(path)
22 | if (!file.exists()) {
23 | return flag
24 | }
25 | if (!file.isDirectory) {
26 | return flag
27 | }
28 | val tempList = file.list()
29 | var temp: File? = null
30 | for (i in tempList.indices) {
31 | temp = if (path.endsWith(File.separator)) {
32 | File(path + tempList[i])
33 | } else {
34 | File(path + File.separator + tempList[i])
35 | }
36 | if (temp.isFile) {
37 | temp.delete()
38 | }
39 | if (temp.isDirectory) {
40 | delAllFile(path + "/" + tempList[i]) // 先删除文件夹里面的文件
41 | delFolder(path + "/" + tempList[i]) // 再删除空文件夹
42 | flag = true
43 | }
44 | }
45 | return flag
46 | }
47 |
48 |
49 | // 删除文件夹
50 | // param folderPath 文件夹完整绝对路径
51 | private fun delFolder(folderPath: String) {
52 | try {
53 | delAllFile(folderPath) // 删除完里面所有内容
54 | val myFilePath = File(folderPath)
55 | myFilePath.delete() // 删除空文件夹
56 | } catch (e: Exception) {
57 | e.printStackTrace()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Plugin/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = '1.7.10'
4 | repositories {
5 | maven {
6 | url 'https://maven.aliyun.com/repository/central/'
7 | }
8 | google()
9 | }
10 | dependencies {
11 | classpath "com.android.tools.build:gradle:$apgVersion"
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | /* maven {
21 | name = "GitHubPackages"
22 | url "https://github.com/Leifzhang/AndroidAutoTrack"
23 | credentials {
24 | username = 'leifzhang'
25 | password = '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'
26 | }
27 | }*/
28 | maven {
29 | url 'https://maven.aliyun.com/repository/central/'
30 | }
31 | google()
32 | }
33 |
34 | configurations.all { Configuration c ->
35 | c.resolutionStrategy {
36 | dependencySubstitution {
37 | all { DependencySubstitution dependency ->
38 | if (dependency.requested instanceof ModuleComponentSelector) {
39 | def p = rootProject.allprojects.find { p -> p.group == dependency.requested.group && p.name == dependency.requested.module }
40 | if (p != null) {
41 | dependency.useTarget(project(p.path), 'selected local project')
42 | }
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
49 | pluginManager.withPlugin("java-library") {
50 | sourceCompatibility = JavaVersion.VERSION_11
51 | targetCompatibility = JavaVersion.VERSION_11
52 | }
53 |
54 | group("com.github.leifzhang")
55 | version = property("plugin.version")
56 | afterEvaluate {
57 | if (project.plugins.hasPlugin('com.android.library') || project.plugins.hasPlugin('com.android.application')) {
58 | def android = project.extensions.getByName('android')
59 | android.compileOptions {
60 | sourceCompatibility JavaVersion.VERSION_11
61 | targetCompatibility JavaVersion.VERSION_11
62 | }
63 | }
64 | }
65 | }
66 |
67 | task clean(type: Delete) {
68 | delete rootProject.buildDir
69 | }
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'groovy'
2 | apply plugin: 'kotlin'
3 | apply plugin: 'java-gradle-plugin'
4 | apply plugin: 'kotlin-kapt'
5 |
6 | dependencies {
7 | implementation gradleApi()
8 | implementation localGroovy()
9 | implementation project(":BasePlugin")
10 | implementation 'commons-io:commons-io:2.6'
11 | implementation 'org.javassist:javassist:3.27.0-GA'
12 | kapt "com.google.auto.service:auto-service:1.0"
13 | implementation 'com.google.auto.service:auto-service:1.0'
14 | }
15 |
16 |
17 | gradlePlugin {
18 | plugins {
19 | version {
20 | // 在 app 模块需要通过 id 引用这个插件
21 | id = 'doubleTap'
22 | // 实现这个插件的类的路径
23 | implementationClass = 'com.kronos.doubletap.DoubleTapPlugin'
24 | }
25 | }
26 | }
27 |
28 | java {
29 | sourceCompatibility = JavaVersion.VERSION_11
30 | targetCompatibility = JavaVersion.VERSION_11
31 | }
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/DoubleTabConfig.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap
2 |
3 | open class DoubleTabConfig {
4 | var injectClassName = ""
5 | var injectFunctionName = ""
6 | fun transform() {
7 | if (injectClassName.isEmpty()) {
8 | ByteCodeInjectClassName = "com/wallstreetcn/sample/DoubleTapCheck"
9 | } else {
10 | ByteCodeInjectClassName = injectClassName.replace(".", "/")
11 | }
12 | if (injectFunctionName.isEmpty()) {
13 | ByteCodeInjectFunctionName = "isNotDoubleTap"
14 | } else {
15 | ByteCodeInjectFunctionName = injectFunctionName
16 | }
17 | }
18 |
19 | companion object {
20 | @JvmField
21 | var ByteCodeInjectClassName = ""
22 |
23 | @JvmField
24 | var ByteCodeInjectFunctionName = ""
25 | }
26 | }
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/DoubleTapAppTransform.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap
2 |
3 | import com.android.build.api.transform.QualifiedContent
4 | import com.android.build.gradle.internal.pipeline.TransformManager
5 |
6 | /**
7 | * @Author LiABao
8 | * @Since 2021/1/4
9 | */
10 | class DoubleTapAppTransform : DoubleTapTransform() {
11 |
12 | override fun getScopes(): MutableSet {
13 | return mutableSetOf().apply {
14 | addAll(TransformManager.SCOPE_FULL_PROJECT)
15 | }
16 | }
17 |
18 | override fun getInputTypes(): Set? {
19 | return TransformManager.CONTENT_JARS
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/DoubleTapLibraryTransform.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap
2 |
3 | import com.android.build.api.transform.QualifiedContent
4 | import com.android.build.gradle.internal.pipeline.TransformManager
5 | import com.google.common.collect.ImmutableSet
6 |
7 | /**
8 | * @Author LiABao
9 | * @Since 2021/1/4
10 | */
11 | class DoubleTapLibraryTransform : DoubleTapTransform() {
12 |
13 | override fun getScopes(): MutableSet {
14 | return ImmutableSet.of(
15 | QualifiedContent.Scope.PROJECT
16 | )
17 | }
18 |
19 | override fun getInputTypes(): Set? {
20 | return TransformManager.CONTENT_CLASS
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/DoubleTapPlugin.java:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap;
2 |
3 | import com.android.build.gradle.AppExtension;
4 | import com.android.build.gradle.AppPlugin;
5 | import com.android.build.gradle.LibraryExtension;
6 |
7 | import org.gradle.api.Action;
8 | import org.gradle.api.Plugin;
9 | import org.gradle.api.Project;
10 |
11 | public class DoubleTapPlugin implements Plugin {
12 |
13 | private static final String EXT_NAME = "doubleTab";
14 |
15 | @Override
16 | public void apply(Project project) {
17 | boolean isApp = project.getPlugins().hasPlugin(AppPlugin.class);
18 | project.getExtensions().create(EXT_NAME, DoubleTabConfig.class);
19 | project.afterEvaluate(project1 -> {
20 | DoubleTabConfig config = (DoubleTabConfig) project1.getExtensions().findByName(EXT_NAME);
21 | if (config == null) {
22 | config = new DoubleTabConfig();
23 | }
24 | config.transform();
25 | });
26 | if (isApp) {
27 | AppExtension appExtension = project.getExtensions().getByType(AppExtension.class);
28 | appExtension.registerTransform(new DoubleTapAppTransform());
29 | return;
30 | }
31 | if (project.getPlugins().hasPlugin("com.android.library")) {
32 | LibraryExtension libraryExtension = project.getExtensions().getByType(LibraryExtension.class);
33 | libraryExtension.registerTransform(new DoubleTapLibraryTransform());
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/DoubleTapProvider.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap
2 |
3 | import com.google.auto.service.AutoService
4 | import com.kronos.plugin.base.PluginProvider
5 | import org.gradle.api.Plugin
6 | import org.gradle.api.Project
7 |
8 | /**
9 | * @Author LiABao
10 | * @Since 2021/1/27
11 | */
12 | @AutoService(value = [PluginProvider::class])
13 | class DoubleTapProvider : PluginProvider {
14 | override fun getPlugin(): Class> {
15 | return DoubleTapPlugin::class.java
16 | }
17 |
18 | override fun dependOn(): List {
19 | return arrayListOf().apply {
20 | // add("com.kronos.plugin.thread.ThreadHookProvider")
21 | }
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/DoubleTapTransform.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap
2 |
3 | import com.android.build.api.transform.Transform
4 | import com.android.build.api.transform.TransformException
5 | import com.android.build.api.transform.TransformInvocation
6 | import com.kronos.doubletap.helper.DoubleTapAsmHelper
7 | import com.kronos.doubletap.helper.DoubleTapClassNodeHelper
8 | import com.kronos.plugin.base.BaseTransform
9 | import com.kronos.plugin.base.ClassUtils
10 | import com.kronos.plugin.base.TransformCallBack
11 | import java.io.IOException
12 |
13 | abstract class DoubleTapTransform : Transform() {
14 | override fun getName(): String {
15 | return "DoubleTapTransform"
16 | }
17 |
18 | override fun isIncremental(): Boolean {
19 | return true
20 | }
21 |
22 | @Throws(TransformException::class, InterruptedException::class, IOException::class)
23 | override fun transform(transformInvocation: TransformInvocation) {
24 | val injectHelper = DoubleTapClassNodeHelper()
25 | val baseTransform = BaseTransform(transformInvocation, object : TransformCallBack {
26 | override fun process(className: String, classBytes: ByteArray?): ByteArray? {
27 | if (ClassUtils.checkClassName(className)) {
28 | try {
29 | return classBytes?.let { injectHelper.modifyClass(it) }
30 | } catch (e: IOException) {
31 | e.printStackTrace()
32 | }
33 | }
34 | return null
35 | }
36 | })
37 | baseTransform.startTransform()
38 | }
39 |
40 | override fun isCacheable(): Boolean {
41 | return true
42 | }
43 | }
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/helper/CheckVisitor.java:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap.helper;
2 |
3 |
4 | import com.kronos.doubletap.DoubleTabConfig;
5 |
6 | import org.objectweb.asm.Label;
7 | import org.objectweb.asm.MethodVisitor;
8 | import org.objectweb.asm.Opcodes;
9 |
10 | public class CheckVisitor extends MethodVisitor {
11 | private String owner;
12 |
13 | CheckVisitor(MethodVisitor mv, String owner) {
14 | super(Opcodes.ASM5, mv);
15 | this.owner = owner;
16 | }
17 |
18 |
19 | @Override
20 | public void visitCode() {
21 | mv.visitVarInsn(Opcodes.ALOAD, 0);
22 | mv.visitFieldInsn(Opcodes.GETFIELD, owner, "doubleTap",
23 | String.format("L%s;", DoubleTabConfig.ByteCodeInjectClassName));
24 | mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, DoubleTabConfig.ByteCodeInjectClassName,
25 | DoubleTabConfig.ByteCodeInjectFunctionName, "()Z", false);
26 | Label label = new Label();
27 | mv.visitJumpInsn(Opcodes.IFNE, label);
28 | mv.visitInsn(Opcodes.RETURN);
29 | mv.visitLabel(label);
30 | // super.visitCode();
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/helper/ClassFilterVisitor.java:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap.helper;
2 |
3 | import com.kronos.doubletap.DoubleTabConfig;
4 |
5 | import org.objectweb.asm.ClassVisitor;
6 | import org.objectweb.asm.MethodVisitor;
7 | import org.objectweb.asm.Opcodes;
8 |
9 | import java.util.Map;
10 |
11 | class ClassFilterVisitor extends ClassVisitor {
12 |
13 | private String[] interfaces;
14 | boolean visitedStaticBlock = false;
15 | private String owner;
16 |
17 | ClassFilterVisitor(ClassVisitor classVisitor) {
18 | super(Opcodes.ASM5, classVisitor);
19 | }
20 |
21 | @Override
22 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
23 | super.visit(version, access, name, signature, superName, interfaces);
24 | this.interfaces = interfaces;
25 | if (interfaces != null && interfaces.length > 0) {
26 | for (Map.Entry entry : MethodHelper.sInterfaceMethods.entrySet()) {
27 | MethodCell cell = entry.getValue();
28 | for (String anInterface : interfaces) {
29 | if (anInterface.equals(cell.parent)) {
30 | visitedStaticBlock = true;
31 | this.owner = name;
32 | cv.visitField(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, "doubleTap",
33 | String.format("L%s;", DoubleTabConfig.ByteCodeInjectClassName),
34 | signature, null);
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
41 |
42 | @Override
43 | public MethodVisitor visitMethod(int access, String name,
44 | String desc, String signature, String[] exceptions) {
45 | if (interfaces != null && interfaces.length > 0) {
46 | try {
47 | if (visitedStaticBlock && name.equals("")) {
48 | MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
49 | return new InitBlockVisitor(methodVisitor, owner);
50 | }
51 | MethodCell cell = MethodHelper.sInterfaceMethods.get(name + desc);
52 | if (cell != null) {
53 | for (String anInterface : interfaces) {
54 | if (anInterface.equals(cell.parent)) {
55 | MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
56 | return new CheckVisitor(methodVisitor, owner);
57 | }
58 | }
59 | }
60 | } catch (Exception e) {
61 | e.printStackTrace();
62 | }
63 | }
64 | return super.visitMethod(access, name, desc, signature, exceptions);
65 | }
66 |
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/helper/DoubleTapAsmHelper.java:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap.helper;
2 |
3 |
4 | import com.kronos.plugin.base.AsmHelper;
5 |
6 | import java.io.IOException;
7 |
8 | public class DoubleTapAsmHelper implements AsmHelper {
9 | @Override
10 | public byte[] modifyClass(byte[] srcClass) throws IOException {
11 | return ModifyUtils.modifyClass(srcClass);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/helper/DoubleTapClassNodeHelper.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap.helper
2 |
3 | import com.kronos.doubletap.DoubleTabConfig
4 | import com.kronos.plugin.base.AsmHelper
5 | import com.kronos.plugin.base.asm.lambdaHelper
6 | import org.objectweb.asm.ClassReader
7 | import org.objectweb.asm.ClassWriter
8 | import org.objectweb.asm.Label
9 | import org.objectweb.asm.Opcodes
10 | import org.objectweb.asm.Opcodes.*
11 | import org.objectweb.asm.tree.*
12 | import java.io.IOException
13 |
14 | class DoubleTapClassNodeHelper : AsmHelper {
15 |
16 | private val classNodeMap = hashMapOf()
17 |
18 | @Throws(IOException::class)
19 | override fun modifyClass(srcClass: ByteArray): ByteArray {
20 | val classNode = ClassNode(ASM5)
21 | val classReader = ClassReader(srcClass)
22 | //1 将读入的字节转为classNode
23 | classReader.accept(classNode, 0)
24 | classNodeMap[classNode.name] = classNode
25 | // 判断当前类是否实现了OnClickListener接口
26 | val hasAnnotation = classNode.hasAnnotation()
27 | val className = classNode.outerClass
28 | val parentNode = classNodeMap[className]
29 | val hasKeepAnnotation = if (hasAnnotation) {
30 | true
31 | } else {
32 | parentNode?.hasAnnotation() ?: false
33 | }
34 | if (!hasKeepAnnotation) {
35 | classNode.interfaces?.forEach {
36 | if (it == "android/view/View\$OnClickListener") {
37 | classNode.methods?.forEach { method ->
38 | // 找到onClick 方法
39 | if (method.name == "") {
40 | initFunction(classNode, method)
41 | }
42 | if (method.name == "onClick" && method.desc == "(Landroid/view/View;)V") {
43 | insertTrack(classNode, method)
44 | }
45 | }
46 | }
47 | }
48 | classNode.lambdaHelper {
49 | (it.name == "onClick" && it.desc.contains(")Landroid/view/View\$OnClickListener;"))
50 | }.apply {
51 | if (isNotEmpty()) {
52 | classNode.methods?.forEach { method ->
53 | if (method.name == "") {
54 | initFunction(classNode, method)
55 | return@forEach
56 | }
57 | }
58 | }
59 | }.forEach { method ->
60 | insertTrack(classNode, method)
61 | }
62 | }
63 | //调用Fragment的onHiddenChange方法
64 | val classWriter = ClassWriter(0)
65 | //3 将classNode转为字节数组
66 | classNode.accept(classWriter)
67 | return classWriter.toByteArray()
68 | }
69 |
70 |
71 | private fun insertLambda(node: ClassNode, method: MethodNode) {
72 | // 根据outClassName 获取到外部类的Node
73 |
74 | }
75 |
76 | private fun initFunction(node: ClassNode, method: MethodNode) {
77 | var hasDoubleTap = false
78 | node.fields?.forEach {
79 | if (it.name == "doubleTap") {
80 | hasDoubleTap = true
81 | }
82 | }
83 | if (!hasDoubleTap) {
84 | node.visitField(ACC_PRIVATE + ACC_FINAL, "doubleTap", String.format("L%s;",
85 | DoubleTabConfig.ByteCodeInjectClassName), node.signature, null)
86 | val instructions = method.instructions
87 | method.instructions?.iterator()?.forEach {
88 | if ((it.opcode >= Opcodes.IRETURN && it.opcode <= Opcodes.RETURN) || it.opcode == Opcodes.ATHROW) {
89 | instructions.insertBefore(it, VarInsnNode(ALOAD, 0))
90 | instructions.insertBefore(it, TypeInsnNode(NEW, DoubleTabConfig.ByteCodeInjectClassName))
91 | instructions.insertBefore(it, InsnNode(DUP))
92 | instructions.insertBefore(it, MethodInsnNode(INVOKESPECIAL, DoubleTabConfig.ByteCodeInjectClassName,
93 | "", "()V", false))
94 | instructions.insertBefore(it, FieldInsnNode(PUTFIELD, node.name, "doubleTap",
95 | String.format("L%s;", DoubleTabConfig.ByteCodeInjectClassName)))
96 | }
97 | }
98 | }
99 | }
100 |
101 |
102 | private fun insertTrack(node: ClassNode, method: MethodNode) {
103 | // 判断方法名和方法描述
104 | val instructions = method.instructions
105 | val firstNode = instructions.first
106 | instructions?.insertBefore(firstNode, LabelNode(Label()))
107 | instructions?.insertBefore(firstNode, VarInsnNode(ALOAD, 0))
108 | instructions?.insertBefore(firstNode, FieldInsnNode(GETFIELD, node.name,
109 | "doubleTap", String.format("L%s;", DoubleTabConfig.ByteCodeInjectClassName)))
110 | instructions?.insertBefore(firstNode, MethodInsnNode(INVOKEVIRTUAL, DoubleTabConfig.ByteCodeInjectClassName,
111 | DoubleTabConfig.ByteCodeInjectFunctionName, "()Z", false))
112 | val labelNode = LabelNode(Label())
113 | instructions?.insertBefore(firstNode, JumpInsnNode(IFNE, labelNode))
114 | instructions?.insertBefore(firstNode, InsnNode(RETURN))
115 | instructions?.insertBefore(firstNode, labelNode)
116 | }
117 |
118 |
119 | // 判断Field是否包含注解
120 | private fun ClassNode.hasAnnotation(): Boolean {
121 | var hasAnnotation = false
122 | this.visibleAnnotations?.forEach { annotation ->
123 | // Log.info("name:$name visibleAnnotations:${annotation.desc} ")
124 | if (annotation.desc == "Lcom/wallstreetcn/sample/adapter/Test;") {
125 | hasAnnotation = true
126 | }
127 | }
128 | this.invisibleAnnotations?.forEach { annotation ->
129 | // Log.info("name:$name visibleAnnotations:${annotation.desc} ")
130 | if (annotation.desc == "Lcom/wallstreetcn/sample/adapter/Test;") {
131 | hasAnnotation = true
132 | }
133 | }
134 | return hasAnnotation
135 | }
136 | }
137 |
138 |
139 |
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/helper/InitBlockVisitor.java:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap.helper;
2 |
3 | import com.kronos.doubletap.DoubleTabConfig;
4 |
5 | import org.objectweb.asm.MethodVisitor;
6 | import org.objectweb.asm.Opcodes;
7 |
8 | public class InitBlockVisitor extends MethodVisitor {
9 | private String owner;
10 |
11 | InitBlockVisitor(MethodVisitor mv, String owner) {
12 | super(Opcodes.ASM5, mv);
13 | this.owner = owner;
14 | }
15 |
16 | @Override
17 | public void visitInsn(int opcode) {
18 | if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
19 | || opcode == Opcodes.ATHROW) {
20 | mv.visitVarInsn(Opcodes.ALOAD, 0);
21 | mv.visitTypeInsn(Opcodes.NEW, DoubleTabConfig.ByteCodeInjectClassName);
22 | mv.visitInsn(Opcodes.DUP);
23 | mv.visitMethodInsn(Opcodes.INVOKESPECIAL, DoubleTabConfig.ByteCodeInjectClassName,
24 | "", "()V", false);
25 | mv.visitFieldInsn(Opcodes.PUTFIELD, owner, "doubleTap",
26 | String.format("L%s;", DoubleTabConfig.ByteCodeInjectClassName));
27 | }
28 | super.visitInsn(opcode);
29 | }
30 |
31 | public void visitMaxs(int maxStack, int maxLocals) {
32 | // The values 3 and 0 come from the fact that our instance
33 | // creation uses 3 stack slots to construct the instances
34 | // above and 0 local variables.
35 | final int ourMaxStack = 3;
36 | final int ourMaxLocals = 0;
37 |
38 | // now, instead of just passing original or our own
39 | // visitMaxs numbers to super, we instead calculate
40 | // the maximum values for both.
41 | super.visitMaxs(Math.max(ourMaxStack, maxStack), Math.max(ourMaxLocals, maxLocals));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/helper/MethodCell.java:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap.helper;
2 |
3 | public class MethodCell {
4 | // 原方法名
5 | String name;
6 | // 原方法描述
7 | String desc;
8 | // 方法所在的接口或类
9 | String parent;
10 | // 采集数据的方法名
11 | String agentName;
12 | // 采集数据的方法描述
13 | String agentDesc;
14 | // 采集数据的方法参数起始索引( 0:this,1+:普通参数 )
15 | int paramsStart;
16 | // 采集数据的方法参数个数
17 | int paramsCount;
18 | // 参数类型对应的ASM指令,加载不同类型的参数需要不同的指令
19 | /*
20 | ('I',Opcodes.ILOAD);// I: int , retrieve integer from local variable
21 | ('Z',Opcodes.ILOAD);// Z: bool , retrieve integer from local variable
22 | ('J',Opcodes.LLOAD);// J: long , retrieve long from local variable
23 | ('F',Opcodes.FLOAD);// F: float , retrieve float from local variable
24 | ('D',Opcodes.DLOAD);// D: double , retrieve double from local variable
25 | */
26 | int[] opcodes;
27 |
28 | public MethodCell(String name, String desc, String parent, String agentName, String agentDesc, int paramsStart,
29 | int paramsCount, int[] opcodes) {
30 | this.name = name;
31 | this.desc = desc;
32 | this.parent = parent;
33 | this.agentName = agentName;
34 | this.agentDesc = agentDesc;
35 | this.paramsStart = paramsStart;
36 | this.paramsCount = paramsCount;
37 | this.opcodes = opcodes;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/helper/MethodHelper.java:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap.helper;
2 |
3 | import org.objectweb.asm.Opcodes;
4 |
5 | import java.util.HashMap;
6 |
7 | class MethodHelper {
8 | public final static HashMap sInterfaceMethods = new HashMap<>();
9 |
10 | static {
11 | sInterfaceMethods.put("onClick(Landroid/view/View;)V", new MethodCell(
12 | "onClick",
13 | "(Landroid/view/View;)V",
14 | "android/view/View$OnClickListener",
15 | "toast",
16 | "(Ljava/lang/Object;Landroid/view/View;Ljava/lang/Object;)V",
17 | 1, 1, new int[]{Opcodes.ALOAD}));
18 | sInterfaceMethods.put("onClick(Landroid/content/DialogInterface;I)V", new MethodCell(
19 | "onClick",
20 | "(Landroid/content/DialogInterface;I)V",
21 | "android/content/DialogInterface$OnClickListener",
22 | "onClick",
23 | "(Ljava/lang/Object;Landroid/content/DialogInterface;I)V",
24 | 0, 3,
25 | new int[]{Opcodes.ALOAD, Opcodes.ALOAD, Opcodes.ILOAD}));
26 |
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Plugin/double_tap_plugin/src/main/java/com/kronos/doubletap/helper/ModifyUtils.java:
--------------------------------------------------------------------------------
1 | package com.kronos.doubletap.helper;
2 |
3 | import org.objectweb.asm.ClassReader;
4 | import org.objectweb.asm.ClassVisitor;
5 | import org.objectweb.asm.ClassWriter;
6 |
7 | import java.io.IOException;
8 |
9 |
10 | public class ModifyUtils {
11 |
12 |
13 | static byte[] modifyClass(byte[] srcClass) throws IOException {
14 | ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
15 | ClassVisitor methodFilterCV = new ClassFilterVisitor(classWriter);
16 | ClassReader cr = new ClassReader(srcClass);
17 | cr.accept(methodFilterCV, ClassReader.SKIP_DEBUG);
18 | return classWriter.toByteArray();
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Plugin/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=obsolete
22 | plugin.version=0.3.0
23 |
24 | apgVersion=7.2.2
--------------------------------------------------------------------------------
/Plugin/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/Plugin/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/Plugin/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Dec 25 19:19:41 CST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Plugin/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/Plugin/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/Plugin/multiPlugin/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/Plugin/multiPlugin/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'kotlin'
3 | id 'java-gradle-plugin'
4 | }
5 |
6 | dependencies {
7 | implementation localGroovy()
8 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
9 | implementation project(":BasePlugin")
10 | implementation 'com.google.auto.service:auto-service:1.0'
11 | }
12 |
13 | gradlePlugin {
14 | plugins {
15 | version {
16 | // 在 app 模块需要通过 id 引用这个插件
17 | id = 'multi-plugin'
18 | // 实现这个插件的类的路径
19 | implementationClass = 'com.kronos.plugin.multi.MultiPlugin'
20 | }
21 | }
22 | }
23 | java {
24 | sourceCompatibility = JavaVersion.VERSION_11
25 | targetCompatibility = JavaVersion.VERSION_11
26 | }
27 |
--------------------------------------------------------------------------------
/Plugin/multiPlugin/src/main/java/com/kronos/plugin/multi/MultiPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.multi
2 |
3 | import com.kronos.plugin.base.PluginProvider
4 | import com.kronos.plugin.multi.graph.Analyzer
5 | import com.kronos.plugin.multi.graph.ModuleNode
6 | import org.gradle.api.Plugin
7 | import org.gradle.api.Project
8 | import java.util.*
9 |
10 | class MultiPlugin : Plugin {
11 |
12 | override fun apply(project: Project) {
13 | // 菜虾版本byteX beta版本
14 | val providers = ServiceLoader.load(PluginProvider::class.java).toList()
15 | val graph = mutableListOf()
16 | val map = hashMapOf()
17 | providers.forEach {
18 | val list = it.dependOn()
19 | val className = it.javaClass.name
20 | val meta = ModuleNode(className, list)
21 | graph.add(meta)
22 | map[className] = it
23 | }
24 | val analyzer = Analyzer(graph)
25 | analyzer.bfsSort()
26 | analyzer.analyze()
27 | analyzer.bfsSortList.forEach {
28 | map[it.moduleName]?.apply {
29 | project.plugins.apply(getPlugin())
30 | }
31 | }
32 |
33 | }
34 | }
--------------------------------------------------------------------------------
/Plugin/multiPlugin/src/main/java/com/kronos/plugin/multi/graph/Analyzer.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.multi.graph
2 |
3 | import com.kronos.plugin.base.Log
4 | import org.gradle.internal.graph.CachingDirectedGraphWalker
5 | import org.gradle.internal.graph.DirectedGraph
6 | import java.util.concurrent.ConcurrentHashMap
7 |
8 | /**
9 | * Use dfs find the circle, replace to Tarjan algorithm later.
10 | */
11 | class Analyzer(private val libs: MutableList) {
12 |
13 | private val modules = ConcurrentHashMap()
14 | lateinit var bfsSortList: MutableList
15 |
16 | fun bfsSort() {
17 | bfsSortList = ModuleBfsHelper.sort(libs)
18 | Log.info("analyzer graphList:$bfsSortList")
19 | }
20 |
21 | fun analyze() {
22 | val walker = CachingDirectedGraphWalker(object : DirectedGraph {
23 | override fun getNodeValues(node: ModuleNode, values: MutableCollection, connectedNodes: MutableCollection) {
24 | node.taskDependencies.forEach { name ->
25 | modules[name]?.let {
26 | connectedNodes.add(it)
27 | }
28 | }
29 | }
30 | })
31 |
32 | bfsSortList.forEach {
33 | val nodes = arrayListOf()
34 | modules[it.moduleName] = it
35 | nodes.add(it)
36 | synchronized(walker) {
37 | walker.add(nodes)
38 | }
39 | }
40 | val cycles = walker.findCycles()
41 |
42 | check(cycles.isEmpty()) {
43 | var num = 1
44 | val sb = StringBuilder()
45 | sb.append("Found Cycles:\n")
46 |
47 | cycles.forEach { cycle ->
48 | sb.append(" Dependency Cycle ${num++}:\n")
49 | cycle.joinTo(sb, "\n") {
50 | "\t" + it.toString()
51 | }
52 | }
53 | throw NullPointerException(sb.toString())
54 | }
55 |
56 | //return cycles
57 | }
58 |
59 | }
60 |
61 |
62 | interface Node {
63 | val moduleName: String
64 | val taskDependencies: List
65 | }
66 |
--------------------------------------------------------------------------------
/Plugin/multiPlugin/src/main/java/com/kronos/plugin/multi/graph/ModuleBfsHelper.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.multi.graph
2 |
3 | import com.kronos.plugin.base.Log
4 | import java.lang.NullPointerException
5 |
6 | /**
7 | * @Author LiABao
8 | * @Since 2021/3/5
9 | */
10 | object ModuleBfsHelper {
11 |
12 | fun sort(list: MutableList): MutableList {
13 | val taskMap: MutableMap = mutableMapOf()
14 | val taskChildMap: MutableMap> = mutableMapOf()
15 | val result = mutableListOf()
16 | // 入度为 0 的队列
17 | val queue = java.util.ArrayDeque()
18 | val taskIntegerHashMap = HashMap()
19 | // 建立每个 task 的入度关系
20 | list.forEach { task: ModuleNode ->
21 | val taskName = task.moduleName
22 | val size = task.taskDependencies.size
23 | taskIntegerHashMap[taskName] = size
24 | taskMap[taskName] = task
25 | if (size == 0) {
26 | queue.offer(task)
27 | }
28 | }
29 | // 建立每个 task 的 children 关系
30 | list.forEach { module: ModuleNode ->
31 | module.taskDependencies.forEach { taskName: String ->
32 | var moduleList = taskChildMap[taskName.trim()]
33 | if (moduleList == null) {
34 | moduleList = ArrayList()
35 | }
36 | moduleList.add(module)
37 | taskChildMap[taskName] = moduleList
38 | }
39 | }
40 |
41 | taskChildMap.entries.iterator().forEach {
42 | Log.info("key is ${it.key}, value is ${it.value}")
43 | }
44 |
45 | // 使用 BFS 方法获得有向无环图的拓扑排序
46 | while (!queue.isEmpty()) {
47 | val anchorTask = queue.pop()
48 | result.add(anchorTask)
49 | val taskName = anchorTask.moduleName
50 | taskChildMap[taskName]?.forEach { // 遍历所有依赖这个顶点的顶点,移除该顶点之后,如果入度为 0,加入到改队列当中
51 | val key = it.moduleName
52 | var resultInt = taskIntegerHashMap[key] ?: 0
53 | resultInt--
54 | if (resultInt == 0) {
55 | queue.offer(it)
56 | }
57 | taskIntegerHashMap[key] = resultInt
58 | }
59 | }
60 |
61 | return result
62 | }
63 | }
--------------------------------------------------------------------------------
/Plugin/multiPlugin/src/main/java/com/kronos/plugin/multi/graph/ModuleNode.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.multi.graph
2 |
3 | data class ModuleNode(
4 | override val moduleName: String,
5 | override var taskDependencies: List
6 | ) : Node {
7 |
8 | override fun equals(other: Any?): Boolean {
9 | if (this === other) return true
10 | if (javaClass != other?.javaClass) return false
11 |
12 | other as ModuleNode
13 |
14 | if (moduleName != other.moduleName) return false
15 |
16 | return true
17 | }
18 |
19 | override fun hashCode(): Int {
20 | return moduleName.hashCode()
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/Plugin/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':double_tap_plugin'
2 | include ':AutoTrackPlugin'
3 | include ':BasePlugin'
4 | include ':thread_hook_plugin'
5 | include ':multiPlugin'
6 | rootProject.name = "Plugin"
7 |
8 |
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 | apply plugin: 'java-gradle-plugin'
3 | apply plugin: 'kotlin-kapt'
4 |
5 | dependencies {
6 | implementation gradleApi()
7 | implementation localGroovy()
8 | implementation project(":BasePlugin")
9 | implementation 'commons-io:commons-io:2.11.0'
10 | implementation 'org.javassist:javassist:3.27.0-GA'
11 | implementation 'com.google.auto.service:auto-service:1.0'
12 | kapt "com.google.auto.service:auto-service:1.0"
13 | }
14 |
15 | java {
16 | sourceCompatibility = JavaVersion.VERSION_11
17 | targetCompatibility = JavaVersion.VERSION_11
18 | }
19 |
20 | gradlePlugin {
21 | plugins {
22 | version {
23 | // 在 app 模块需要通过 id 引用这个插件
24 | id = 'thread-hook'
25 | // 实现这个插件的类的路径
26 | implementationClass = 'com.kronos.plugin.thread.ThreadHookPlugin'
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/HookTransform.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread
2 |
3 | import com.android.build.api.transform.QualifiedContent
4 | import com.android.build.api.transform.Transform
5 | import com.android.build.api.transform.TransformInvocation
6 | import com.android.build.gradle.internal.pipeline.TransformManager
7 | import com.kronos.plugin.base.BaseTransform
8 | import com.kronos.plugin.base.ClassUtils
9 | import com.kronos.plugin.base.TransformCallBack
10 | import com.kronos.plugin.thread.visitor.ThreadAsmHelper
11 |
12 | /**
13 | * @Author LiABao
14 | * @Since 2020/10/12
15 | */
16 | class HookTransform : Transform() {
17 | override fun getName(): String {
18 | return "HookTransform"
19 | }
20 |
21 | override fun getInputTypes(): MutableSet {
22 | return mutableSetOf().apply {
23 | addAll(TransformManager.CONTENT_JARS)
24 | }
25 | }
26 |
27 | override fun isIncremental(): Boolean {
28 | return true
29 | }
30 |
31 | override fun getScopes(): MutableSet {
32 | return mutableSetOf().apply {
33 | addAll(TransformManager.SCOPE_FULL_PROJECT)
34 | }
35 | }
36 |
37 | override fun transform(transformInvocation: TransformInvocation?) {
38 | val helper = ThreadAsmHelper()
39 | val baseTransform = BaseTransform(transformInvocation, object : TransformCallBack {
40 | override fun process(className: String, classBytes: ByteArray?): ByteArray? {
41 | return if (ClassUtils.checkClassName(className)) {
42 | helper.modifyClass(classBytes)
43 | } else {
44 | null
45 | }
46 | }
47 | })
48 | baseTransform.startTransform()
49 | }
50 | }
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/PoolEntity.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread
2 |
3 | class PoolEntity(
4 | val code: Int,
5 | val owner: String,
6 | val name: String,
7 | val desc: String,
8 | val methodName: String = "getTHREAD_POOL_SHARE"
9 | ) {
10 |
11 | fun replaceDesc(): String {
12 | val index = desc.lastIndexOf(")")
13 | return desc.substring(0, index + 1) + ClassName
14 | }
15 |
16 | companion object {
17 | const val ClassName = "Lcom/wallstreetcn/sample/utils/TestIOThreadExecutor;"
18 | const val Owner = "com/wallstreetcn/sample/utils/TestIOThreadExecutor"
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/PrivacyClassVisitorFactory.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread
2 |
3 | import com.android.build.api.instrumentation.AsmClassVisitorFactory
4 | import com.android.build.api.instrumentation.ClassContext
5 | import com.android.build.api.instrumentation.ClassData
6 | import com.android.build.api.instrumentation.InstrumentationParameters
7 | import com.kronos.plugin.thread.visitor.privacy.PrivacyClassNode
8 | import org.gradle.api.provider.Property
9 | import org.gradle.api.tasks.Input
10 | import org.objectweb.asm.ClassVisitor
11 |
12 | /**
13 | *
14 | * @Author LiABao
15 | * @Since 2021/10/5
16 | *
17 | */
18 | abstract class PrivacyClassVisitorFactory : AsmClassVisitorFactory {
19 |
20 | override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {
21 | return PrivacyClassNode(nextClassVisitor)
22 | }
23 |
24 | override fun isInstrumentable(classData: ClassData): Boolean {
25 | return true
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/ThreadHookPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread
2 |
3 | import com.android.build.api.artifact.SingleArtifact
4 | import com.android.build.api.instrumentation.FramesComputationMode
5 | import com.android.build.api.instrumentation.InstrumentationScope
6 | import com.android.build.api.variant.AndroidComponentsExtension
7 | import com.android.build.gradle.BaseExtension
8 | import com.kronos.plugin.thread.task.ManifestTask
9 | import org.gradle.api.Plugin
10 | import org.gradle.api.Project
11 |
12 | /**
13 | * @Author LiABao
14 | * @Since 2020/10/12
15 | */
16 | class ThreadHookPlugin : Plugin {
17 |
18 | override fun apply(project: Project) {
19 | val appExtension = project.extensions.getByType(
20 | BaseExtension::class.java
21 | )
22 | appExtension.registerTransform(HookTransform())
23 | val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
24 |
25 | androidComponents.onVariants { variant ->
26 | // artifacts 简单使用
27 | val taskProvider = project.tasks.register(
28 | "manifestCopy${variant.name}Task",
29 | ManifestTask::class.java
30 | )
31 | variant.artifacts.use(taskProvider).wiredWithFiles(
32 | ManifestTask::mergedManifest,
33 | ManifestTask::updatedManifest
34 | ).toTransform(SingleArtifact.MERGED_MANIFEST)
35 | variant.instrumentation.transformClassesWith(PrivacyClassVisitorFactory::class.java,
36 | InstrumentationScope.ALL) {
37 |
38 | }
39 | variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/ThreadHookProvider.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread
2 |
3 | import com.google.auto.service.AutoService
4 | import com.kronos.plugin.base.PluginProvider
5 | import org.gradle.api.Plugin
6 | import org.gradle.api.Project
7 |
8 | /**
9 | * @Author LiABao
10 | * @Since 2021/1/27
11 | */
12 |
13 | @AutoService(value = [PluginProvider::class])
14 | class ThreadHookProvider : PluginProvider {
15 | override fun getPlugin(): Class> {
16 | return ThreadHookPlugin::class.java
17 | }
18 |
19 | override fun dependOn(): List {
20 | return emptyList()
21 | }
22 | }
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/task/ManifestTask.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread.task
2 |
3 | import org.gradle.api.DefaultTask
4 | import org.gradle.api.file.RegularFileProperty
5 | import org.gradle.api.tasks.InputFile
6 | import org.gradle.api.tasks.OutputFile
7 | import org.gradle.api.tasks.TaskAction
8 |
9 | /**
10 | *
11 | * @Author LiABao
12 | * @Since 2022/1/18
13 | *
14 | */
15 | abstract class ManifestTask : DefaultTask() {
16 | // 输入
17 | @get:InputFile
18 | abstract val mergedManifest: RegularFileProperty
19 | // 输出
20 | @get:OutputFile
21 | abstract val updatedManifest: RegularFileProperty
22 |
23 | @TaskAction
24 | fun taskAction() {
25 | val file = mergedManifest.get().asFile.inputStream()
26 | val steam = updatedManifest.get().asFile.outputStream()
27 | steam.use {
28 | it.write(file.readBytes())
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/visitor/PrivacyAsmHelper.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread.visitor
2 |
3 | import com.kronos.plugin.base.AsmHelper
4 | import com.kronos.plugin.thread.visitor.privacy.PrivacyAsmEntity
5 | import com.kronos.plugin.thread.visitor.privacy.PrivacyHelper
6 | import org.objectweb.asm.ClassReader
7 | import org.objectweb.asm.ClassWriter
8 | import org.objectweb.asm.Opcodes
9 | import org.objectweb.asm.tree.ClassNode
10 | import org.objectweb.asm.tree.MethodInsnNode
11 | import org.objectweb.asm.tree.MethodNode
12 |
13 | /**
14 | * @Author LiABao
15 | * @Since 2021/8/9
16 | */
17 | class PrivacyAsmHelper : AsmHelper {
18 |
19 | override fun modifyClass(srcClass: ByteArray?): ByteArray {
20 | val classNode = ClassNode(Opcodes.ASM5)
21 | val classReader = ClassReader(srcClass)
22 | //1 将读入的字节转为classNode
23 | classReader.accept(classNode, 0)
24 | //2 对classNode的处理逻辑
25 | PrivacyHelper.whiteList.let {
26 | val result = it.firstOrNull { name ->
27 | classNode.name.contains(name, true)
28 | }
29 | result
30 | }.apply {
31 | if (this == null) {
32 | println("filter: ${classNode.name}")
33 | }
34 | }
35 | PrivacyHelper.whiteList.firstOrNull {
36 | classNode.name.contains(it, true)
37 | }?.apply {
38 | val iterator: Iterator = classNode.methods.iterator()
39 | // println("classNodeName: ${classNode.name}")
40 | while (iterator.hasNext()) {
41 | val method = iterator.next()
42 | method.instructions?.iterator()?.forEach {
43 | if (it is MethodInsnNode) {
44 | it.isPrivacy()?.apply {
45 | it.opcode = code
46 | it.owner = owner
47 | it.name = name
48 | it.desc = desc
49 | }
50 | }
51 | }
52 | }
53 | }
54 | val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
55 | //3 将classNode转为字节数组
56 | classNode.accept(classWriter)
57 | return classWriter.toByteArray()
58 | }
59 |
60 |
61 | private fun MethodInsnNode.isPrivacy(): PrivacyAsmEntity? {
62 | val pair = PrivacyHelper.privacyList.firstOrNull {
63 | val first = it.first
64 | first.owner == owner && first.code == opcode && first.name == name && first.desc == desc
65 | }
66 | return pair?.second
67 |
68 | }
69 | }
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/visitor/ThreadAsmHelper.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread.visitor
2 |
3 | import com.kronos.plugin.base.AsmHelper
4 | import com.kronos.plugin.thread.PoolEntity.Companion.Owner
5 | import com.kronos.plugin.thread.visitor.ThreadPoolCreator.EXECUTORS_OWNER
6 | import org.objectweb.asm.ClassReader
7 | import org.objectweb.asm.ClassWriter
8 | import org.objectweb.asm.Opcodes
9 | import org.objectweb.asm.tree.ClassNode
10 | import org.objectweb.asm.tree.MethodInsnNode
11 | import org.objectweb.asm.tree.MethodNode
12 | import java.io.IOException
13 |
14 | /**
15 | * @Author LiABao
16 | * @Since 2020/10/14
17 | */
18 | class ThreadAsmHelper : AsmHelper {
19 | @Throws(IOException::class)
20 | override fun modifyClass(srcClass: ByteArray?): ByteArray {
21 | val classNode = ClassNode(Opcodes.ASM5)
22 | val classReader = ClassReader(srcClass)
23 | //1 将读入的字节转为classNode
24 | classReader.accept(classNode, 0)
25 | //2 对classNode的处理逻辑
26 | val iterator: Iterator = classNode.methods.iterator()
27 | while (iterator.hasNext()) {
28 | val method = iterator.next()
29 | method.instructions?.iterator()?.forEach {
30 | if (it.opcode == Opcodes.INVOKESTATIC) {
31 | if (it is MethodInsnNode) {
32 | it.hookExecutors()
33 | }
34 | }
35 | }
36 | }
37 | val classWriter = ClassWriter(0)
38 | //3 将classNode转为字节数组
39 | classNode.accept(classWriter)
40 | return classWriter.toByteArray()
41 | }
42 |
43 | private fun MethodInsnNode.hookExecutors() {
44 | when (this.owner) {
45 | EXECUTORS_OWNER -> {
46 | ThreadPoolCreator.poolList.forEach {
47 | if (it.name == this.name && this.name == it.name && this.owner == it.owner) {
48 | this.owner = Owner
49 | this.name = it.methodName
50 | this.desc = it.replaceDesc()
51 | }
52 | }
53 |
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/visitor/ThreadPoolCreator.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread.visitor
2 |
3 | import com.kronos.plugin.thread.PoolEntity
4 | import org.objectweb.asm.Opcodes
5 |
6 | object ThreadPoolCreator {
7 |
8 | val poolList = mutableListOf()
9 | const val EXECUTORS_OWNER = "java/util/concurrent/Executors"
10 |
11 | init {
12 | val fix = PoolEntity(
13 | Opcodes.INVOKESTATIC,
14 | "java/util/concurrent/Executors",
15 | "newFixedThreadPool",
16 | "(I)Ljava/util/concurrent/ExecutorService;",
17 | "getThreadPool"
18 | )
19 | poolList.add(fix)
20 | var single = PoolEntity(
21 | Opcodes.INVOKESTATIC,
22 | "java/util/concurrent/Executors",
23 | "newSingleThreadExecutor",
24 | "()Ljava/util/concurrent/ExecutorService;",
25 | "getThreadPool"
26 | )
27 | poolList.add(single)
28 | single = PoolEntity(
29 | Opcodes.INVOKESTATIC,
30 | "java/util/concurrent/Executors",
31 | "newSingleThreadExecutor",
32 | "(Ljava/util/concurrent/ThreadFactory;)Ljava/util/concurrent/ExecutorService;",
33 | "getThreadPool"
34 | )
35 | poolList.add(single)
36 | val work = PoolEntity(
37 | Opcodes.INVOKESTATIC,
38 | "java/util/concurrent/Executors",
39 | "newWorkStealingPool",
40 | "(I)Ljava/util/concurrent/ExecutorService;"
41 | )
42 | poolList.add(work)
43 | val cache = PoolEntity(
44 | Opcodes.INVOKESTATIC,
45 | "java/util/concurrent/Executors",
46 | "newCachedThreadPool",
47 | "(I)Ljava/util/concurrent/ExecutorService;",
48 | "getThreadPool"
49 | )
50 | poolList.add(cache)
51 | }
52 | }
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/visitor/ThreadPoolMethodVisitor.java:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread.visitor;
2 |
3 |
4 | import com.kronos.plugin.thread.PoolEntity;
5 |
6 | import org.gradle.internal.impldep.com.esotericsoftware.minlog.Log;
7 | import org.objectweb.asm.MethodVisitor;
8 | import org.objectweb.asm.Opcodes;
9 |
10 | import java.util.List;
11 |
12 | public class ThreadPoolMethodVisitor extends MethodVisitor {
13 |
14 | public ThreadPoolMethodVisitor(MethodVisitor mv) {
15 | super(Opcodes.ASM5, mv);
16 | }
17 |
18 |
19 | @Override
20 | public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
21 | PoolEntity isThreadPool = isThreadPool(opcode, owner, name, desc);
22 | if (isThreadPool != null) {
23 | Log.info("owner:" + owner + " name: " + name + "desc:" + desc);
24 | mv.visitMethodInsn(Opcodes.INVOKESTATIC, PoolEntity.Owner,
25 | isThreadPool.getMethodName(),
26 | isThreadPool.replaceDesc(), itf);
27 | } else {
28 | super.visitMethodInsn(opcode, owner, name, desc, itf);
29 | }
30 | super.visitMethodInsn(opcode, owner, name, desc, itf);
31 | }
32 |
33 |
34 | PoolEntity isThreadPool(int opcode, String owner, String name, String desc) {
35 | List list = ThreadPoolCreator.INSTANCE.getPoolList();
36 | for (PoolEntity poolEntity : list) {
37 | if (opcode != poolEntity.getCode()) {
38 | continue;
39 | }
40 | if (!owner.equals(poolEntity.getOwner())) {
41 | continue;
42 | }
43 | if (!name.equals(poolEntity.getName())) {
44 | continue;
45 | }
46 | if (!desc.equals(poolEntity.getDesc())) {
47 | continue;
48 | }
49 | return poolEntity;
50 | }
51 | return null;
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/visitor/privacy/PrivacyAsmEntity.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread.visitor.privacy
2 |
3 | class PrivacyAsmEntity(
4 | val code: Int,
5 | val owner: String,
6 | val name: String,
7 | val desc: String
8 | )
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/visitor/privacy/PrivacyClassNode.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread.visitor.privacy
2 |
3 | import org.objectweb.asm.ClassVisitor
4 | import org.objectweb.asm.Opcodes
5 | import org.objectweb.asm.tree.ClassNode
6 | import org.objectweb.asm.tree.MethodInsnNode
7 | import org.objectweb.asm.tree.MethodNode
8 |
9 | /**
10 | * @Author LiABao
11 | * @Since 2021/10/5
12 | */
13 | class PrivacyClassNode(private val nextVisitor: ClassVisitor) : ClassNode(Opcodes.ASM5) {
14 | override fun visitEnd() {
15 | super.visitEnd()
16 | PrivacyHelper.whiteList.let {
17 | val result = it.firstOrNull { whiteName ->
18 | name.contains(whiteName, true)
19 | }
20 | result
21 | }.apply {
22 | if (this == null) {
23 | // println("filter: $name")
24 | }
25 | }
26 | PrivacyHelper.whiteList.firstOrNull {
27 | name.contains(it, true)
28 | }?.apply {
29 | val iterator: Iterator = methods.iterator()
30 | while (iterator.hasNext()) {
31 | val method = iterator.next()
32 | method.instructions?.iterator()?.forEach {
33 | if (it is MethodInsnNode) {
34 | it.isPrivacy()?.apply {
35 | println("privacy transform classNodeName: ${name@this}")
36 | it.opcode = code
37 | it.owner = owner
38 | it.name = name
39 | it.desc = desc
40 | }
41 | }
42 | }
43 | }
44 | }
45 | accept(nextVisitor)
46 | }
47 | }
48 |
49 |
50 | private fun MethodInsnNode.isPrivacy(): PrivacyAsmEntity? {
51 | val pair = PrivacyHelper.privacyList.firstOrNull {
52 | val first = it.first
53 | first.owner == owner && first.code == opcode && first.name == name && first.desc == desc
54 | }
55 | return pair?.second
56 |
57 | }
--------------------------------------------------------------------------------
/Plugin/thread_hook_plugin/src/main/java/com/kronos/plugin/thread/visitor/privacy/PrivacyHelper.kt:
--------------------------------------------------------------------------------
1 | package com.kronos.plugin.thread.visitor.privacy
2 |
3 | import org.objectweb.asm.Opcodes
4 |
5 | /**
6 | *
7 | * @Author LiABao
8 | * @Since 2021/9/2
9 | *
10 | */
11 | object PrivacyHelper {
12 |
13 | val privacyList = mutableListOf>().apply {
14 | add(
15 | PrivacyAsmEntity(Opcodes.INVOKEVIRTUAL, "android/telephony/TelephonyManager",
16 | "getDeviceId", "()Ljava/lang/String;") to
17 | PrivacyAsmEntity(Opcodes.INVOKESTATIC, "com/wallstreetcn/sample/utils/PrivacyUtils",
18 | "getImei", "(Landroid/telephony/TelephonyManager;)Ljava/lang/String;")
19 |
20 | )
21 | add(
22 | PrivacyAsmEntity(Opcodes.INVOKEVIRTUAL, "android/net/wifi/WifiInfo",
23 | "getSSID", "()Ljava/lang/String;") to
24 | PrivacyAsmEntity(Opcodes.INVOKESTATIC, "com/wallstreetcn/sample/utils/PrivacyUtils",
25 | "getSSID", "(Landroid/net/wifi/WifiInfo;)Ljava/lang/String;")
26 | )
27 | add(
28 | PrivacyAsmEntity(Opcodes.INVOKEVIRTUAL, "android/net/wifi/WifiInfo",
29 | "getBSSID", "()Ljava/lang/String;") to
30 | PrivacyAsmEntity(Opcodes.INVOKESTATIC, "com/wallstreetcn/sample/utils/PrivacyUtils",
31 | "getBSSID", "(Landroid/net/wifi/WifiInfo;)Ljava/lang/String;")
32 | )
33 | add(
34 | PrivacyAsmEntity(Opcodes.INVOKESTATIC, "android/provider/Settings\$Secure",
35 | "getString", "(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;") to
36 | PrivacyAsmEntity(Opcodes.INVOKESTATIC, "com/wallstreetcn/sample/utils/PrivacyUtils",
37 | "getString", "(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;")
38 | )
39 | add(
40 | PrivacyAsmEntity(Opcodes.INVOKEVIRTUAL, "android/content/pm/PackageManager",
41 | "getInstalledPackages", "(I)Ljava/util/List;") to
42 | PrivacyAsmEntity(Opcodes.INVOKESTATIC, "com/wallstreetcn/sample/utils/PrivacyUtils",
43 | "getAppPackageInfo", "(Landroid/content/pm/PackageManager;I)Ljava/util/List;")
44 | )
45 |
46 | add(
47 | PrivacyAsmEntity(Opcodes.INVOKEVIRTUAL, "android/content/pm/PackageManager",
48 | "getInstalledApplications", "(I)Ljava/util/List;") to
49 | PrivacyAsmEntity(Opcodes.INVOKESTATIC, "com/wallstreetcn/sample/utils/PrivacyUtils",
50 | "getAppPackageInfo", "(Landroid/content/pm/PackageManager;I)Ljava/util/List;")
51 | )
52 |
53 |
54 | }
55 |
56 |
57 | val whiteList = mutableListOf("com/wallstreetcn", "leakcanary","shark").apply {
58 |
59 | }
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AndroidAutoTrack
2 |
3 | [](https://github.com/trekhleb/state-of-the-art-shitcode)
4 |
5 |
6 | 本项目主要就是给大家一个参考学习的demo而已,主要是打算简化学习gradle插件的成本,以及对于`android transform`的一次抽象,将增量更新等等进行一次抽象,以方便大家学习开发。
7 |
8 | # 技术栈罗列
9 |
10 | 1. compose building 混合编译,提供plugin当场调试能力
11 | 2. Transform dependOn 依赖,通过拓扑排序解决依赖问题
12 | 3. 通过`auto-service` 动态组合多个plugin
13 | 4. 自动化埋点demo,透传参数,以及对于onHiddenChange方法覆盖,可以拿来做fragment可视化逻辑判断 (由于都是字节码操作,所以开发人员不需要感知到)
14 | 5. 双击保护优化,解决了d8导致的java8 lambda,动态引用问题
15 | 6. thread pool 构造动态替换成共享
16 | 7. 简单的tree api 使用demo
17 | 8. 多线程字节码操作
18 | 9. transform 增量编译抽象
19 |
20 | ## ~~buildSrc 优化~~
21 |
22 | ~~之前通过buildSrc形式重构项目,不需要本地推aar,同时module可以被同一个buildSrc关联上,方便调试和代码上传。~~
23 |
24 | ## ComposeBuilding 优化
25 |
26 | 通过项目组合编译的方式重构,同样不需要本地推aar,保留了上述所有的优点的同时,由于buildSrc是一个优先编译的工程,所以无法使用项目内的build.gradle,而`ComposeBuild`则由于是一个独立Project,所以可以使用当前下面的所有共用属性。
27 |
28 | [协程 路由 组件化 1+1+1>3 | 掘金年度征文](https://juejin.cn/post/6908232077200588814),文章内有对`ComposeBuilding`的更详细的介绍和使用。
29 |
30 | ## Tips 小贴士
31 |
32 | 直接编译你的项目,观察项目下的build/imtermediates/transform/ 文件夹下面,因为class类android studio已经帮你完成了转化,所以无需担心看不懂的问题。
33 |
34 | 最好各位可以安装一个ASM ByteCode Viewer插件,可以辅助大家快速阅读对应代码。
35 |
36 | 仔细观察编译前java代码和编译后class文件的差别。如果有插入的代码那么代表该插件已经编织代码成功。
37 |
38 | ## dejavu x
39 |
40 | 这次牛逼了,通过最简单的serviceloader机制,把多个plugin通过DI的形式收拢到一起,方便多插件组合接入。
41 |
42 | ```java
43 | class MultiPlugin : Plugin {
44 |
45 | override fun apply(project: Project) {
46 | // 菜虾版本byteX beta版本
47 | val providers = ServiceLoader.load(PluginProvider::class.java).toList()
48 | providers.forEach {
49 | project.plugins.apply(it.getPlugin())
50 | }
51 | }
52 | }
53 |
54 | ```
55 |
56 | 只要把不同的插件的classpath 加载进来,之后在主工程下声明你的合并插件即可直接使用。
57 |
58 |
59 |
60 | ### 依赖任务
61 |
62 | 这次通过有向图的方式重新计算了下plugin之间的依赖关系,这样就可以指定transform的执行顺序了。
63 |
64 | 实现参考了gradle的`CachingDirectedGraphWalker`,有兴趣的大佬可以自行阅读,这部分说实话,算法我不是特别熟悉。
65 |
66 |
67 | ## AutoTrackPlugin 安卓无痕埋点Demo
68 |
69 | ~~以前使用的是`ClassVisitor`,由于无痕埋点相关其实有上下文以及传输数据等等的要求,所以该方案废弃了。~~
70 |
71 | 当前通过`ClassNode`方式实现,ClassNode是类似Ast语法树的一种`ClassVisitor`的实现类,可以通过主动访问的方式,去对当前你需要变更的类进行快速访问逻辑判断,同时由于在外层判断逻辑,所以可以更方便的添加代码组合等,让asm操作更简化。
72 |
73 | 通过编译时检索代码中是否实现了View.OnClickListener接口,然后在onClick方法尾部插入代码打点代码。
74 |
75 | ### 如何将参数传递给打点代码
76 |
77 | 通过标识注解的方式可以将外部的参数直接传输给埋点事件,这样就可以更丰富简单的拓展无痕埋点系统。
78 |
79 | ```java
80 | View.OnClickListener listener=new View.OnClickListener() {
81 | @Test
82 | private Entity mdata;
83 |
84 | @Override
85 | public void onClick(View v) {
86 | mdata = new Entity();
87 | Log.i("MainActivity", v.toString());
88 | Intent intent = new Intent();
89 | intent.setClass(MainActivity.this, SecondActivity.class);
90 | startActivity(intent);
91 | }
92 | });
93 | ```
94 |
95 | ### 完成Fragment hidden开发
96 |
97 | 后续会补充上给fragment activity 生命周期方法补充增强
98 |
99 | ## double tap plugin 双击优化
100 |
101 | ### 新增功能
102 |
103 | 可以让当前双击保护只作用于Module下面,而不是App下面,让同学可以热拔插这部分代码,因为双击保护其实更针对模块开发同学,所以可以直接使用该插件,同时该插件也会对上传AAr生效,放心使用。
104 |
105 | 原理和无痕埋点相似,当前还是保留以前开发无痕埋点的visitor形式。
106 |
107 | 通过`ClassVisitor`的机制访问所有View.OnClickListener的子类,然后插入双击优化的代码块。但是插入的是一个类,所以有一部分逻辑代码,织入操作更为复杂,可以使用gradle插件去更好的学习。
108 |
109 | `InitBlockVisitor` 这个类MethodVisitor会给当前类的init 添加一个成员变量。`DoubleTapCheck doubleTap = new DoubleTapCheck();` 然后在onClick 方法前添加一个逻辑判断。
110 |
111 | ### 使用原则
112 |
113 | 根目录build 添加插件
114 |
115 | ```gradle
116 | buildscript {
117 |
118 | repositories {
119 | maven {
120 | url "file://${rootDir.absolutePath}/.repo"
121 | }
122 | google()
123 | jcenter()
124 |
125 | }
126 | dependencies {
127 | classpath 'com.kronos.doubleTap:double_tap_plugin:0.1.3'
128 | }
129 | }
130 | ```
131 |
132 | app 运行工程下引入插件 同时将你需要插入的代码的className 和functionname 标记在Extension中
133 |
134 | ```gradle
135 | apply plugin: 'doubleTap'
136 |
137 | doubleTab {
138 | injectClassName = "com.a.doubleclickplugin.DoubleTapCheck"
139 | injectFunctionName = "isNotDoubleTap"
140 | }
141 |
142 | ```
143 |
144 |
145 |
146 | ## Thread Hook plugin 线程hook更换
147 |
148 | 通过字节码访问,查找项目内的线程池构造等,发现之后替换成自定义的线程构造。
149 |
150 | ### 方法
151 |
152 | 通过ASM的ClassNode 的方式读取了当前类的所有构造函数,然后判断当前的执行内容是否是需要变魔改的类,如果是则替换他的desc owner name相关。
153 |
154 | ~~~kotlin
155 | class ThreadAsmHelper : AsmHelper {
156 | @Throws(IOException::class)
157 | override fun modifyClass(srcClass: ByteArray): ByteArray {
158 | val classNode = ClassNode(Opcodes.ASM5)
159 | val classReader = ClassReader(srcClass)
160 | //1 将读入的字节转为classNode
161 | classReader.accept(classNode, 0)
162 | //2 对classNode的处理逻辑
163 | val iterator: Iterator =
164 | classNode.methods.iterator()
165 | while (iterator.hasNext()) {
166 | val method = iterator.next()
167 | method.instructions?.iterator()?.forEach {
168 | if (it.opcode == Opcodes.INVOKESTATIC) {
169 | if (it is MethodInsnNode) {
170 | it.hookExecutors(classNode, method)
171 | }
172 | }
173 | }
174 | }
175 | val classWriter = ClassWriter(0)
176 | //3 将classNode转为字节数组
177 | classNode.accept(classWriter)
178 | return classWriter.toByteArray()
179 | }
180 |
181 | private fun MethodInsnNode.hookExecutors(classNode: ClassNode, methodNode: MethodNode) {
182 | when (this.owner) {
183 | EXECUTORS_OWNER -> {
184 | info("owner:${this.owner} name:${this.name} ")
185 | ThreadPoolCreator.poolList.forEach {
186 | if (it.name == this.name && this.name == it.name && this.owner == it.owner) {
187 | this.owner = Owner
188 | this.name = it.methodName
189 | this.desc = it.replaceDesc()
190 | info("owner:${this.owner} name:${this.name} desc:${this.desc} ")
191 | }
192 | }
193 |
194 | }
195 | }
196 | }
197 | }
198 | ~~~
199 |
200 | 最后在编译阶段该类就会被替换成我们想要的类,举个例子`Executors.newSingleThreadExecutor();`变更成`TestIOThreadExecutor.getThreadPool();`。
201 |
202 | ## 升级更新
203 |
204 | ### 多线程操作字节码
205 |
206 | base plugin 代码升级,使用多线程优化,讲字节码操作执行在线程中,之后在主函数等待所有task执行完成之后在结束。
207 |
208 | base plugin 主要是辅助后续有兴趣的同学可以快速的进行transform开发学习,在当前类基础上,可以无视繁琐的增量编译和额外的文件拷贝操作,只专注于Asm的学习。
209 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'multi-plugin'
4 |
5 | android {
6 | compileSdkVersion 30
7 | defaultConfig {
8 | applicationId "com.wallstreetcn.sample"
9 | minSdkVersion 21
10 | targetSdkVersion 30
11 | versionCode 1
12 | versionName "1.0"
13 | multiDexEnabled = true
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled true
19 | shrinkResources true
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | signingConfig signingConfigs.findByName('release') ?: signingConfigs.debug
22 | debuggable false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | debug {
26 |
27 | }
28 | }
29 | }
30 |
31 |
32 | dependencies {
33 | implementation fileTree(include: ['*.jar'], dir: 'libs')
34 | implementation 'androidx.appcompat:appcompat:1.4.2'
35 | implementation 'androidx.recyclerview:recyclerview:1.2.1'
36 | implementation project(":testmodule")
37 | implementation 'androidx.multidex:multidex:2.0.1'
38 | implementation 'com.google.code.gson:gson:2.8.9'
39 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
40 | implementation 'com.squareup.leakcanary:leakcanary-android-process:2.7'
41 | }
42 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/App.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample
2 |
3 | import android.app.Application
4 | import androidx.multidex.MultiDexApplication
5 | import com.wallstreetcn.testmodule.KronosContext
6 |
7 | /**
8 | * @Author LiABao
9 | * @Since 2021/1/4
10 | */
11 | class App : MultiDexApplication() {
12 | override fun onCreate() {
13 | super.onCreate()
14 | KronosContext.app = this
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/DoubleTapCheck.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample
2 |
3 | import android.os.SystemClock
4 | import android.util.Log
5 | import com.wallstreetcn.testmodule.KronosContext
6 | import com.wallstreetcn.testmodule.show
7 | import kotlin.math.abs
8 |
9 | class DoubleTapCheck {
10 | private var timeCheck =
11 | TIME_CHECK
12 |
13 | constructor(int: Int) {
14 | timeCheck = int
15 | }
16 |
17 | constructor() : this(TIME_CHECK)
18 |
19 | private var downTimeTemp: Long = 0
20 |
21 | fun isNotDoubleTap(): Boolean {
22 | Log.i(
23 | "isNotDoubleTap",
24 | "isNotDoubleTap:${abs(downTimeTemp - System.currentTimeMillis()) > timeCheck}"
25 | )
26 | if (abs(downTimeTemp - System.currentTimeMillis()) > timeCheck) {
27 | downTimeTemp = System.currentTimeMillis()
28 | return true
29 | }
30 | KronosContext.requireApplication().show()
31 | return false
32 | }
33 |
34 | fun update() {
35 | downTimeTemp = SystemClock.currentThreadTimeMillis()
36 | }
37 |
38 | companion object {
39 | const val TIME_CHECK = 1000
40 | }
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/Entity.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample;
2 |
3 | import android.os.Parcel;
4 | import android.os.Parcelable;
5 |
6 | import androidx.annotation.NonNull;
7 |
8 | public class Entity implements Parcelable {
9 | private String test = "test";
10 |
11 | @Override
12 | public int describeContents() {
13 | return 0;
14 | }
15 |
16 | @Override
17 | public void writeToParcel(Parcel dest, int flags) {
18 | dest.writeString(this.test);
19 | }
20 |
21 | public Entity() {
22 | }
23 |
24 | public String getTest() {
25 | return test;
26 | }
27 |
28 | public void setTest(String test) {
29 | this.test = test;
30 | }
31 |
32 | @NonNull
33 | @Override
34 | public String toString() {
35 | return "Entity";
36 | }
37 |
38 | protected Entity(Parcel in) {
39 | this.test = in.readString();
40 | }
41 |
42 | public static final Creator CREATOR = new Creator() {
43 | @Override
44 | public Entity createFromParcel(Parcel source) {
45 | return new Entity(source);
46 | }
47 |
48 | @Override
49 | public Entity[] newArray(int size) {
50 | return new Entity[size];
51 | }
52 | };
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/Fragment2.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import java.util.*
6 |
7 | /**
8 | * @Author LiABao
9 | * @Since 2021/1/5
10 | */
11 | open class Fragment2 : Fragment() {
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | retainInstance = true
15 | }
16 | override fun onHiddenChanged(hidden: Boolean) {
17 | super.onHiddenChanged(hidden)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/JavaFragment.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample;
2 |
3 | import android.app.Activity;
4 |
5 | import androidx.fragment.app.Fragment;
6 |
7 | /**
8 | * @Author LiABao
9 | * @Since 2021/1/5
10 | */
11 | public class JavaFragment extends Fragment {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.provider.Settings;
6 | import android.telephony.TelephonyManager;
7 | import android.util.Log;
8 | import android.view.View;
9 |
10 | import androidx.annotation.Keep;
11 | import androidx.annotation.Nullable;
12 | import androidx.appcompat.app.AppCompatActivity;
13 |
14 | import com.wallstreetcn.sample.adapter.Test;
15 | import com.wallstreetcn.sample.utils.PrivacyUtils;
16 | import com.wallstreetcn.sample.utils.TestIOThreadExecutor;
17 |
18 | import java.util.concurrent.ExecutorService;
19 | import java.util.concurrent.Executors;
20 |
21 | @Test
22 | public class MainActivity extends AppCompatActivity implements View.OnClickListener {
23 |
24 | @Nullable
25 | private View.OnClickListener listener = v -> {
26 | Log.i("MainActivity", v.toString());
27 | Intent intent = new Intent();
28 | intent.setClass(MainActivity.this, SecondActivity.class);
29 | startActivity(intent);
30 | };
31 |
32 | ExecutorService service;
33 |
34 | ExecutorService pool = Executors.newSingleThreadExecutor();
35 | @Test
36 | private Entity mdata = new Entity();
37 |
38 | @Override
39 | protected void onCreate(Bundle savedInstanceState) {
40 | super.onCreate(savedInstanceState);
41 | setContentView(R.layout.activity_main);
42 | TestIOThreadExecutor.Companion.getThreadPool("1234");
43 | // service = Executors.newFixedThreadPool(2);
44 | // service = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
45 | findViewById(R.id.textView1).setOnClickListener(new View.OnClickListener() {
46 |
47 | private NewEntity mdata = new NewEntity();
48 |
49 | @Override
50 | public void onClick(View v) {
51 | String test = mdata.getTest();
52 | Log.i("MainActivity", test);
53 | Intent intent = new Intent();
54 | intent.setClass(MainActivity.this, SecondActivity.class);
55 | startActivity(intent);
56 | }
57 | });
58 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
59 | TelephonyManager manager = getSystemService(TelephonyManager.class);
60 | PrivacyUtils.getImei(manager);
61 | String did = manager.getDeviceId();
62 | String androidId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
63 | // String newDid =PrivacyUtils.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
64 | }
65 | }
66 |
67 | @Override
68 | public void onClick(View v) {
69 | Log.i("MainActivity", v.toString());
70 | Intent intent = new Intent();
71 | intent.setClass(MainActivity.this, SecondActivity.class);
72 | startActivity(intent);
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/NewEntity.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample;
2 |
3 | /**
4 | * @Author LiABao
5 | * @Since 2021/4/23
6 | */
7 | public class NewEntity {
8 |
9 | private String test = "test";
10 |
11 | public String getTest() {
12 | return test;
13 | }
14 |
15 | public void setTest(String test) {
16 | this.test = test;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/NewTest.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample;
2 |
3 | public class NewTest {
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/SecondActivity.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample
2 |
3 | import android.content.Context
4 | import android.content.pm.PackageInfo
5 | import android.content.pm.PackageManager
6 | import android.net.wifi.WifiManager
7 | import android.os.Bundle
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import androidx.recyclerview.widget.RecyclerView
11 | import com.wallstreetcn.sample.adapter.TestAdapter
12 | import com.wallstreetcn.sample.utils.PrivacyUtils
13 | import java.util.concurrent.ExecutorService
14 | import java.util.concurrent.Executors
15 |
16 | class SecondActivity : AppCompatActivity() {
17 |
18 | var poolExecutor: ExecutorService? = null
19 |
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 | setContentView(R.layout.activity_second)
24 | val recyclerView = findViewById(R.id.recyclerView)
25 | recyclerView.layoutManager = LinearLayoutManager(this)
26 | recyclerView.adapter = TestAdapter()
27 | poolExecutor = Executors.newFixedThreadPool(2)
28 | val wifiMgr = applicationContext.getSystemService(Context.WIFI_SERVICE)
29 | as WifiManager
30 | /* val info = wifiMgr.connectionInfo
31 | val ssid = info?.bssid*/
32 |
33 | val list: MutableList = mutableListOf()
34 | val pm: PackageManager = packageManager
35 | val installedPackages = pm.getInstalledApplications(PackageManager.GET_META_DATA)
36 | // val newList = PrivacyUtils.getAppPackageInfo(pm, PackageManager.GET_META_DATA)
37 | for (pi in installedPackages) {
38 | // list.add(pi)
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/ToastHelper.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample;
2 |
3 | import android.view.View;
4 | import android.widget.Toast;
5 |
6 | import com.wallstreetcn.sample.utils.ReflectUtil;
7 | import com.wallstreetcn.testmodule.KronosContextKt;
8 |
9 |
10 | public class ToastHelper {
11 | public static void toast(View view) {
12 | String toastText = "插入的代码";
13 | Toast.makeText(view.getContext(), toastText, Toast.LENGTH_LONG).show();
14 | }
15 |
16 | public static void toast(Object text, View view) {
17 | toast(text, view, null);
18 | }
19 |
20 | public static void toast(Object text, View view, Object data) {
21 | try {
22 | String toastText;
23 | if (text instanceof String) {
24 | toastText = (String) text;
25 | } else {
26 | toastText = text.toString();
27 | }
28 | if (data != null) {
29 | toastText += data.toString();
30 | }
31 | Toast.makeText(view.getContext(), toastText, Toast.LENGTH_LONG).show();
32 | } catch (Exception e) {
33 | e.printStackTrace();
34 | }
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/adapter/OtherTestViewHolder.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.adapter;
2 |
3 | import android.util.Log;
4 | import android.view.View;
5 |
6 |
7 | import androidx.annotation.NonNull;
8 | import androidx.recyclerview.widget.RecyclerView;
9 |
10 | import com.wallstreetcn.sample.DoubleTapCheck;
11 | import com.wallstreetcn.sample.ToastHelper;
12 | import com.wallstreetcn.sample.viewexpose.AutoExposeImp;
13 | import com.wallstreetcn.sample.viewexpose.ItemViewExtensionKt;
14 | import com.wallstreetcn.sample.viewexpose.OnExposeListener;
15 |
16 | import java.util.Collections;
17 |
18 | import kotlin.Unit;
19 | import kotlin.jvm.functions.Function1;
20 |
21 | public class OtherTestViewHolder extends RecyclerView.ViewHolder {
22 | private int i = 100;
23 | private DoubleTapCheck doubleTapCheck = new DoubleTapCheck();
24 |
25 |
26 | public OtherTestViewHolder(@NonNull View itemView) {
27 | super(itemView);
28 | itemView.setOnClickListener(v -> {
29 | Log.i("TAG", doubleTapCheck.toString());
30 | });
31 | ItemViewExtensionKt.addExposeListener(itemView, new AutoExposeImp(this));
32 |
33 | String result="(Landroidx/recyclerview/widget/RecyclerView$ViewHolder;)V";
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/adapter/Test.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.adapter
2 |
3 | import java.lang.annotation.Retention
4 | import java.lang.annotation.RetentionPolicy
5 |
6 | @Target(
7 | AnnotationTarget.ANNOTATION_CLASS,
8 | AnnotationTarget.CLASS,
9 | AnnotationTarget.FIELD,
10 | AnnotationTarget.LOCAL_VARIABLE
11 | )
12 | @Retention(RetentionPolicy.RUNTIME)
13 | annotation class Test
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/adapter/TestAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.adapter
2 |
3 | import android.content.Intent
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.TextView
8 | import androidx.recyclerview.widget.RecyclerView
9 | import com.wallstreetcn.sample.Entity
10 | import com.wallstreetcn.sample.R
11 | import com.wallstreetcn.testmodule.ModuleActivity
12 |
13 | class TestAdapter : RecyclerView.Adapter() {
14 | override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder {
15 | return ViewHolder(
16 | LayoutInflater.from(viewGroup.context)
17 | .inflate(R.layout.recycler_item_view, viewGroup, false)
18 | )
19 | }
20 |
21 | override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
22 | viewHolder.bindViewHolder(i)
23 | }
24 |
25 | override fun getItemCount(): Int {
26 | return 300
27 | }
28 |
29 | class ViewHolder constructor(itemView: View) : RecyclerView.ViewHolder(itemView),
30 | View.OnClickListener {
31 |
32 | @Test
33 | private val entity = Entity()
34 |
35 | override fun onClick(v: View?) {
36 | v?.context?.apply {
37 | startActivity(Intent(this, ModuleActivity::class.java))
38 | }
39 | }
40 |
41 | fun bindViewHolder(position: Int) {
42 | itemView.findViewById(R.id.titleTv).text = "这是第" + position + "条目"
43 | itemView.setOnClickListener(this)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/utils/PrivacyUtils.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.utils
2 |
3 | import android.content.ContentResolver
4 | import android.content.Context
5 | import android.content.pm.PackageInfo
6 | import android.content.pm.PackageManager
7 | import android.net.wifi.WifiInfo
8 | import android.net.wifi.WifiManager
9 | import android.provider.Settings
10 | import android.telephony.TelephonyManager
11 |
12 | /**
13 | *
14 | * @Author LiABao
15 | * @Since 2021/8/9
16 | *
17 | */
18 | object PrivacyUtils {
19 | @JvmStatic
20 | fun getImei(manager: TelephonyManager): String {
21 | // manager.deviceId
22 | return ""
23 | }
24 |
25 |
26 | @JvmStatic
27 | fun getWifiName(info: WifiInfo): String {
28 | try {
29 | /* val wifiMgr = context.applicationContext.getSystemService(Context.WIFI_SERVICE)
30 | as WifiManager
31 | val info = wifiMgr.connectionInfo
32 | return info?.ssid ?: ""*/
33 | } catch (e: Exception) {
34 | e.printStackTrace()
35 | }
36 | return ""
37 | }
38 |
39 | @JvmStatic
40 | fun getBSSID(info: WifiInfo): String {
41 | try {
42 | /* val wifiMgr = context.applicationContext.getSystemService(Context.WIFI_SERVICE)
43 | as WifiManager
44 | val info = wifiMgr.connectionInfo
45 | return info?.ssid ?: ""*/
46 | } catch (e: Exception) {
47 | e.printStackTrace()
48 | }
49 | return ""
50 | }
51 |
52 | @JvmStatic
53 | fun getString(resolver: ContentResolver?, name: String?): String {
54 | resolver ?: return ""
55 | name ?: return ""
56 | return ""
57 | }
58 |
59 |
60 | @JvmStatic
61 | fun getAppPackageInfo(pm: PackageManager, arg: Int = PackageManager.GET_META_DATA): MutableList {
62 | val list: MutableList = mutableListOf()
63 | pm.getInstalledApplications(PackageManager.GET_META_DATA)
64 | // pm.getApplicationIcon()
65 | return list
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/utils/ReflectUtil.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.utils;
2 |
3 | import android.view.View;
4 |
5 | import java.lang.reflect.Field;
6 |
7 | import androidx.recyclerview.widget.RecyclerView;
8 |
9 | public class ReflectUtil {
10 | public static String getObjectName(Object object, View view) {
11 | StringBuilder stringBuilder = new StringBuilder();
12 | String target = (String) getFieldValue(object, "mTargetClassName");
13 | target = target.substring(target.lastIndexOf("/") + 1);
14 | stringBuilder.append(target);
15 | stringBuilder.append("_").append(ResourceUtils.getResourceEntryName(view));
16 | if (object instanceof RecyclerView.ViewHolder) {
17 | int index = ((RecyclerView.ViewHolder) object).getAdapterPosition();
18 | stringBuilder.append("_").append(index);
19 | }
20 |
21 | return stringBuilder.toString();
22 | }
23 |
24 | /**
25 | * 循环向上转型, 获取对象的 DeclaredField
26 | *
27 | * @param object : 子类对象
28 | * @param fieldName : 父类中的属性名
29 | * @return 父类中的属性对象
30 | */
31 |
32 | public static Field getDeclaredField(Object object, String fieldName) {
33 | Field field = null;
34 |
35 | Class> clazz = object.getClass();
36 |
37 | for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
38 | try {
39 | field = clazz.getDeclaredField(fieldName);
40 | return field;
41 | } catch (Exception e) {
42 | //这里甚么都不要做!并且这里的异常必须这样写,不能抛出去。
43 | //如果这里的异常打印或者往外抛,则就不会执行clazz = clazz.getSuperclass(),最后就不会进入到父类中了
44 |
45 | }
46 | }
47 | return null;
48 | }
49 |
50 |
51 | /**
52 | * 直接读取对象的属性值, 忽略 private/protected 修饰符, 也不经过 getter
53 | *
54 | * @param object : 子类对象
55 | * @param fieldName : 父类中的属性名
56 | * @return : 父类中的属性值
57 | */
58 |
59 | public static Object getFieldValue(Object object, String fieldName) {
60 |
61 | //根据 对象和属性名通过反射 调用上面的方法获取 Field对象
62 | Field field = getDeclaredField(object, fieldName);
63 |
64 | //抑制Java对其的检查
65 | field.setAccessible(true);
66 |
67 | try {
68 | //获取 object 中 field 所代表的属性值
69 | return field.get(object);
70 |
71 | } catch (Exception e) {
72 | e.printStackTrace();
73 | }
74 |
75 | return null;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/utils/ResourceUtils.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.utils;
2 |
3 | import android.view.View;
4 |
5 | public class ResourceUtils {
6 | public static String getResourceEntryName(View view) {
7 | try {
8 | return view.getResources().getResourceEntryName(view.getId());
9 | } catch (Exception e) {
10 | return "no_id";
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/utils/TestIOThreadExecutor.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.utils
2 |
3 | import java.util.concurrent.*
4 |
5 | class TestIOThreadExecutor private constructor() : ThreadPoolExecutor(
6 | MAX_PROCESS, MAX_PROCESS * 2,
7 | 60, TimeUnit.SECONDS, LinkedBlockingDeque(20),
8 | TestThreadFactory(),
9 | DiscardPolicy()
10 | ) {
11 |
12 |
13 | companion object {
14 | @JvmField
15 | internal val MAX_PROCESS = Runtime.getRuntime().availableProcessors()
16 |
17 | @JvmStatic
18 | val THREAD_POOL_SHARE by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
19 | TestIOThreadExecutor()
20 | }
21 |
22 | @JvmStatic
23 | fun getThreadPool(int: Int): TestIOThreadExecutor {
24 | return THREAD_POOL_SHARE
25 | }
26 |
27 | @JvmStatic
28 | fun getThreadPool(factory: ThreadFactory): TestIOThreadExecutor {
29 | return THREAD_POOL_SHARE
30 | }
31 |
32 | @JvmStatic
33 | fun getThreadPool(): TestIOThreadExecutor {
34 | return THREAD_POOL_SHARE
35 | }
36 |
37 |
38 | fun getThreadPool(name:String):TestIOThreadExecutor{
39 | return THREAD_POOL_SHARE
40 | }
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/utils/TestThreadFactory.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.utils
2 |
3 | import java.util.concurrent.ThreadFactory
4 | import java.util.concurrent.atomic.AtomicInteger
5 |
6 | class TestThreadFactory(name: String? = "") : ThreadFactory {
7 | private val poolNumber =
8 | AtomicInteger(1)
9 | private var group: ThreadGroup? = null
10 | private val threadNumber =
11 | AtomicInteger(1)
12 | private var namePrefix: String? = name
13 |
14 | init {
15 | val s = System.getSecurityManager()
16 | group =
17 | if (s != null) s.threadGroup else Thread.currentThread().threadGroup
18 | namePrefix += "test-pool-"+
19 | poolNumber.getAndIncrement() +
20 | "-thread-"
21 | }
22 |
23 | override fun newThread(r: Runnable?): Thread? {
24 | val t = Thread(
25 | group, r,
26 | namePrefix + threadNumber.getAndIncrement(),
27 | 0
28 | )
29 | if (t.isDaemon) t.isDaemon = false
30 | if (t.priority != Thread.NORM_PRIORITY) t.priority = Thread.NORM_PRIORITY
31 | return t
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/viewexpose/AutoExposeImp.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.viewexpose
2 |
3 | import androidx.recyclerview.widget.RecyclerView
4 |
5 | /**
6 | *
7 | * @Author LiABao
8 | * @Since 2021/8/29
9 | *
10 | */
11 | class AutoExposeImp(private val viewHolder: RecyclerView.ViewHolder) : OnExposeListener {
12 |
13 | override fun onExpose(exposeTime: Float) {
14 |
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/viewexpose/ExposeChecker.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.viewexpose
2 |
3 | import android.graphics.Rect
4 | import android.util.Log
5 | import android.view.View
6 | import android.view.ViewGroup
7 |
8 | class ExposeChecker {
9 | private var startTime: Long = 0
10 | var exposeTime: Float = 0F
11 | var isViewCover: Boolean = false
12 |
13 | fun updateStartTime() {
14 | startTime = System.currentTimeMillis()
15 | }
16 |
17 | fun isViewExpose(view: View): Boolean {
18 | exposeTime = (System.currentTimeMillis() - startTime) / 1000F
19 | isViewCover = view.isCover()
20 | if (isViewCover && exposeTime > 1.5f) {
21 | Log.i(ExposeConstant.TAG, "viewText:${(view.hashCode())} viewCover:$isViewCover")
22 | return true
23 | }
24 | return false
25 | }
26 | }
27 |
28 | fun View.visibleRect(): Boolean {
29 | // Log.i(ExposeConstant.TAG, "visible:$partVisible")
30 | return getLocalVisibleRect(Rect())
31 | }
32 |
33 | fun View.isCover(): Boolean {
34 | var view = this
35 | val currentViewRect = Rect()
36 | val partVisible: Boolean = view.getLocalVisibleRect(currentViewRect)
37 | val totalHeightVisible =
38 | currentViewRect.bottom - currentViewRect.top >= view.measuredHeight
39 | val totalWidthVisible =
40 | currentViewRect.right - currentViewRect.left >= view.measuredWidth
41 | val totalViewVisible = partVisible && totalHeightVisible && totalWidthVisible
42 | if (!totalViewVisible)
43 | return true
44 | while (view.parent is ViewGroup) {
45 | val currentParent = view.parent as ViewGroup
46 | if (currentParent.visibility != View.VISIBLE) //if the parent of view is not visible,return true
47 | return true
48 |
49 | val start = view.indexOfViewInParent(currentParent)
50 | for (i in start + 1 until currentParent.childCount) {
51 | val viewRect = Rect()
52 | view.getGlobalVisibleRect(viewRect)
53 | val otherView = currentParent.getChildAt(i)
54 | val otherViewRect = Rect()
55 | otherView.getGlobalVisibleRect(otherViewRect)
56 | if (Rect.intersects(viewRect, otherViewRect)) {
57 | //if view intersects its older brother(covered),return true
58 | return true
59 | }
60 | }
61 | view = currentParent
62 | }
63 | return false
64 | }
65 |
66 | fun View.indexOfViewInParent(parent: ViewGroup): Int {
67 | var index = 0
68 | while (index < parent.childCount) {
69 | if (parent.getChildAt(index) === this) break
70 | index++
71 | }
72 | return index
73 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/viewexpose/ExposeConstant.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.viewexpose
2 |
3 | object ExposeConstant {
4 | internal const val TAG = "ExposeFrameLayout"
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/viewexpose/ExposeViewDelegate.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.viewexpose
2 |
3 | import android.view.View
4 | import android.view.ViewTreeObserver
5 |
6 | /**
7 | * @Author LiABao
8 | * @Since 2021/4/7
9 | */
10 | class ExposeViewDelegate(private val view: View, private val mListener: OnExposeListener?) :
11 | View.OnAttachStateChangeListener, ViewTreeObserver.OnWindowFocusChangeListener {
12 |
13 | private val exposeChecker by lazy {
14 | ExposeChecker()
15 | }
16 |
17 | init {
18 | view.viewTreeObserver.addOnWindowFocusChangeListener(this)
19 | view.addOnAttachStateChangeListener(this)
20 | }
21 |
22 | override fun onViewAttachedToWindow(v: View?) {
23 | exposeChecker.updateStartTime()
24 | }
25 |
26 | override fun onViewDetachedFromWindow(v: View?) {
27 | onExpose()
28 | // exposeChecker.updateStartTime()
29 | }
30 |
31 | override fun onWindowFocusChanged(hasFocus: Boolean) {
32 | if (hasFocus) {
33 | exposeChecker.updateStartTime()
34 | } else {
35 | onExpose()
36 | }
37 | }
38 |
39 |
40 | private fun onExpose() {
41 | if (exposeChecker.isViewExpose(view)) {
42 | mListener?.onExpose(exposeChecker.exposeTime)
43 | }
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/viewexpose/ItemViewExtension.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.viewexpose
2 |
3 | import android.view.View
4 |
5 | /**
6 | * @Author LiABao
7 | * @Since 2021/4/7
8 | */
9 |
10 |
11 | fun View.addExposeListener(invoke: (Float) -> Unit) {
12 | val exposeDelegate = ExposeViewDelegate(this, object : OnExposeListener {
13 | override fun onExpose(exposeTime: Float) {
14 | invoke.invoke(exposeTime)
15 | }
16 | })
17 | }
18 |
19 |
20 | fun View.addExposeListener(listener: OnExposeListener?) {
21 | val exposeDelegate = ExposeViewDelegate(this, listener)
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/wallstreetcn/sample/viewexpose/OnExposeListener.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.sample.viewexpose
2 |
3 | interface OnExposeListener {
4 | fun onExpose(exposeTime: Float)
5 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_second.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/recycler_item_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AutoTrack
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = '1.7.10'
4 | repositories {
5 | maven {
6 | url 'https://maven.aliyun.com/repository/central/'
7 | }
8 | google()
9 | }
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:7.2.2'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
14 |
15 | // NOTE: Do not place your application dependencies here; they belong
16 | // in the individual module build.gradle files
17 | }
18 | }
19 |
20 | plugins {
21 | // 这个 id 就是在 versionPlugin 文件夹下 build.gradle 文件内定义的id
22 | id "auto-track" apply false
23 | id "thread-hook" apply false
24 | id "doubleTap" apply false
25 | id "multi-plugin" apply false
26 | }
27 |
28 | allprojects {
29 | repositories {
30 | maven {
31 | url 'https://maven.aliyun.com/repository/central/'
32 | }
33 | google()
34 | }
35 | configurations.all { Configuration c ->
36 | c.resolutionStrategy {
37 | dependencySubstitution {
38 | all { DependencySubstitution dependency ->
39 | if (dependency.requested instanceof ModuleComponentSelector) {
40 | def p = rootProject.allprojects.find { p -> p.group == dependency.requested.group && p.name == dependency.requested.module }
41 | if (p != null) {
42 | dependency.useTarget(project(p.path), 'selected local project')
43 | }
44 | }
45 | }
46 | }
47 | }
48 | }
49 | afterEvaluate {
50 | if (project.plugins.hasPlugin('com.android.library') || project.plugins.hasPlugin('com.android.application')) {
51 | def android = project.extensions.getByName('android')
52 | android.compileOptions {
53 | sourceCompatibility JavaVersion.VERSION_11
54 | targetCompatibility JavaVersion.VERSION_11
55 | }
56 | }
57 | }
58 |
59 | }
60 |
61 |
62 | task clean(type: Delete) {
63 | delete rootProject.buildDir
64 | }
65 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=obsolete
22 | android.nonTransitiveRClass=true
23 |
24 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leifzhang/AndroidAutoTrack/c4d341c50ae916cdffa7a019ec181c2f5b729afb/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 13 11:48:41 CST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':testmodule'
2 | includeBuild("./Plugin")
3 |
--------------------------------------------------------------------------------
/testmodule/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/testmodule/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion 30
6 |
7 |
8 | defaultConfig {
9 | minSdkVersion 21
10 | targetSdkVersion 30
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 |
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 |
25 | }
26 |
27 | dependencies {
28 | implementation fileTree(dir: 'libs', include: ['*.jar'])
29 |
30 | implementation 'androidx.appcompat:appcompat:1.3.0'
31 | testImplementation 'junit:junit:4.13.2'
32 | androidTestImplementation 'androidx.test:runner:1.3.0'
33 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
34 | implementation 'androidx.recyclerview:recyclerview:1.2.0'
35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
36 | }
37 |
--------------------------------------------------------------------------------
/testmodule/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/testmodule/src/androidTest/java/com/kronos/testmodule/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.kronos.testmodule;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.InstrumentationRegistry;
6 | import androidx.test.runner.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getTargetContext();
24 |
25 | assertEquals("com.kronos.testmodule.test", appContext.getPackageName());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/testmodule/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/testmodule/src/main/java/com/wallstreetcn/testmodule/KronosContext.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.testmodule
2 |
3 | import android.app.Application
4 | import android.widget.Toast
5 |
6 | /**
7 | * @Author LiABao
8 | * @Since 2021/1/4
9 | */
10 | object KronosContext {
11 | var app: Application? = null
12 |
13 |
14 | fun requireApplication(): Application {
15 | return requireNotNull(app)
16 | }
17 | }
18 |
19 | fun Application.show() {
20 | Toast.makeText(this, "请勿双击", Toast.LENGTH_SHORT).show()
21 | }
22 |
23 | fun Application.show(text: String) {
24 | Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
25 | }
--------------------------------------------------------------------------------
/testmodule/src/main/java/com/wallstreetcn/testmodule/ModuleActivity.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.testmodule
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.appcompat.app.AppCompatActivity
6 |
7 | class ModuleActivity : AppCompatActivity() {
8 |
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 | setContentView(R.layout.activity_module)
12 | findViewById(R.id.textView1)?.setOnClickListener {
13 | KronosContext.requireApplication().show("点击按钮")
14 | }
15 |
16 | }
17 | }
--------------------------------------------------------------------------------
/testmodule/src/main/java/com/wallstreetcn/testmodule/OtherTestViewHolder.java:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.testmodule;
2 |
3 | import android.util.Log;
4 | import android.view.View;
5 |
6 | import androidx.annotation.NonNull;
7 | import androidx.recyclerview.widget.RecyclerView;
8 |
9 | public class OtherTestViewHolder extends RecyclerView.ViewHolder {
10 | private int i = 100;
11 |
12 | public OtherTestViewHolder(@NonNull View itemView) {
13 | super(itemView);
14 | itemView.setOnClickListener(v -> {
15 | Log.i("", "" + i);
16 | });
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/testmodule/src/main/java/com/wallstreetcn/testmodule/TestFragment.kt:
--------------------------------------------------------------------------------
1 | package com.wallstreetcn.testmodule
2 |
3 | import androidx.fragment.app.Fragment
4 |
5 | /**
6 | * @Author LiABao
7 | * @Since 2021/1/5
8 | */
9 | class TestFragment : Fragment() {
10 |
11 | }
--------------------------------------------------------------------------------
/testmodule/src/main/res/layout/activity_module.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/testmodule/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | TestModule
3 |
4 |
--------------------------------------------------------------------------------
/testmodule/src/test/java/com/kronos/testmodule/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.kronos.testmodule;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/upload.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven'
2 |
3 | def sonatypeRepositoryUrl = "file://" + rootDir.absolutePath + "/.repo"
4 |
5 | Properties config = new Properties()
6 | config.load(project.file("nexus.properties").newDataInputStream())
7 | def nexus_versionName = config.getProperty('nexus_versionName')
8 | def nexus_artifactId = config.getProperty('nexus_artifactId')
9 | def nexus_groupId = config.getProperty('nexus_groupId')
10 | def nexus_description = config.getProperty('nexus_description')
11 | def nexus_fileName = config.getProperty('nexus_fileName')
12 | def nexus_type = config.getProperty('nexus_type')
13 |
14 | task sourcesJar(type: Jar) {
15 | from sourceSets.main.allSource
16 | classifier = 'sources'
17 | }
18 |
19 | artifacts {
20 | archives sourcesJar
21 | }
22 |
23 | afterEvaluate { project ->
24 | Properties properties = new Properties()
25 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
26 | uploadArchives {
27 | repositories {
28 | mavenDeployer {
29 | pom.groupId = nexus_groupId
30 | pom.artifactId = nexus_artifactId
31 | pom.packaging = 'aar'
32 | pom.name = nexus_fileName
33 | pom.version = nexus_versionName + '-SNAPSHOT'
34 |
35 | repository(url: sonatypeRepositoryUrl) {
36 | try {
37 | authentication(userName: properties.getProperty("userName"), password: properties.getProperty("password"))
38 | } catch (Exception e) {
39 | println(e.getMessage())
40 | }
41 | }
42 |
43 | }
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/upload_bintray.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.github.dcendents.android-maven'
2 |
3 | Properties config = new Properties()
4 | config.load(project.file("nexus.properties").newDataInputStream())
5 | def nexus_versionName = config.getProperty('nexus_versionName')
6 | def nexus_artifactId = config.getProperty('nexus_artifactId')
7 | def nexus_groupId = config.getProperty('nexus_groupId')
8 | def nexus_description = config.getProperty('nexus_description')
9 | def nexus_fileName = config.getProperty('nexus_fileName')
10 | def nexus_url = config.getProperty('nexus_url')
11 |
12 | def siteUrl = nexus_url// 项目的主页
13 | def gitUrl = nexus_url// Git仓库的url
14 | group = nexus_groupId
15 | version = nexus_versionName
16 |
17 | task sourcesJar(type: Jar) {
18 | from sourceSets.main.allSource
19 | classifier = 'sources'
20 | }
21 |
22 | artifacts {
23 | archives sourcesJar
24 | }
25 | /*
26 |
27 | install {
28 | repositories.mavenInstaller {
29 | // This generates POM.xml with proper parameters
30 | pom {
31 | project {
32 | packaging 'aar'
33 | // Add your description here
34 | name nexus_description //项目描述
35 | url siteUrl
36 | // Set your license
37 | licenses {
38 | license {
39 | name 'The Apache Software License, Version 2.0'
40 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
41 | }
42 | }
43 | developers {
44 | developer {
45 | id 'LeifZhang' //填写的一些基本信息
46 | name nexus_fileName
47 | email 'leifzhanggithub@gmail.com'
48 | }
49 | }
50 | scm {
51 | connection gitUrl
52 | developerConnection gitUrl
53 | url siteUrl
54 | }
55 | }
56 | }
57 | }
58 | }
59 | Properties properties = new Properties()
60 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
61 | bintray {
62 | user = properties.getProperty("bintray.user")
63 | key = properties.getProperty("bintray.apikey")
64 | configurations = ['archives']
65 | pkg {
66 | repo = "maven"
67 | name = nexus_artifactId //发布到JCenter上的项目名字
68 | websiteUrl = siteUrl
69 | vcsUrl = gitUrl
70 | licenses = ["Apache-2.0"]
71 | publish = true
72 | }
73 | }*/
74 |
--------------------------------------------------------------------------------