├── .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 | 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 | [![State-of-the-art Shitcode](https://img.shields.io/static/v1?label=State-of-the-art&message=Shitcode&color=7B5804)](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 | --------------------------------------------------------------------------------