From c0ab2323b89eee881b912c4dfa95d82355a9e88d Mon Sep 17 00:00:00 2001
From: fengyuecanzhu <1021300691@qq.com>
Date: Tue, 29 Mar 2022 15:39:50 +0800
Subject: [PATCH] add plugin
---
.idea/compiler.xml | 2 +-
.idea/gradle.xml | 2 +
.idea/misc.xml | 2 +-
.idea/modules.xml | 1 +
app/build.gradle | 3 +
app/libs/dynamic-release.aar | Bin 0 -> 3249 bytes
app/src/main/AndroidManifest.xml | 1 +
app/src/main/assets/updatelog.fy | 4 +
.../xyz/fycz/myreader/application/App.java | 7 +-
.../xyz/fycz/myreader/common/APPCONST.java | 4 +-
.../xyz/fycz/myreader/common/URLCONST.java | 2 +
.../xyz/fycz/myreader/entity/PluginConfig.kt | 13 +++
.../fycz/myreader/util/utils/PluginUtils.kt | 92 ++++++++++++++++++
build.gradle | 2 +-
dynamic/.gitignore | 1 +
dynamic/build.gradle | 38 ++++++++
dynamic/consumer-rules.pro | 0
dynamic/proguard-rules.pro | 21 ++++
.../fycz/dynamic/ExampleInstrumentedTest.kt | 24 +++++
dynamic/src/main/AndroidManifest.xml | 5 +
.../main/java/xyz/fycz/dynamic/AppLoadImpl.kt | 38 ++++++++
.../main/java/xyz/fycz/dynamic/AppParam.java | 18 ++++
.../java/xyz/fycz/dynamic/IAppLoader.java | 9 ++
.../java/xyz/fycz/dynamic/ExampleUnitTest.kt | 17 ++++
settings.gradle | 1 +
25 files changed, 302 insertions(+), 5 deletions(-)
create mode 100644 app/libs/dynamic-release.aar
create mode 100644 app/src/main/java/xyz/fycz/myreader/entity/PluginConfig.kt
create mode 100644 app/src/main/java/xyz/fycz/myreader/util/utils/PluginUtils.kt
create mode 100644 dynamic/.gitignore
create mode 100644 dynamic/build.gradle
create mode 100644 dynamic/consumer-rules.pro
create mode 100644 dynamic/proguard-rules.pro
create mode 100644 dynamic/src/androidTest/java/xyz/fycz/dynamic/ExampleInstrumentedTest.kt
create mode 100644 dynamic/src/main/AndroidManifest.xml
create mode 100644 dynamic/src/main/java/xyz/fycz/dynamic/AppLoadImpl.kt
create mode 100644 dynamic/src/main/java/xyz/fycz/dynamic/AppParam.java
create mode 100644 dynamic/src/main/java/xyz/fycz/dynamic/IAppLoader.java
create mode 100644 dynamic/src/test/java/xyz/fycz/dynamic/ExampleUnitTest.kt
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 61a9130..fb7f4a8 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index eb20a8e..976a899 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -7,11 +7,13 @@
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 88ef7a3..bba6cb8 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
index ba83cda..35e7452 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -5,6 +5,7 @@
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 50ce4ec..dadc77d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -274,6 +274,9 @@ dependencies {
exclude module: 'support-v4'
exclude group: 'com.android.support'
}
+
+ //maple
+ implementation("me.fycz.maple:maple:1.6")
}
greendao {
diff --git a/app/libs/dynamic-release.aar b/app/libs/dynamic-release.aar
new file mode 100644
index 0000000000000000000000000000000000000000..59aa4b8508cfed7f358b0eae82d34a31a1625e2e
GIT binary patch
literal 3249
zcmbVPdpHx^AD_7=bKUD6S*e6!im@Wswjm@H!q}_{V;hBTDv6Z4YVJ%!p&_?1k|eq0
zlIw)#QbR0=zUQ3p^PJD~ISy!kfo%YQhzI}xIKl$}
z0Q_?V0JigD5Q&Hn!H}>zlnWGofWyDmf-Vj=F$Dnt%N+m!F94i=FUmYU8pt^)d1>+F
zH~zI}N#tRK5TBF4kalm#lk}XuBCa%?x@=KF;jVXio9}XWq4Nq~*Fa-zGc7=PP_zYV
zFEej|3Qk>*EFDmt8*eEhluhnC8)op`PB$7$$DMN<9SN~+kdxxI;^
zn-l7Xw)>w{ZuJ>$IlHe>?APJmjbr%F5Ckb-sNs)^@br|m{et5cIi_fg(aO#N4T;##
z@l^+UoGdcHFxMv%NoSwONQLRZU#SV`iXmzE5AyjFF)ndb2;;|d>DI56DWNRkf|8-z
zv}NgB!@>Z%j|YCl1bU?
z7kAml_N@^)_)}@FAno8|Z7b^n{{f|4ef=Mt7%~b-ii?zMe_;A_ib;xelP?|!?7h*x
zI+81XiEh&N$OkpAQG0c~;5I;g)nuE%TqpMD0Yr4CICLA!e@9)PV)Xp^oEw4${#ms*
zwuhjG@XyAu_x1sMpY7s`C_1<&F*@@LM5^+Tz+^d-ow*c(A!J`p_*K?Xz6RUp$>mRs
zO&9~Gylx1+XSRG*$WK%qSKrpiJ{Wna%>)tdT}jvNgx_}iF?MzPOen^NLxckmfzbwzb&)S$ESiC*Fm
zGP8c|wcJ@W$a;Tf@LW77>V7}rc=1=y`N^S`8J{th{tr9$!3Pl~eq{~gK@O5xVbU2g
z;z|fRQ0Q85K_fkSbVaiF5lZj==bVrz{1Df#
zw&~4h?)Gy!-aw0n+H?U$M3iCWDqo7fY1NdRQ<9n3b^CcdE#dS<5Zzrrtv&5@)~MJy
zwmCJV{N|VH#q6M1ZaG)jt5h}R{^u%tR7mt`4uz4lA#
zB@?3kFVUIao>wj6@VgG<_atUvd~3vR4(uqM`9=ILx$bmh)3dpVHMFu(O-Lg(=1xTM
zdyYX!ZH(#D_>x|ovcWTR4=>e^Y-W4a(OryJUrHjXSl!uoab_Zo5ZjjqYKETj4PhjR9i@t}eN{n5Baje-0{}8$oaB$TAirflHX;>NGxx5tK7;OPKx?uWmbsw{d^M1%v^$i|m
zcsLS>B@qc&J4_(K7e}V(TnO;T#UBA(SCh5;s_`!JM}q^?Pn^4xN;wA5&b>P6lb^+^
z;^uvv_g&V1Fu5?5HB|tiPMq8OQS{cD{$9?8VrxHG?reIT$)p`!FA@CplxUf^Zt+bU
zl*J4A_UKUH$E@QD$}-n+j*wAU8?4+yp%gKMLo4
zO-In0_#bw|8;q~+&8Eqg^C91#m6{DzXH{^JD?fa%g;YVy09ZG&}1l
zdl)`xCo(<*G4ya1ioSqlmCtB8jw~^MI@N!On~l`}N~COay9j?ZKw!(0bQWBwj^oJ=
z4??S(D&crYh)W9!OI!~8LHV5j@)~+NZV`HMzG1|jKw6*$EKcrPm{!z}L|R?ZC__%3
zlxg^UiB(gLn~E(!c=x6JafPInEgfWes(qM!rTuV4=IvkBZW(X8PYu<~H#PjUy0hqU
zrtW7C{(uCt_2ulH2a
zK~)a?*k_h?i^oquir2f@3pnZ?-CIx~L%PZr+_V6xm&{gw
z3YId;%K}$W-B2~|-uivcd;z&eUG*}2{`yC%)BJ-sq*z^hW`tL=n|vO`((N-3xg}o?
z#0P(`7;ID(Q`KEfmvvAJLO8ZI?0$Ztkpf1i_9pdRs>;UKYiF)k4u#y~h;tqnye@56
z*Hl@B1+G47WU{m+b&Lnf2Pgb`t8cN}LzM2S;@10<%Jqj%%@K#cM{xxYtDJ}D?XRjp
z{H$nW`n0=St;%M#bTj91VpZ8V8O|oIs48qw!LfDK#+Wrndn#|LGbT6T;a;DxSUc=r
z>NS=fO|u8r?RjBK7}p?=f0=a$(bL_&8Y|F(a5EwsKf7H}rJEl6I2D`%I$iM|Cr{i1
z82_cptwSdci9Os_5@udYuBYu#DM2Y
z*(ig-5Up5GWu&GuKG!k=Hhf^4-BMD92hs1=&(JG>1;SS?=N40rN8Dw@lbs^!i;5=O
z@*sM{cyad7z@cwsLQitS0)FO34dd-PG~x5nE~>m-nrsn`?cbX=s6N%&YINk|Vg@7v
z<+lkvO`eWPc%YQ7l&+X=W{&bknY#ix)-kuN!5Cjcj-mVmdHEo`yR(^^Qohp7EQFL=
zX5#&p*uWj(dm&B28tTo6ru0;C+I&V!wm^AT;N
zy*1Q_7y!lmrD;&_5Q0AziYH;P{x~QGL(&ewQ7~8x1*60Bzd<+>g@7ZYZM8_Rq3w2B
zqG)Z+u8!`C1O2dxX_T$jvYj^47O91_loICc%?Fg_|KE0+cYr?|E%JBxr@7wB+-k1>
z;sF4(NG;y
+
versionCode;
diff --git a/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java b/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java
index 58cd4e6..1fbd492 100644
--- a/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java
+++ b/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java
@@ -44,6 +44,8 @@ public class APPCONST {
+ "book_cache" + File.separator;
public static String HTML_CACHE_PATH = FileUtils.getCachePath() + File.separator
+ "html_cache" + File.separator;
+ public static String PLUGIN_DIR_PATH = App.getmContext().getFilesDir().getParent()
+ + File.separator + "plugin" + File.separator;
public static long exitTime;
public static final int exitConfirmTime = 2000;
@@ -143,7 +145,7 @@ public class APPCONST {
public static final String androidId = getAndroidId();
- public static String getAndroidId(){
+ public static String getAndroidId() {
return Settings.System.getString(App.getmContext().getContentResolver(), Settings.Secure.ANDROID_ID);
}
}
diff --git a/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java b/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java
index 5a66b96..4870894 100644
--- a/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java
+++ b/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java
@@ -38,6 +38,8 @@ public class URLCONST {
public static final String QUOTATION = "https://v1.hitokoto.cn/?encode=json&charset=utf-8";
+ public static final String DEFAULT_PLUGIN_CONFIG_URL = "https://fyreader.coding.net/p/img/d/Plugin/git/raw/master/release/config_FYReader.json";
+
public static String getDefaultDomain() {
return SharedPreUtils.getInstance().getString("domain", "fycz.me");
}
diff --git a/app/src/main/java/xyz/fycz/myreader/entity/PluginConfig.kt b/app/src/main/java/xyz/fycz/myreader/entity/PluginConfig.kt
new file mode 100644
index 0000000..a6b07c6
--- /dev/null
+++ b/app/src/main/java/xyz/fycz/myreader/entity/PluginConfig.kt
@@ -0,0 +1,13 @@
+package xyz.fycz.myreader.entity
+
+/**
+ * @author fengyue
+ * @date 2022/3/29 14:55
+ */
+data class PluginConfig(
+ val name: String,
+ val versionCode: Int,
+ val version: String,
+ val url: String,
+ val changelog: String
+)
diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/PluginUtils.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/PluginUtils.kt
new file mode 100644
index 0000000..3dcb729
--- /dev/null
+++ b/app/src/main/java/xyz/fycz/myreader/util/utils/PluginUtils.kt
@@ -0,0 +1,92 @@
+package xyz.fycz.myreader.util.utils
+
+import android.content.Context
+import android.util.Log
+import dalvik.system.DexClassLoader
+import xyz.fycz.dynamic.AppParam
+import xyz.fycz.dynamic.IAppLoader
+import xyz.fycz.myreader.application.App
+import xyz.fycz.myreader.common.APPCONST
+import xyz.fycz.myreader.common.URLCONST.DEFAULT_PLUGIN_CONFIG_URL
+import xyz.fycz.myreader.entity.PluginConfig
+import xyz.fycz.myreader.model.third3.Coroutine
+import xyz.fycz.myreader.model.third3.http.getProxyClient
+import xyz.fycz.myreader.model.third3.http.newCallResponse
+import xyz.fycz.myreader.model.third3.http.newCallResponseBody
+import xyz.fycz.myreader.model.third3.http.text
+import xyz.fycz.myreader.util.SharedPreUtils
+import xyz.fycz.myreader.util.ToastUtils
+import java.io.File
+
+
+/**
+ * @author fengyue
+ * @date 2022/3/29 12:36
+ */
+object PluginUtils {
+
+ val TAG = PluginUtils.javaClass.simpleName
+
+ fun init() {
+ val pluginConfigUrl =
+ SharedPreUtils.getInstance().getString("pluginConfigUrl", DEFAULT_PLUGIN_CONFIG_URL)
+ var config: PluginConfig? = null
+ Coroutine.async {
+ val configJson = getProxyClient().newCallResponseBody {
+ url(pluginConfigUrl)
+ }.text()
+ config = GSON.fromJsonObject(configJson)
+ val oldConfig = GSON.fromJsonObject(
+ SharedPreUtils.getInstance().getString("pluginConfig")
+ ) ?: PluginConfig("dynamic.dex", 100, "", "", "")
+ if (config != null) {
+ if (config!!.versionCode > oldConfig.versionCode) {
+ downloadPlugin(config!!)
+ SharedPreUtils.getInstance().putString("pluginConfig", configJson)
+ }
+ } else {
+ config = oldConfig
+ }
+ }.onSuccess {
+ loadAppLoader(App.getmContext(), config)
+ }
+ }
+
+ private suspend fun downloadPlugin(config: PluginConfig) {
+ val res = getProxyClient().newCallResponseBody {
+ url(config.url)
+ }
+ FileUtils.getFile(APPCONST.PLUGIN_DIR_PATH + config.name)
+ .writeBytes(res.byteStream().readBytes())
+ }
+
+ private fun loadAppLoader(context: Context, config: PluginConfig?) {
+ config?.let {
+ val pluginPath = APPCONST.PLUGIN_DIR_PATH + it.name
+ val desFile = File(pluginPath)
+ if (desFile.exists()) {
+ val dexClassLoader = DexClassLoader(
+ pluginPath,
+ FileUtils.getCachePath(),
+ null,
+ context.classLoader
+ )
+ try {
+ val libClazz = dexClassLoader.loadClass("xyz.fycz.dynamic.AppLoadImpl")
+ val appLoader = libClazz.newInstance() as IAppLoader?
+ appLoader?.run {
+ val appParam = AppParam()
+ appParam.classLoader = context.classLoader
+ appParam.packageName = context.packageName
+ appParam.appInfo = context.applicationInfo
+ onLoad(appParam)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ } else {
+ Log.d(TAG, pluginPath + "文件不存在")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 4166b49..caa4464 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,7 +14,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/dynamic/.gitignore b/dynamic/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/dynamic/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/dynamic/build.gradle b/dynamic/build.gradle
new file mode 100644
index 0000000..27227fc
--- /dev/null
+++ b/dynamic/build.gradle
@@ -0,0 +1,38 @@
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ compileSdkVersion 29
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 29
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+ implementation 'androidx.core:core-ktx:1.6.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ compileOnly("me.fycz.maple:maple:1.6")
+}
\ No newline at end of file
diff --git a/dynamic/consumer-rules.pro b/dynamic/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/dynamic/proguard-rules.pro b/dynamic/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/dynamic/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/dynamic/src/androidTest/java/xyz/fycz/dynamic/ExampleInstrumentedTest.kt b/dynamic/src/androidTest/java/xyz/fycz/dynamic/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..2a072e6
--- /dev/null
+++ b/dynamic/src/androidTest/java/xyz/fycz/dynamic/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package xyz.fycz.dynamic
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("xyz.fycz.dynamic.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/dynamic/src/main/AndroidManifest.xml b/dynamic/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..338ca0d
--- /dev/null
+++ b/dynamic/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/dynamic/src/main/java/xyz/fycz/dynamic/AppLoadImpl.kt b/dynamic/src/main/java/xyz/fycz/dynamic/AppLoadImpl.kt
new file mode 100644
index 0000000..a2e3bc0
--- /dev/null
+++ b/dynamic/src/main/java/xyz/fycz/dynamic/AppLoadImpl.kt
@@ -0,0 +1,38 @@
+package xyz.fycz.dynamic
+
+import me.fycz.maple.MapleBridge
+import me.fycz.maple.MapleUtils
+import me.fycz.maple.MethodHook
+import me.fycz.maple.MethodReplacement
+
+/**
+ * @author fengyue
+ * @date 2022/3/29 11:59
+ */
+class AppLoadImpl : IAppLoader {
+ override fun onLoad(appParam: AppParam) {
+ try {
+ MapleUtils.findAndHookMethod(
+ "xyz.fycz.myreader.util.utils.AdUtils",
+ appParam.classLoader,
+ "checkHasAd",
+ Boolean::class.java,
+ Boolean::class.java,
+ object : MethodReplacement() {
+ override fun replaceHookedMethod(param: MapleBridge.MethodHookParam): Any? {
+ val just = MapleUtils.findMethodExact(
+ "io.reactivex.Single",
+ appParam.classLoader,
+ "just",
+ Any::class.java
+ )
+ return just.invoke(null, false)
+ }
+ }
+ )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ MapleUtils.log(e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/dynamic/src/main/java/xyz/fycz/dynamic/AppParam.java b/dynamic/src/main/java/xyz/fycz/dynamic/AppParam.java
new file mode 100644
index 0000000..3a2cd49
--- /dev/null
+++ b/dynamic/src/main/java/xyz/fycz/dynamic/AppParam.java
@@ -0,0 +1,18 @@
+package xyz.fycz.dynamic;
+
+import android.content.pm.ApplicationInfo;
+
+/**
+ * @author fengyue
+ * @date 2022/3/29 11:31
+ */
+public class AppParam {
+ /** The name of the package being loaded. */
+ public String packageName;
+
+ /** The ClassLoader used for this package. */
+ public ClassLoader classLoader;
+
+ /** More information about the application being loaded. */
+ public ApplicationInfo appInfo;
+}
diff --git a/dynamic/src/main/java/xyz/fycz/dynamic/IAppLoader.java b/dynamic/src/main/java/xyz/fycz/dynamic/IAppLoader.java
new file mode 100644
index 0000000..b9c615a
--- /dev/null
+++ b/dynamic/src/main/java/xyz/fycz/dynamic/IAppLoader.java
@@ -0,0 +1,9 @@
+package xyz.fycz.dynamic;
+
+/**
+ * @author fengyue
+ * @date 2022/3/29 11:27
+ */
+public interface IAppLoader {
+ void onLoad(AppParam appParam);
+}
diff --git a/dynamic/src/test/java/xyz/fycz/dynamic/ExampleUnitTest.kt b/dynamic/src/test/java/xyz/fycz/dynamic/ExampleUnitTest.kt
new file mode 100644
index 0000000..e853819
--- /dev/null
+++ b/dynamic/src/test/java/xyz/fycz/dynamic/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package xyz.fycz.dynamic
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 9fdc043..31799a6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,3 @@
include ':app'
include ':DialogX'
+include ':dynamic'
|