Skip to content

Commit

Permalink
feat: add saas plugin (#19)
Browse files Browse the repository at this point in the history
* add AGP 8.1

* adjust plugin dirs

* fix project extra

* fixed gradle version to 7.6.1:
1. plugin_publish require at least 7.6.1;
2. gradle 4.2 test will failure when gradle upgrade above 8.0;
3. AGP 8.1.1 running ok.

* remove publish check

* Update gradle.properties

* add saas plugin

* add saas plugin
  • Loading branch information
cpacm authored Aug 29, 2023
1 parent 56a0231 commit ec4bddc
Show file tree
Hide file tree
Showing 35 changed files with 1,232 additions and 49 deletions.
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:

jobs:
publish:
if: ${{ !startsWith(github.event.release.name, 'Saas') }}
runs-on: ubuntu-latest

steps:
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/saas_publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Publish Gradle Portal
on:
release:
types: [ published ]

jobs:
publish:
if: ${{ startsWith(github.event.release.name, 'Saas') }}
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up JDK 11
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 11

# Gradle 缓存配置
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle

# 给 gradlew 文件授权
# 构建项目
- name: Build with Gradle
run: |
chmod +x gradlew
./gradlew :autotracker-gradle-plugin:clean
- name: Publish plugin to gradlePortal
run: ./gradlew :autotracker-gradle-plugin:saas-gradle-plugin:publishPlugins -Pgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }}
13 changes: 4 additions & 9 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,15 @@
android:name=".menuitem.MenuItemActivity"
android:exported="true"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="true">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<data android:scheme="growing.6b963145e9509ad0" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,3 @@ interface AutoTrackerContext {
interface AutoTrackerTransformListener {
fun transform(context: AutoTrackerContext, bytecode: ByteArray): ByteArray
}

interface ClassContextCompat {
val className: String

fun isAssignable(subClazz: String, superClazz: String): Boolean

fun classIncluded(clazz: String): Boolean
}

3 changes: 3 additions & 0 deletions autotracker-gradle-plugin/agp-wrapper-impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ dependencies {
implementation(project(":agp-wrapper-72"))
implementation(project(":agp-wrapper-81"))
implementation(gradleApi())

compileOnly("org.ow2.asm:asm:9.5")
compileOnly("org.ow2.asm:asm-commons:9.5")
compileOnly("com.android.tools.build:gradle-api:${rootProject.extra["agp_version"]}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.growingio.android.plugin.hook

import com.growingio.android.plugin.util.normalize
import java.util.*

/**
Expand Down Expand Up @@ -47,8 +48,15 @@ object HookClassesConfig {
}

init {
initDefaultInjector(null)
}

fun initDefaultInjector(includeList: List<String>?) {
AROUND_HOOK_CLASSES.clear()
val aroundList = HookInjectorClass.initAroundClass()
for (around in aroundList) {
aroundList.filter {
includeList?.contains(normalize(it.injectClassName)) ?: true
}.forEach { around ->
putHookMethod(
AROUND_HOOK_CLASSES,
around.targetClassName,
Expand All @@ -61,8 +69,11 @@ object HookClassesConfig {
)
}

SUPER_HOOK_CLASSES.clear()
val superList = HookInjectorClass.initSuperClass()
for (s in superList) {
superList.filter {
includeList?.contains(normalize(it.injectClassName)) ?: true
}.forEach { s ->
putHookMethod(
SUPER_HOOK_CLASSES,
s.targetClassName,
Expand All @@ -75,8 +86,11 @@ object HookClassesConfig {
)
}

TARGET_HOOK_CLASSES.clear()
val targetList = HookInjectorClass.initTargetClass()
for (t in targetList) {
targetList.filter {
includeList?.contains(normalize(it.injectClassName)) ?: true
}.forEach { t ->
putHookMethod(
TARGET_HOOK_CLASSES,
t.targetClassName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,26 @@ public object HookInjectorClass {
AROUND_HOOK_CLASSES.add(HookData("com/uc/webview/export/WebView","loadData","(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V","com/growingio/android/sdk/autotrack/inject/UcWebViewInjector","ucWebViewLoadData","(Lcom/uc/webview/export/WebView;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",false))
AROUND_HOOK_CLASSES.add(HookData("com/uc/webview/export/WebView","loadDataWithBaseURL","(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V","com/growingio/android/sdk/autotrack/inject/UcWebViewInjector","ucWebViewLoadDataWithBaseURL","(Lcom/uc/webview/export/WebView;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",false))
AROUND_HOOK_CLASSES.add(HookData("com/uc/webview/export/WebView","postUrl","(Ljava/lang/String;[B)V","com/growingio/android/sdk/autotrack/inject/UcWebViewInjector","ucWebViewPostUrl","(Lcom/uc/webview/export/WebView;Ljava/lang/String;[B)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/webkit/WebView","setWebChromeClient","(Landroid/webkit/WebChromeClient;)V","com/growingio/android/sdk/autotrack/inject/WebChromeClientInjector","setWebChromeClient","(Landroid/webkit/WebView;Landroid/webkit/WebChromeClient;)V",false))
AROUND_HOOK_CLASSES.add(HookData("com/tencent/smtt/sdk/WebView","setWebChromeClient","(Lcom/tencent/smtt/sdk/WebChromeClient;)V","com/growingio/android/sdk/autotrack/inject/WebChromeClientInjector","setX5WebChromeClient","(Lcom/tencent/smtt/sdk/WebView;Lcom/tencent/smtt/sdk/WebChromeClient;)V",false))
AROUND_HOOK_CLASSES.add(HookData("com/uc/webview/export/WebView","setWebChromeClient","(Lcom/uc/webview/export/WebChromeClient;)V","com/growingio/android/sdk/autotrack/inject/WebChromeClientInjector","setUcWebChromeClient","(Lcom/uc/webview/export/WebView;Lcom/uc/webview/export/WebChromeClient;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/webkit/WebView","loadUrl","(Ljava/lang/String;)V","com/growingio/android/sdk/autotrack/inject/WebViewInjector","webkitWebViewLoadUrl","(Landroid/webkit/WebView;Ljava/lang/String;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/webkit/WebView","loadUrl","(Ljava/lang/String;Ljava/util/Map;)V","com/growingio/android/sdk/autotrack/inject/WebViewInjector","webkitWebViewLoadUrl","(Landroid/webkit/WebView;Ljava/lang/String;Ljava/util/Map;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/webkit/WebView","loadData","(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V","com/growingio/android/sdk/autotrack/inject/WebViewInjector","webkitWebViewLoadData","(Landroid/webkit/WebView;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/webkit/WebView","loadDataWithBaseURL","(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V","com/growingio/android/sdk/autotrack/inject/WebViewInjector","webkitWebViewLoadDataWithBaseURL","(Landroid/webkit/WebView;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/webkit/WebView","postUrl","(Ljava/lang/String;[B)V","com/growingio/android/sdk/autotrack/inject/WebViewInjector","webkitWebViewPostUrl","(Landroid/webkit/WebView;Ljava/lang/String;[B)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/widget/Toast","show","()V","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","toastShow","(Landroid/widget/Toast;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/app/Dialog","show","()V","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","dialogShow","(Landroid/app/Dialog;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/app/TimePickerDialog","show","()V","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","timePickerDialogShow","(Landroid/app/TimePickerDialog;)V",false))
AROUND_HOOK_CLASSES.add(HookData("androidx/fragment/app/DialogFragment","show","(Landroidx/fragment/app/FragmentManager;Ljava/lang/String;)V","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","dialogFragmentShow","(Landroidx/fragment/app/DialogFragment;Landroidx/fragment/app/FragmentManager;Ljava/lang/String;)V",false))
AROUND_HOOK_CLASSES.add(HookData("androidx/fragment/app/DialogFragment","show","(Landroidx/fragment/app/FragmentTransaction;Ljava/lang/String;)I","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","dialogFragmentShowFt","(Landroidx/fragment/app/DialogFragment;Landroidx/fragment/app/FragmentTransaction;Ljava/lang/String;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/app/DialogFragment","show","(Landroid/app/FragmentManager;Ljava/lang/String;)V","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","dialogFragmentSystemShow","(Landroid/app/DialogFragment;Landroid/app/FragmentManager;Ljava/lang/String;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/app/DialogFragment","show","(Landroid/app/FragmentTransaction;Ljava/lang/String;)I","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","dialogFragmentSystemShowFt","(Landroid/app/DialogFragment;Landroid/app/FragmentTransaction;Ljava/lang/String;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/support/v4/app/Fragment","show","(Landroid/support/v4/app/FragmentManager;Ljava/lang/String;)V","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","dialogFragmentV4Show","(Landroid/support/v4/app/DialogFragment;Landroid/support/v4/app/FragmentManager;Ljava/lang/String;)V",true))
AROUND_HOOK_CLASSES.add(HookData("android/support/v4/app/Fragment","show","(Landroid/support/v4/app/FragmentTransaction;Ljava/lang/String;)I","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","dialogFragmentV4ShowFt","(Landroid/support/v4/app/DialogFragment;Landroid/support/v4/app/FragmentTransaction;Ljava/lang/String;)V",true))
AROUND_HOOK_CLASSES.add(HookData("androidx/appcompat/widget/PopupMenu","show","()V","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","popupMenuShow","(Landroidx/appcompat/widget/PopupMenu;)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/widget/PopupWindow","showAsDropDown","(Landroid/view/View;III)V","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","popupWindowShowAsDropDown","(Landroid/widget/PopupWindow;Landroid/view/View;III)V",false))
AROUND_HOOK_CLASSES.add(HookData("android/widget/PopupWindow","showAtLocation","(Landroid/view/View;III)V","com/growingio/android/sdk/autotrack/inject/WindowShowInjector","popupWindowShowAtLocation","(Landroid/widget/PopupWindow;Landroid/view/View;III)V",false))
AROUND_HOOK_CLASSES.add(HookData("com/tencent/smtt/sdk/WebView","loadUrl","(Ljava/lang/String;)V","com/growingio/android/sdk/autotrack/inject/X5WebViewInjector","x5WebViewLoadUrl","(Lcom/tencent/smtt/sdk/WebView;Ljava/lang/String;)V",false))
AROUND_HOOK_CLASSES.add(HookData("com/tencent/smtt/sdk/WebView","loadUrl","(Ljava/lang/String;Ljava/util/Map;)V","com/growingio/android/sdk/autotrack/inject/X5WebViewInjector","x5WebViewLoadUrl","(Lcom/tencent/smtt/sdk/WebView;Ljava/lang/String;Ljava/util/Map;)V",false))
AROUND_HOOK_CLASSES.add(HookData("com/tencent/smtt/sdk/WebView","loadData","(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V","com/growingio/android/sdk/autotrack/inject/X5WebViewInjector","x5WebViewLoadData","(Lcom/tencent/smtt/sdk/WebView;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",false))
Expand Down Expand Up @@ -103,6 +118,12 @@ public object HookInjectorClass {
SUPER_HOOK_CLASSES.add(HookData("android/widget/ExpandableListView${'$'}OnGroupClickListener","onGroupClick","(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z","com/growingio/android/sdk/autotrack/inject/ViewClickInjector","expandableListViewOnGroupClick","(Landroid/widget/ExpandableListView${'$'}OnGroupClickListener;Landroid/widget/ExpandableListView;Landroid/view/View;IJ)V",false))
SUPER_HOOK_CLASSES.add(HookData("android/widget/ExpandableListView${'$'}OnChildClickListener","onChildClick","(Landroid/widget/ExpandableListView;Landroid/view/View;IIJ)Z","com/growingio/android/sdk/autotrack/inject/ViewClickInjector","expandableListViewOnChildClick","(Landroid/widget/ExpandableListView${'$'}OnChildClickListener;Landroid/widget/ExpandableListView;Landroid/view/View;IIJ)V",false))
SUPER_HOOK_CLASSES.add(HookData("com/google/android/material/tabs/TabLayout${'$'}OnTabSelectedListener","onTabSelected","(Lcom/google/android/material/tabs/TabLayout${'$'}Tab;)V","com/growingio/android/sdk/autotrack/inject/ViewClickInjector","tabLayoutOnTabSelected","(Lcom/google/android/material/tabs/TabLayout${'$'}OnTabSelectedListener;Lcom/google/android/material/tabs/TabLayout${'$'}Tab;)V",false))
SUPER_HOOK_CLASSES.add(HookData("android/webkit/WebChromeClient","onProgressChanged","(Landroid/webkit/WebView;I)V","com/growingio/android/sdk/autotrack/inject/WebChromeClientInjector","onProgressChangedStart","(Landroid/webkit/WebView;I)V",false))
SUPER_HOOK_CLASSES.add(HookData("android/webkit/WebChromeClient","onProgressChanged","(Landroid/webkit/WebView;I)V","com/growingio/android/sdk/autotrack/inject/WebChromeClientInjector","onProgressChangedEnd","(Landroid/webkit/WebView;I)V",true))
SUPER_HOOK_CLASSES.add(HookData("com/tencent/smtt/sdk/WebChromeClient","onProgressChanged","(Lcom/tencent/smtt/sdk/WebView;I)V","com/growingio/android/sdk/autotrack/inject/WebChromeClientInjector","onX5ProgressChangedStart","(Lcom/tencent/smtt/sdk/WebView;I)V",false))
SUPER_HOOK_CLASSES.add(HookData("com/tencent/smtt/sdk/WebChromeClient","onProgressChanged","(Lcom/tencent/smtt/sdk/WebView;I)V","com/growingio/android/sdk/autotrack/inject/WebChromeClientInjector","onX5ProgressChangedEnd","(Lcom/tencent/smtt/sdk/WebView;I)V",true))
SUPER_HOOK_CLASSES.add(HookData("com/uc/webview/export/WebChromeClient","onProgressChanged","(Lcom/uc/webview/export/WebView;I)V","com/growingio/android/sdk/autotrack/inject/WebChromeClientInjector","onUcProgressChangedStart","(Lcom/uc/webview/export/WebView;I)V",false))
SUPER_HOOK_CLASSES.add(HookData("com/uc/webview/export/WebChromeClient","onProgressChanged","(Lcom/uc/webview/export/WebView;I)V","com/growingio/android/sdk/autotrack/inject/WebChromeClientInjector","onUcProgressChangedEnd","(Lcom/uc/webview/export/WebView;I)V",true))
return SUPER_HOOK_CLASSES
}

Expand All @@ -119,6 +140,7 @@ public object HookInjectorClass {
TARGET_HOOK_CLASSES.add(HookData("com/google/android/gms/analytics/Tracker","set","(Ljava/lang/String;Ljava/lang/String;)V","com/growingio/android/analytics/google/GoogleAnalyticsInjector","set","(Lcom/google/android/gms/analytics/Tracker;Ljava/lang/String;Ljava/lang/String;)V",true))
TARGET_HOOK_CLASSES.add(HookData("com/google/android/gms/analytics/Tracker","send","(Ljava/util/Map;)V","com/growingio/android/analytics/google/GoogleAnalyticsInjector","send","(Lcom/google/android/gms/analytics/Tracker;Ljava/util/Map;)V",true))
TARGET_HOOK_CLASSES.add(HookData("com/google/android/gms/analytics/Tracker","setClientId","(Ljava/lang/String;)V","com/growingio/android/analytics/google/GoogleAnalyticsInjector","setClientId","(Lcom/google/android/gms/analytics/Tracker;Ljava/lang/String;)V",true))
TARGET_HOOK_CLASSES.add(HookData("com/facebook/react/uimanager/ViewGroupManager","addView","(Landroidview/ViewGroup;Landroid/view/View;I)V","com/growingio/android/sdk/plugin/rn/ReactNativeInjector","addRNView","(Ljava/lang/Object;Landroid/view/ViewGroup;Landroid/view/View;I)V",false))
TARGET_HOOK_CLASSES.add(HookData("com/sensorsdata/analytics/android/sdk/SensorsDataAPI","disableSDK","()V","com/growingio/android/analytics/sensor/SensorAnalyticsInjector","disableSDK","()V",false))
TARGET_HOOK_CLASSES.add(HookData("com/sensorsdata/analytics/android/sdk/SensorsDataAPI","enableSDK","()V","com/growingio/android/analytics/sensor/SensorAnalyticsInjector","enableSDK","()V",false))
TARGET_HOOK_CLASSES.add(HookData("com/sensorsdata/analytics/android/sdk/AbstractSensorsDataAPI","trackEvent","(Lcom/sensorsdata/analytics/android/sdk/internal/beans/EventType;Ljava/lang/String;Lorg/json/JSONObject;Ljava/lang/String;)V","com/growingio/android/analytics/sensor/SensorAnalyticsInjector","trackEvent","(Lcom/sensorsdata/analytics/android/sdk/internal/beans/EventType;Ljava/lang/String;Lorg/json/JSONObject;Ljava/lang/String;)V",false))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ package com.growingio.android.plugin.visitor

import com.growingio.android.plugin.hook.HookClassesConfig
import com.growingio.android.plugin.hook.TargetMethod
import com.growingio.android.plugin.transform.ClassContextCompat
import com.growingio.android.plugin.util.ClassContextCompat
import com.growingio.android.plugin.util.info
import com.growingio.android.plugin.util.unNormalize
import org.objectweb.asm.*
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.Handle
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.commons.AdviceAdapter
import org.objectweb.asm.commons.GeneratorAdapter
import org.objectweb.asm.commons.Method
Expand All @@ -31,7 +35,7 @@ import org.objectweb.asm.commons.Method
* 优化了原版的 DesugarVisit
* @author cpacm 2022/4/19
*/
internal class DesugarClassVisitor(
class DesugarClassVisitor(
api: Int, ncv: ClassVisitor, classContext: ClassContextCompat
) : ClassVisitor(api, ncv), ClassContextCompat by classContext {

Expand Down Expand Up @@ -140,11 +144,11 @@ internal class DesugarClassVisitor(
adapter.visitLdcInsn("[GenerateDynamicMethod]")
adapter.visitLdcInsn(methodBlock.methodName)
adapter.visitInsn(Opcodes.ICONST_0)
adapter.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
adapter.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object")
adapter.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/growingio/android/sdk/track/log/Logger",
"d",
"com/growingio/android/sdk/autotrack/inject/UtilsInjector",
"log",
"(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V",
false
)
Expand Down Expand Up @@ -191,7 +195,7 @@ internal class DesugarClassVisitor(
super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments)
return
}
if(bootstrapMethodArguments.isEmpty()) return
if (bootstrapMethodArguments.isEmpty()) return
//info("[visitInvokeDynamicInsn]${className}-${name}==>${(bootstrapMethodArguments[1] as Handle).name}")
val handle = bootstrapMethodArguments[1] as Handle
if (name == handle.name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.growingio.android.plugin.visitor
import com.growingio.android.plugin.hook.HookClassesConfig
import com.growingio.android.plugin.hook.TargetClass
import com.growingio.android.plugin.hook.TargetMethod
import com.growingio.android.plugin.transform.ClassContextCompat
import com.growingio.android.plugin.util.ClassContextCompat
import com.growingio.android.plugin.util.info
import com.growingio.android.plugin.util.simpleClass
import org.objectweb.asm.ClassVisitor
Expand All @@ -34,7 +34,7 @@ import org.objectweb.asm.commons.Method
*
* @author cpacm 2022/4/26
*/
internal class InjectAroundClassVisitor(
class InjectAroundClassVisitor(
api: Int, ncv: ClassVisitor, classContext: ClassContextCompat
) : ClassVisitor(api, ncv), ClassContextCompat by classContext {

Expand Down
Loading

0 comments on commit ec4bddc

Please sign in to comment.