From 829d37f6201b765cfa252378747197b334858298 Mon Sep 17 00:00:00 2001 From: John Zhao Date: Thu, 10 Jan 2019 14:55:46 +0800 Subject: [PATCH 01/21] feat(android): add android wrapper project --- wrappers/android/mynteye/.gitignore | 8 + wrappers/android/mynteye/app/.gitignore | 1 + wrappers/android/mynteye/app/build.gradle | 30 +++ .../android/mynteye/app/proguard-rules.pro | 21 +++ .../mynteye/demo/ExampleInstrumentedTest.java | 26 +++ .../mynteye/app/src/main/AndroidManifest.xml | 20 ++ .../slightech/mynteye/demo/MainActivity.java | 13 ++ .../app/src/main/res/layout/activity_main.xml | 21 +++ .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 11 ++ .../mynteye/demo/ExampleUnitTest.java | 17 ++ wrappers/android/mynteye/build.gradle | 27 +++ wrappers/android/mynteye/gradle.properties | 15 ++ .../mynteye/gradle/dependencies.gradle | 44 +++++ .../mynteye/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + wrappers/android/mynteye/gradlew | 172 ++++++++++++++++++ wrappers/android/mynteye/gradlew.bat | 84 +++++++++ wrappers/android/mynteye/settings.gradle | 1 + 20 files changed, 525 insertions(+) create mode 100644 wrappers/android/mynteye/.gitignore create mode 100644 wrappers/android/mynteye/app/.gitignore create mode 100644 wrappers/android/mynteye/app/build.gradle create mode 100644 wrappers/android/mynteye/app/proguard-rules.pro create mode 100644 wrappers/android/mynteye/app/src/androidTest/java/com/slightech/mynteye/demo/ExampleInstrumentedTest.java create mode 100644 wrappers/android/mynteye/app/src/main/AndroidManifest.xml create mode 100644 wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java create mode 100644 wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml create mode 100644 wrappers/android/mynteye/app/src/main/res/values/colors.xml create mode 100644 wrappers/android/mynteye/app/src/main/res/values/strings.xml create mode 100644 wrappers/android/mynteye/app/src/main/res/values/styles.xml create mode 100644 wrappers/android/mynteye/app/src/test/java/com/slightech/mynteye/demo/ExampleUnitTest.java create mode 100644 wrappers/android/mynteye/build.gradle create mode 100644 wrappers/android/mynteye/gradle.properties create mode 100644 wrappers/android/mynteye/gradle/dependencies.gradle create mode 100644 wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.jar create mode 100644 wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.properties create mode 100755 wrappers/android/mynteye/gradlew create mode 100644 wrappers/android/mynteye/gradlew.bat create mode 100644 wrappers/android/mynteye/settings.gradle diff --git a/wrappers/android/mynteye/.gitignore b/wrappers/android/mynteye/.gitignore new file mode 100644 index 0000000..0df7064 --- /dev/null +++ b/wrappers/android/mynteye/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/wrappers/android/mynteye/app/.gitignore b/wrappers/android/mynteye/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/wrappers/android/mynteye/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/wrappers/android/mynteye/app/build.gradle b/wrappers/android/mynteye/app/build.gradle new file mode 100644 index 0000000..cc86012 --- /dev/null +++ b/wrappers/android/mynteye/app/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion xversions.compileSdk + defaultConfig { + applicationId "com.slightech.mynteye.demo" + minSdkVersion xversions.minSdk + targetSdkVersion xversions.targetSdk + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation xdeps.support.appcompat + implementation xdeps.support.constraint + + testImplementation xdeps.junit + androidTestImplementation xdeps.support.test.runner + androidTestImplementation xdeps.support.test.espresso +} diff --git a/wrappers/android/mynteye/app/proguard-rules.pro b/wrappers/android/mynteye/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/wrappers/android/mynteye/app/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 diff --git a/wrappers/android/mynteye/app/src/androidTest/java/com/slightech/mynteye/demo/ExampleInstrumentedTest.java b/wrappers/android/mynteye/app/src/androidTest/java/com/slightech/mynteye/demo/ExampleInstrumentedTest.java new file mode 100644 index 0000000..a7c3f03 --- /dev/null +++ b/wrappers/android/mynteye/app/src/androidTest/java/com/slightech/mynteye/demo/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.slightech.mynteye.demo; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.slightech.mynteye.demo", appContext.getPackageName()); + } +} diff --git a/wrappers/android/mynteye/app/src/main/AndroidManifest.xml b/wrappers/android/mynteye/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..76abdcc --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java new file mode 100644 index 0000000..e4578d3 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java @@ -0,0 +1,13 @@ +package com.slightech.mynteye.demo; + +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } +} diff --git a/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml b/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..3b779f6 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/wrappers/android/mynteye/app/src/main/res/values/colors.xml b/wrappers/android/mynteye/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..b2b4267 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #008577 + #00574B + #D81B60 + diff --git a/wrappers/android/mynteye/app/src/main/res/values/strings.xml b/wrappers/android/mynteye/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d69f619 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + mynteye + diff --git a/wrappers/android/mynteye/app/src/main/res/values/styles.xml b/wrappers/android/mynteye/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..705be27 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/wrappers/android/mynteye/app/src/test/java/com/slightech/mynteye/demo/ExampleUnitTest.java b/wrappers/android/mynteye/app/src/test/java/com/slightech/mynteye/demo/ExampleUnitTest.java new file mode 100644 index 0000000..0e0a74c --- /dev/null +++ b/wrappers/android/mynteye/app/src/test/java/com/slightech/mynteye/demo/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.slightech.mynteye.demo; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/wrappers/android/mynteye/build.gradle b/wrappers/android/mynteye/build.gradle new file mode 100644 index 0000000..e7a3ada --- /dev/null +++ b/wrappers/android/mynteye/build.gradle @@ -0,0 +1,27 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + apply from: rootProject.file('gradle/dependencies.gradle') + + repositories { + google() + jcenter() + } + dependencies { + classpath xplugins.android.buildGradle + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/wrappers/android/mynteye/gradle.properties b/wrappers/android/mynteye/gradle.properties new file mode 100644 index 0000000..82618ce --- /dev/null +++ b/wrappers/android/mynteye/gradle.properties @@ -0,0 +1,15 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + + diff --git a/wrappers/android/mynteye/gradle/dependencies.gradle b/wrappers/android/mynteye/gradle/dependencies.gradle new file mode 100644 index 0000000..da2d862 --- /dev/null +++ b/wrappers/android/mynteye/gradle/dependencies.gradle @@ -0,0 +1,44 @@ +ext { + xplugins = [ + 'android': [ + // Gradle: https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google + buildGradle: 'com.android.tools.build:gradle:3.2.1', + ], + ] + + xversions = [ + 'compileSdk': 28, + 'minSdk': 21, + 'targetSdk': 28, + + 'androidSupport': '28.0.0', + ] + + xdeps = [ + // Support Library + // https://developer.android.com/topic/libraries/support-library/ + 'support': [ + // Android AppCompat Library V7 + // https://mvnrepository.com/artifact/com.android.support/appcompat-v7?repo=google + 'appcompat': "com.android.support:appcompat-v7:${xversions.androidSupport}", + + // Android ConstraintLayout Solver + // https://mvnrepository.com/artifact/com.android.support.constraint/constraint-layout-solver + 'constraint': 'com.android.support.constraint:constraint-layout:1.1.3', + // Android Multi Dex Library + // https://mvnrepository.com/artifact/com.android.support/multidex + 'multidex': 'com.android.support:multidex:1.0.3', + + // Test apps on Android + // https://developer.android.com/training/testing/ + 'test': [ + 'espresso': 'com.android.support.test.espresso:espresso-core:3.0.2', + 'rules': 'com.android.support.test:rules:1.0.2', + 'runner': 'com.android.support.test:runner:1.0.2', + ], + ], + + // JUnit4: https://github.com/junit-team/junit4 + 'junit': 'junit:junit:4.12', + ] +} diff --git a/wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.jar b/wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 zzqSuO91lJod(SBDDw<*sX(+F6Uq~YAeYV#2A;XQu_p=N5X+#cmu19Qk>QAnV=k!?wbk5I;tDWgFc}0NkvC*G=V+Yh1cyeJVq~9czZiDXe+S=VfL2g`LWo8om z$Y~FQc6MFjV-t1Y`^D9XMwY*U_re2R?&(O~68T&D4S{X`6JYU-pz=}ew-)V0AOUT1 zVOkHAB-8uBcRjLvz<9HS#a@X*Kc@|W)nyiSgi|u5$Md|P()%2(?olGg@ypoJwp6>m z*dnfjjWC>?_1p;%1brqZyDRR;8EntVA92EJ3ByOxj6a+bhPl z;a?m4rQAV1@QU^#M1HX)0+}A<7TCO`ZR_RzF}X9-M>cRLyN4C+lCk2)kT^3gN^`IT zNP~fAm(wyIoR+l^lQDA(e1Yv}&$I!n?&*p6?lZcQ+vGLLd~fM)qt}wsbf3r=tmVYe zl)ntf#E!P7wlakP9MXS7m0nsAmqxZ*)#j;M&0De`oNmFgi$ov#!`6^4)iQyxg5Iuj zjLAhzQ)r`^hf7`*1`Rh`X;LVBtDSz@0T?kkT1o!ijeyTGt5vc^Cd*tmNgiNo^EaWvaC8$e+nb_{W01j3%=1Y&92YacjCi>eNbwk%-gPQ@H-+4xskQ}f_c=jg^S-# zYFBDf)2?@5cy@^@FHK5$YdAK9cI;!?Jgd}25lOW%xbCJ>By3=HiK@1EM+I46A)Lsd zeT|ZH;KlCml=@;5+hfYf>QNOr^XNH%J-lvev)$Omy8MZ`!{`j>(J5cG&ZXXgv)TaF zg;cz99i$4CX_@3MIb?GL0s*8J=3`#P(jXF(_(6DXZjc@(@h&=M&JG)9&Te1?(^XMW zjjC_70|b=9hB6pKQi`S^Ls7JyJw^@P>Ko^&q8F&?>6i;#CbxUiLz1ZH4lNyd@QACd zu>{!sqjB!2Dg}pbAXD>d!3jW}=5aN0b;rw*W>*PAxm7D)aw(c*RX2@bTGEI|RRp}vw7;NR2wa;rXN{L{Q#=Fa z$x@ms6pqb>!8AuV(prv>|aU8oWV={C&$c zMa=p=CDNOC2tISZcd8~18GN5oTbKY+Vrq;3_obJlfSKRMk;Hdp1`y`&LNSOqeauR_ z^j*Ojl3Ohzb5-a49A8s|UnM*NM8tg}BJXdci5%h&;$afbmRpN0&~9rCnBA`#lG!p zc{(9Y?A0Y9yo?wSYn>iigf~KP$0*@bGZ>*YM4&D;@{<%Gg5^uUJGRrV4 z(aZOGB&{_0f*O=Oi0k{@8vN^BU>s3jJRS&CJOl3o|BE{FAA&a#2YYiX3pZz@|Go-F z|Fly;7eX2OTs>R}<`4RwpHFs9nwh)B28*o5qK1Ge=_^w0m`uJOv!=&!tzt#Save(C zgKU=Bsgql|`ui(e1KVxR`?>Dx>(rD1$iWp&m`v)3A!j5(6vBm*z|aKm*T*)mo(W;R zNGo2`KM!^SS7+*9YxTm6YMm_oSrLceqN*nDOAtagULuZl5Q<7mOnB@Hq&P|#9y{5B z!2x+2s<%Cv2Aa0+u{bjZXS);#IFPk(Ph-K7K?3i|4ro> zRbqJoiOEYo(Im^((r}U4b8nvo_>4<`)ut`24?ILnglT;Pd&U}$lV3U$F9#PD(O=yV zgNNA=GW|(E=&m_1;uaNmipQe?pon4{T=zK!N!2_CJL0E*R^XXIKf*wi!>@l}3_P9Z zF~JyMbW!+n-+>!u=A1ESxzkJy$DRuG+$oioG7(@Et|xVbJ#BCt;J43Nvj@MKvTxzy zMmjNuc#LXBxFAwIGZJk~^!q$*`FME}yKE8d1f5Mp}KHNq(@=Z8YxV}0@;YS~|SpGg$_jG7>_8WWYcVx#4SxpzlV9N4aO>K{c z$P?a_fyDzGX$Of3@ykvedGd<@-R;M^Shlj*SswJLD+j@hi_&_>6WZ}#AYLR0iWMK|A zH_NBeu(tMyG=6VO-=Pb>-Q#$F*or}KmEGg*-n?vWQREURdB#+6AvOj*I%!R-4E_2$ zU5n9m>RWs|Wr;h2DaO&mFBdDb-Z{APGQx$(L`if?C|njd*fC=rTS%{o69U|meRvu?N;Z|Y zbT|ojL>j;q*?xXmnHH#3R4O-59NV1j=uapkK7}6@Wo*^Nd#(;$iuGsb;H315xh3pl zHaJ>h-_$hdNl{+|Zb%DZH%ES;*P*v0#}g|vrKm9;j-9e1M4qX@zkl&5OiwnCz=tb6 zz<6HXD+rGIVpGtkb{Q^LIgExOm zz?I|oO9)!BOLW#krLmWvX5(k!h{i>ots*EhpvAE;06K|u_c~y{#b|UxQ*O@Ks=bca z^_F0a@61j3I(Ziv{xLb8AXQj3;R{f_l6a#H5ukg5rxwF9A$?Qp-Mo54`N-SKc}fWp z0T)-L@V$$&my;l#Ha{O@!fK4-FSA)L&3<${Hcwa7ue`=f&YsXY(NgeDU#sRlT3+9J z6;(^(sjSK@3?oMo$%L-nqy*E;3pb0nZLx6 z;h5)T$y8GXK1DS-F@bGun8|J(v-9o=42&nLJy#}M5D0T^5VWBNn$RpC zZzG6Bt66VY4_?W=PX$DMpKAI!d`INr) zkMB{XPQ<52rvWVQqgI0OL_NWxoe`xxw&X8yVftdODPj5|t}S6*VMqN$-h9)1MBe0N zYq?g0+e8fJCoAksr0af1)FYtz?Me!Cxn`gUx&|T;)695GG6HF7!Kg1zzRf_{VWv^bo81v4$?F6u2g|wxHc6eJQAg&V z#%0DnWm2Rmu71rPJ8#xFUNFC*V{+N_qqFH@gYRLZ6C?GAcVRi>^n3zQxORPG)$-B~ z%_oB?-%Zf7d*Fe;cf%tQwcGv2S?rD$Z&>QC2X^vwYjnr5pa5u#38cHCt4G3|efuci z@3z=#A13`+ztmp;%zjXwPY_aq-;isu*hecWWX_=Z8paSqq7;XYnUjK*T>c4~PR4W7 z#C*%_H&tfGx`Y$w7`dXvVhmovDnT>btmy~SLf>>~84jkoQ%cv=MMb+a{JV&t0+1`I z32g_Y@yDhKe|K^PevP~MiiVl{Ou7^Mt9{lOnXEQ`xY^6L8D$705GON{!1?1&YJEl#fTf5Z)da=yiEQ zGgtC-soFGOEBEB~ZF_{7b(76En>d}mI~XIwNw{e>=Fv)sgcw@qOsykWr?+qAOZSVrQfg}TNI ztKNG)1SRrAt6#Q?(me%)>&A_^DM`pL>J{2xu>xa$3d@90xR61TQDl@fu%_85DuUUA za9tn64?At;{`BAW6oykwntxHeDpXsV#{tmt5RqdN7LtcF4vR~_kZNT|wqyR#z^Xcd zFdymVRZvyLfTpBT>w9<)Ozv@;Yk@dOSVWbbtm^y@@C>?flP^EgQPAwsy75bveo=}T zFxl(f)s)j(0#N_>Or(xEuV(n$M+`#;Pc$1@OjXEJZumkaekVqgP_i}p`oTx;terTx zZpT+0dpUya2hqlf`SpXN{}>PfhajNk_J0`H|2<5E;U5Vh4F8er z;RxLSFgpGhkU>W?IwdW~NZTyOBrQ84H7_?gviIf71l`EETodG9a1!8e{jW?DpwjL? zGEM&eCzwoZt^P*8KHZ$B<%{I}>46IT%jJ3AnnB5P%D2E2Z_ z1M!vr#8r}1|KTqWA4%67ZdbMW2YJ81b(KF&SQ2L1Qn(y-=J${p?xLMx3W7*MK;LFQ z6Z`aU;;mTL4XrrE;HY*Rkh6N%?qviUGNAKiCB~!P}Z->IpO6E(gGd7I#eDuT7j|?nZ zK}I(EJ>$Kb&@338M~O+em9(L!+=0zBR;JAQesx|3?Ok90)D1aS9P?yTh6Poh8Cr4X zk3zc=f2rE7jj+aP7nUsr@~?^EGP>Q>h#NHS?F{Cn`g-gD<8F&dqOh-0sa%pfL`b+1 zUsF*4a~)KGb4te&K0}bE>z3yb8% zibb5Q%Sfiv7feb1r0tfmiMv z@^4XYwg@KZI=;`wC)`1jUA9Kv{HKe2t$WmRcR4y8)VAFjRi zaz&O7Y2tDmc5+SX(bj6yGHYk$dBkWc96u3u&F)2yEE~*i0F%t9Kg^L6MJSb&?wrXi zGSc;_rln$!^ybwYBeacEFRsVGq-&4uC{F)*Y;<0y7~USXswMo>j4?~5%Zm!m@i@-> zXzi82sa-vpU{6MFRktJy+E0j#w`f`>Lbog{zP|9~hg(r{RCa!uGe>Yl536cn$;ouH za#@8XMvS-kddc1`!1LVq;h57~zV`7IYR}pp3u!JtE6Q67 zq3H9ZUcWPm2V4IukS}MCHSdF0qg2@~ufNx9+VMjQP&exiG_u9TZAeAEj*jw($G)zL zq9%#v{wVyOAC4A~AF=dPX|M}MZV)s(qI9@aIK?Pe+~ch|>QYb+78lDF*Nxz2-vpRbtQ*F4$0fDbvNM#CCatgQ@z1+EZWrt z2dZfywXkiW=no5jus-92>gXn5rFQ-COvKyegmL=4+NPzw6o@a?wGE-1Bt;pCHe;34K%Z z-FnOb%!nH;)gX+!a3nCk?5(f1HaWZBMmmC@lc({dUah+E;NOros{?ui1zPC-Q0);w zEbJmdE$oU$AVGQPdm{?xxI_0CKNG$LbY*i?YRQ$(&;NiA#h@DCxC(U@AJ$Yt}}^xt-EC_ z4!;QlLkjvSOhdx!bR~W|Ezmuf6A#@T`2tsjkr>TvW*lFCMY>Na_v8+{Y|=MCu1P8y z89vPiH5+CKcG-5lzk0oY>~aJC_0+4rS@c@ZVKLAp`G-sJB$$)^4*A!B zmcf}lIw|VxV9NSoJ8Ag3CwN&d7`|@>&B|l9G8tXT^BDHOUPrtC70NgwN4${$k~d_4 zJ@eo6%YQnOgq$th?0{h`KnqYa$Nz@vlHw<%!C5du6<*j1nwquk=uY}B8r7f|lY+v7 zm|JU$US08ugor8E$h3wH$c&i~;guC|3-tqJy#T;v(g( zBZtPMSyv%jzf->435yM(-UfyHq_D=6;ouL4!ZoD+xI5uCM5ay2m)RPmm$I}h>()hS zO!0gzMxc`BPkUZ)WXaXam%1;)gedA7SM8~8yIy@6TPg!hR0=T>4$Zxd)j&P-pXeSF z9W`lg6@~YDhd19B9ETv(%er^Xp8Yj@AuFVR_8t*KS;6VHkEDKI#!@l!l3v6`W1`1~ zP{C@keuV4Q`Rjc08lx?zmT$e$!3esc9&$XZf4nRL(Z*@keUbk!GZi(2Bmyq*saOD? z3Q$V<*P-X1p2}aQmuMw9nSMbOzuASsxten7DKd6A@ftZ=NhJ(0IM|Jr<91uAul4JR zADqY^AOVT3a(NIxg|U;fyc#ZnSzw2cr}#a5lZ38>nP{05D)7~ad7JPhw!LqOwATXtRhK!w0X4HgS1i<%AxbFmGJx9?sEURV+S{k~g zGYF$IWSlQonq6}e;B(X(sIH|;52+(LYW}v_gBcp|x%rEAVB`5LXg_d5{Q5tMDu0_2 z|LOm$@K2?lrLNF=mr%YP|U-t)~9bqd+wHb4KuPmNK<}PK6e@aosGZK57=Zt+kcszVOSbe;`E^dN! ze7`ha3WUUU7(nS0{?@!}{0+-VO4A{7+nL~UOPW9_P(6^GL0h${SLtqG!} zKl~Ng5#@Sy?65wk9z*3SA`Dpd4b4T^@C8Fhd8O)k_4%0RZL5?#b~jmgU+0|DB%0Z) zql-cPC>A9HPjdOTpPC` zQwvF}uB5kG$Xr4XnaH#ruSjM*xG?_hT7y3G+8Ox`flzU^QIgb_>2&-f+XB6MDr-na zSi#S+c!ToK84<&m6sCiGTd^8pNdXo+$3^l3FL_E`0 z>8it5YIDxtTp2Tm(?}FX^w{fbfgh7>^8mtvN>9fWgFN_*a1P`Gz*dyOZF{OV7BC#j zQV=FQM5m>47xXgapI$WbPM5V`V<7J9tD)oz@d~MDoM`R^Y6-Na(lO~uvZlpu?;zw6 zVO1faor3dg#JEb5Q*gz4<W8tgC3nE2BG2jeIQs1)<{In&7hJ39x=;ih;CJDy)>0S1at*7n?Wr0ahYCpFjZ|@u91Zl7( zv;CSBRC65-6f+*JPf4p1UZ)k=XivKTX6_bWT~7V#rq0Xjas6hMO!HJN8GdpBKg_$B zwDHJF6;z?h<;GXFZan8W{XFNPpOj!(&I1`&kWO86p?Xz`a$`7qV7Xqev|7nn_lQuX ziGpU1MMYt&5dE2A62iX3;*0WzNB9*nSTzI%62A+N?f?;S>N@8M=|ef3gtQTIA*=yq zQAAjOqa!CkHOQo4?TsqrrsJLclXcP?dlAVv?v`}YUjo1Htt;6djP@NPFH+&p1I+f_ z)Y279{7OWomY8baT(4TAOlz1OyD{4P?(DGv3XyJTA2IXe=kqD)^h(@*E3{I~w;ws8 z)ZWv7E)pbEM zd3MOXRH3mQhks9 zv6{s;k0y5vrcjXaVfw8^>YyPo=oIqd5IGI{)+TZq5Z5O&hXAw%ZlL}^6FugH;-%vP zAaKFtt3i^ag226=f0YjzdPn6|4(C2sC5wHFX{7QF!tG1E-JFA`>eZ`}$ymcRJK?0c zN363o{&ir)QySOFY0vcu6)kX#;l??|7o{HBDVJN+17rt|w3;(C_1b>d;g9Gp=8YVl zYTtA52@!7AUEkTm@P&h#eg+F*lR zQ7iotZTcMR1frJ0*V@Hw__~CL>_~2H2cCtuzYIUD24=Cv!1j6s{QS!v=PzwQ(a0HS zBKx04KA}-Ue+%9d`?PG*hIij@54RDSQpA7|>qYVIrK_G6%6;#ZkR}NjUgmGju)2F`>|WJoljo)DJgZr4eo1k1i1+o z1D{>^RlpIY8OUaOEf5EBu%a&~c5aWnqM zxBpJq98f=%M^{4mm~5`CWl%)nFR64U{(chmST&2jp+-r z3675V<;Qi-kJud%oWnCLdaU-)xTnMM%rx%Jw6v@=J|Ir=4n-1Z23r-EVf91CGMGNz zb~wyv4V{H-hkr3j3WbGnComiqmS0vn?n?5v2`Vi>{Ip3OZUEPN7N8XeUtF)Ry6>y> zvn0BTLCiqGroFu|m2zG-;Xb6;W`UyLw)@v}H&(M}XCEVXZQoWF=Ykr5lX3XWwyNyF z#jHv)A*L~2BZ4lX?AlN3X#axMwOC)PoVy^6lCGse9bkGjb=qz%kDa6}MOmSwK`cVO zt(e*MW-x}XtU?GY5}9{MKhRhYOlLhJE5=ca+-RmO04^ z66z{40J=s=ey9OCdc(RCzy zd7Zr1%!y3}MG(D=wM_ebhXnJ@MLi7cImDkhm0y{d-Vm81j`0mbi4lF=eirlr)oW~a zCd?26&j^m4AeXEsIUXiTal)+SPM4)HX%%YWF1?(FV47BaA`h9m67S9x>hWMVHx~Hg z1meUYoLL(p@b3?x|9DgWeI|AJ`Ia84*P{Mb%H$ZRROouR4wZhOPX15=KiBMHl!^JnCt$Az`KiH^_d>cev&f zaG2>cWf$=A@&GP~DubsgYb|L~o)cn5h%2`i^!2)bzOTw2UR!>q5^r&2Vy}JaWFUQE04v>2;Z@ZPwXr?y&G(B^@&y zsd6kC=hHdKV>!NDLIj+3rgZJ|dF`%N$DNd;B)9BbiT9Ju^Wt%%u}SvfM^=|q-nxDG zuWCQG9e#~Q5cyf8@y76#kkR^}{c<_KnZ0QsZcAT|YLRo~&tU|N@BjxOuy`#>`X~Q< z?R?-Gsk$$!oo(BveQLlUrcL#eirhgBLh`qHEMg`+sR1`A=1QX7)ZLMRT+GBy?&mM8 zQG^z-!Oa&J-k7I(3_2#Q6Bg=NX<|@X&+YMIOzfEO2$6Mnh}YV!m!e^__{W@-CTprr zbdh3f=BeCD$gHwCrmwgM3LAv3!Mh$wM)~KWzp^w)Cu6roO7uUG5z*}i0_0j47}pK; ztN530`ScGatLOL06~zO)Qmuv`h!gq5l#wx(EliKe&rz-5qH(hb1*fB#B+q`9=jLp@ zOa2)>JTl7ovxMbrif`Xe9;+fqB1K#l=Dv!iT;xF zdkCvS>C5q|O;}ns3AgoE({Ua-zNT-9_5|P0iANmC6O76Sq_(AN?UeEQJ>#b54fi3k zFmh+P%b1x3^)0M;QxXLP!BZ^h|AhOde*{9A=f3|Xq*JAs^Y{eViF|=EBfS6L%k4ip zk+7M$gEKI3?bQg?H3zaE@;cyv9kv;cqK$VxQbFEsy^iM{XXW0@2|DOu$!-k zSFl}Y=jt-VaT>Cx*KQnHTyXt}f9XswFB9ibYh+k2J!ofO+nD?1iw@mwtrqI4_i?nE zhLkPp41ED62me}J<`3RN80#vjW;wt`pP?%oQ!oqy7`miL>d-35a=qotK$p{IzeSk# ze_$CFYp_zIkrPFVaW^s#U4xT1lI^A0IBe~Y<4uS%zSV=wcuLr%gQT=&5$&K*bwqx| zWzCMiz>7t^Et@9CRUm9E+@hy~sBpm9fri$sE1zgLU((1?Yg{N1Sars=DiW&~Zw=3I zi7y)&oTC?UWD2w97xQ&5vx zRXEBGeJ(I?Y}eR0_O{$~)bMJRTsNUPIfR!xU9PE7A>AMNr_wbrFK>&vVw=Y;RH zO$mlpmMsQ}-FQ2cSj7s7GpC+~^Q~dC?y>M}%!-3kq(F3hGWo9B-Gn02AwUgJ>Z-pKOaj zysJBQx{1>Va=*e@sLb2z&RmQ7ira;aBijM-xQ&cpR>X3wP^foXM~u1>sv9xOjzZpX z0K;EGouSYD~oQ&lAafj3~EaXfFShC+>VsRlEMa9cg9i zFxhCKO}K0ax6g4@DEA?dg{mo>s+~RPI^ybb^u--^nTF>**0l5R9pocwB?_K)BG_)S zyLb&k%XZhBVr7U$wlhMqwL)_r&&n%*N$}~qijbkfM|dIWP{MyLx}X&}ES?}7i;9bW zmTVK@zR)7kE2+L42Q`n4m0VVg5l5(W`SC9HsfrLZ=v%lpef=Gj)W59VTLe+Z$8T8i z4V%5+T0t8LnM&H>Rsm5C%qpWBFqgTwL{=_4mE{S3EnBXknM&u8n}A^IIM4$s3m(Rd z>zq=CP-!9p9es2C*)_hoL@tDYABn+o#*l;6@7;knWIyDrt5EuakO99S$}n((Fj4y} zD!VvuRzghcE{!s;jC*<_H$y6!6QpePo2A3ZbX*ZzRnQq*b%KK^NF^z96CHaWmzU@f z#j;y?X=UP&+YS3kZx7;{ zDA{9(wfz7GF`1A6iB6fnXu0?&d|^p|6)%3$aG0Uor~8o? z*e}u#qz7Ri?8Uxp4m_u{a@%bztvz-BzewR6bh*1Xp+G=tQGpcy|4V_&*aOqu|32CM zz3r*E8o8SNea2hYJpLQ-_}R&M9^%@AMx&`1H8aDx4j%-gE+baf2+9zI*+Pmt+v{39 zDZ3Ix_vPYSc;Y;yn68kW4CG>PE5RoaV0n@#eVmk?p$u&Fy&KDTy!f^Hy6&^-H*)#u zdrSCTJPJw?(hLf56%2;_3n|ujUSJOU8VPOTlDULwt0jS@j^t1WS z!n7dZIoT+|O9hFUUMbID4Ec$!cc($DuQWkocVRcYSikFeM&RZ=?BW)mG4?fh#)KVG zcJ!<=-8{&MdE)+}?C8s{k@l49I|Zwswy^ZN3;E!FKyglY~Aq?4m74P-0)sMTGXqd5(S<-(DjjM z&7dL-Mr8jhUCAG$5^mI<|%`;JI5FVUnNj!VO2?Jiqa|c2;4^n!R z`5KK0hyB*F4w%cJ@Un6GC{mY&r%g`OX|1w2$B7wxu97%<@~9>NlXYd9RMF2UM>(z0 zouu4*+u+1*k;+nFPk%ly!nuMBgH4sL5Z`@Rok&?Ef=JrTmvBAS1h?C0)ty5+yEFRz zY$G=coQtNmT@1O5uk#_MQM1&bPPnspy5#>=_7%WcEL*n$;sSAZcXxMpcXxLe;_mLA z5F_paad+bGZV*oh@8h0(|D2P!q# zTHjmiphJ=AazSeKQPkGOR-D8``LjzToyx{lfK-1CDD6M7?pMZOdLKFtjZaZMPk4}k zW)97Fh(Z+_Fqv(Q_CMH-YYi?fR5fBnz7KOt0*t^cxmDoIokc=+`o# zrud|^h_?KW=Gv%byo~(Ln@({?3gnd?DUf-j2J}|$Mk>mOB+1{ZQ8HgY#SA8END(Zw z3T+W)a&;OO54~m}ffemh^oZ!Vv;!O&yhL0~hs(p^(Yv=(3c+PzPXlS5W79Er8B1o* z`c`NyS{Zj_mKChj+q=w)B}K za*zzPhs?c^`EQ;keH{-OXdXJet1EsQ)7;{3eF!-t^4_Srg4(Ot7M*E~91gwnfhqaM zNR7dFaWm7MlDYWS*m}CH${o?+YgHiPC|4?X?`vV+ws&Hf1ZO-w@OGG^o4|`b{bLZj z&9l=aA-Y(L11!EvRjc3Zpxk7lc@yH1e$a}8$_-r$)5++`_eUr1+dTb@ zU~2P1HM#W8qiNN3b*=f+FfG1!rFxnNlGx{15}BTIHgxO>Cq4 z;#9H9YjH%>Z2frJDJ8=xq>Z@H%GxXosS@Z>cY9ppF+)e~t_hWXYlrO6)0p7NBMa`+ z^L>-#GTh;k_XnE)Cgy|0Dw;(c0* zSzW14ZXozu)|I@5mRFF1eO%JM=f~R1dkNpZM+Jh(?&Zje3NgM{2ezg1N`AQg5%+3Y z64PZ0rPq6;_)Pj-hyIOgH_Gh`1$j1!jhml7ksHA1`CH3FDKiHLz+~=^u@kUM{ilI5 z^FPiJ7mSrzBs9{HXi2{sFhl5AyqwUnU{sPcUD{3+l-ZHAQ)C;c$=g1bdoxeG(5N01 zZy=t8i{*w9m?Y>V;uE&Uy~iY{pY4AV3_N;RL_jT_QtLFx^KjcUy~q9KcLE3$QJ{!)@$@En{UGG7&}lc*5Kuc^780;7Bj;)X?1CSy*^^ zPP^M)Pr5R>mvp3_hmCtS?5;W^e@5BjE>Cs<`lHDxj<|gtOK4De?Sf0YuK5GX9G93i zMYB{8X|hw|T6HqCf7Cv&r8A$S@AcgG1cF&iJ5=%+x;3yB`!lQ}2Hr(DE8=LuNb~Vs z=FO&2pdc16nD$1QL7j+!U^XWTI?2qQKt3H8=beVTdHHa9=MiJ&tM1RRQ-=+vy!~iz zj3O{pyRhCQ+b(>jC*H)J)%Wq}p>;?@W*Eut@P&?VU+Sdw^4kE8lvX|6czf{l*~L;J zFm*V~UC;3oQY(ytD|D*%*uVrBB}BbAfjK&%S;z;7$w68(8PV_whC~yvkZmX)xD^s6 z{$1Q}q;99W?*YkD2*;)tRCS{q2s@JzlO~<8x9}X<0?hCD5vpydvOw#Z$2;$@cZkYrp83J0PsS~!CFtY%BP=yxG?<@#{7%2sy zOc&^FJxsUYN36kSY)d7W=*1-{7ghPAQAXwT7z+NlESlkUH&8ODlpc8iC*iQ^MAe(B z?*xO4i{zFz^G=^G#9MsLKIN64rRJykiuIVX5~0#vAyDWc9-=6BDNT_aggS2G{B>dD ze-B%d3b6iCfc5{@yz$>=@1kdK^tX9qh0=ocv@9$ai``a_ofxT=>X7_Y0`X}a^M?d# z%EG)4@`^Ej_=%0_J-{ga!gFtji_byY&Vk@T1c|ucNAr(JNr@)nCWj?QnCyvXg&?FW;S-VOmNL6^km_dqiVjJuIASVGSFEos@EVF7St$WE&Z%)`Q##+0 zjaZ=JI1G@0!?l|^+-ZrNd$WrHBi)DA0-Eke>dp=_XpV<%CO_Wf5kQx}5e<90dt>8k zAi00d0rQ821nA>B4JHN7U8Zz=0;9&U6LOTKOaC1FC8GgO&kc=_wHIOGycL@c*$`ce703t%>S}mvxEnD-V!;6c`2(p74V7D0No1Xxt`urE66$0(ThaAZ1YVG#QP$ zy~NN%kB*zhZ2Y!kjn826pw4bh)75*e!dse+2Db(;bN34Uq7bLpr47XTX{8UEeC?2i z*{$`3dP}32${8pF$!$2Vq^gY|#w+VA_|o(oWmQX8^iw#n_crb(K3{69*iU?<%C-%H zuKi)3M1BhJ@3VW>JA`M>L~5*_bxH@Euy@niFrI$82C1}fwR$p2E&ZYnu?jlS}u7W9AyfdXh2pM>78bIt3 z)JBh&XE@zA!kyCDfvZ1qN^np20c1u#%P6;6tU&dx0phT1l=(mw7`u!-0e=PxEjDds z9E}{E!7f9>jaCQhw)&2TtG-qiD)lD(4jQ!q{`x|8l&nmtHkdul# zy+CIF8lKbp9_w{;oR+jSLtTfE+B@tOd6h=QePP>rh4@~!8c;Hlg9m%%&?e`*Z?qz5-zLEWfi>`ord5uHF-s{^bexKAoMEV@9nU z^5nA{f{dW&g$)BAGfkq@r5D)jr%!Ven~Q58c!Kr;*Li#`4Bu_?BU0`Y`nVQGhNZk@ z!>Yr$+nB=`z#o2nR0)V3M7-eVLuY`z@6CT#OTUXKnxZn$fNLPv7w1y7eGE=Qv@Hey`n;`U=xEl|q@CCV^#l)s0ZfT+mUf z^(j5r4)L5i2jnHW4+!6Si3q_LdOLQi<^fu?6WdohIkn79=jf%Fs3JkeXwF(?_tcF? z?z#j6iXEd(wJy4|p6v?xNk-)iIf2oX5^^Y3q3ziw16p9C6B;{COXul%)`>nuUoM*q zzmr|NJ5n)+sF$!yH5zwp=iM1#ZR`O%L83tyog-qh1I z0%dcj{NUs?{myT~33H^(%0QOM>-$hGFeP;U$puxoJ>>o-%Lk*8X^rx1>j|LtH$*)>1C!Pv&gd16%`qw5LdOIUbkNhaBBTo}5iuE%K&ZV^ zAr_)kkeNKNYJRgjsR%vexa~&8qMrQYY}+RbZ)egRg9_$vkoyV|Nc&MH@8L)`&rpqd zXnVaI@~A;Z^c3+{x=xgdhnocA&OP6^rr@rTvCnhG6^tMox$ulw2U7NgUtW%|-5VeH z_qyd47}1?IbuKtqNbNx$HR`*+9o=8`%vM8&SIKbkX9&%TS++x z5|&6P<%=F$C?owUI`%uvUq^yW0>`>yz!|WjzsoB9dT;2Dx8iSuK%%_XPgy0dTD4kd zDXF@&O_vBVVKQq(9YTClUPM30Sk7B!v7nOyV`XC!BA;BIVwphh+c)?5VJ^(C;GoQ$ zvBxr7_p*k$T%I1ke}`U&)$uf}I_T~#3XTi53OX)PoXVgxEcLJgZG^i47U&>LY(l%_ z;9vVDEtuMCyu2fqZeez|RbbIE7@)UtJvgAcVwVZNLccswxm+*L&w`&t=ttT=sv6Aq z!HouSc-24Y9;0q$>jX<1DnnGmAsP))- z^F~o99gHZw`S&Aw7e4id6Lg7kMk-e)B~=tZ!kE7sGTOJ)8@q}np@j7&7Sy{2`D^FH zI7aX%06vKsfJ168QnCM2=l|i>{I{%@gcr>ExM0Dw{PX6ozEuqFYEt z087%MKC;wVsMV}kIiuu9Zz9~H!21d!;Cu#b;hMDIP7nw3xSX~#?5#SSjyyg+Y@xh| z%(~fv3`0j#5CA2D8!M2TrG=8{%>YFr(j)I0DYlcz(2~92?G*?DeuoadkcjmZszH5& zKI@Lis%;RPJ8mNsbrxH@?J8Y2LaVjUIhRUiO-oqjy<&{2X~*f|)YxnUc6OU&5iac= z*^0qwD~L%FKiPmlzi&~a*9sk2$u<7Al=_`Ox^o2*kEv?p`#G(p(&i|ot8}T;8KLk- zPVf_4A9R`5^e`Om2LV*cK59EshYXse&IoByj}4WZaBomoHAPKqxRKbPcD`lMBI)g- zeMRY{gFaUuecSD6q!+b5(?vAnf>c`Z(8@RJy%Ulf?W~xB1dFAjw?CjSn$ph>st5bc zUac1aD_m6{l|$#g_v6;=32(mwpveQDWhmjR7{|B=$oBhz`7_g7qNp)n20|^^op3 zSfTdWV#Q>cb{CMKlWk91^;mHap{mk)o?udk$^Q^^u@&jd zfZ;)saW6{e*yoL6#0}oVPb2!}r{pAUYtn4{P~ES9tTfC5hXZnM{HrC8^=Pof{G4%Bh#8 ze~?C9m*|fd8MK;{L^!+wMy>=f^8b&y?yr6KnTq28$pFMBW9Oy7!oV5z|VM$s-cZ{I|Xf@}-)1=$V&x7e;9v81eiTi4O5-vs?^5pCKy2l>q);!MA zS!}M48l$scB~+Umz}7NbwyTn=rqt@`YtuwiQSMvCMFk2$83k50Q>OK5&fe*xCddIm)3D0I6vBU<+!3=6?(OhkO|b4fE_-j zimOzyfBB_*7*p8AmZi~X2bgVhyPy>KyGLAnOpou~sx9)S9%r)5dE%ADs4v%fFybDa_w*0?+>PsEHTbhKK^G=pFz z@IxLTCROWiKy*)cV3y%0FwrDvf53Ob_XuA1#tHbyn%Ko!1D#sdhBo`;VC*e1YlhrC z?*y3rp86m#qI|qeo8)_xH*G4q@70aXN|SP+6MQ!fJQqo1kwO_v7zqvUfU=Gwx`CR@ zRFb*O8+54%_8tS(ADh}-hUJzE`s*8wLI>1c4b@$al)l}^%GuIXjzBK!EWFO8W`>F^ ze7y#qPS0NI7*aU)g$_ziF(1ft;2<}6Hfz10cR8P}67FD=+}MfhrpOkF3hFhQu;Q1y zu%=jJHTr;0;oC94Hi@LAF5quAQ(rJG(uo%BiRQ@8U;nhX)j0i?0SL2g-A*YeAqF>RVCBOTrn{0R27vu}_S zS>tX4!#&U4W;ikTE!eFH+PKw%p+B(MR2I%n#+m0{#?qRP_tR@zpgCb=4rcrL!F=;A zh%EIF8m6%JG+qb&mEfuFTLHSxUAZEvC-+kvZKyX~SA3Umt`k}}c!5dy?-sLIM{h@> z!2=C)@nx>`;c9DdwZ&zeUc(7t<21D7qBj!|1^Mp1eZ6)PuvHx+poKSDCSBMFF{bKy z;9*&EyKitD99N}%mK8431rvbT+^%|O|HV23{;RhmS{$5tf!bIPoH9RKps`-EtoW5h zo6H_!s)Dl}2gCeGF6>aZtah9iLuGd19^z0*OryPNt{70RvJSM<#Ox9?HxGg04}b^f zrVEPceD%)#0)v5$YDE?f`73bQ6TA6wV;b^x*u2Ofe|S}+q{s5gr&m~4qGd!wOu|cZ||#h_u=k*fB;R6&k?FoM+c&J;ISg70h!J7*xGus)ta4veTdW)S^@sU@ z4$OBS=a~@F*V0ECic;ht4@?Jw<9kpjBgHfr2FDPykCCz|v2)`JxTH55?b3IM={@DU z!^|9nVO-R#s{`VHypWyH0%cs;0GO3E;It6W@0gX6wZ%W|Dzz&O%m17pa19db(er}C zUId1a4#I+Ou8E1MU$g=zo%g7K(=0Pn$)Rk z<4T2u<0rD)*j+tcy2XvY+0 z0d2pqm4)4lDewsAGThQi{2Kc3&C=|OQF!vOd#WB_`4gG3@inh-4>BoL!&#ij8bw7? zqjFRDaQz!J-YGitV4}$*$hg`vv%N)@#UdzHFI2E<&_@0Uw@h_ZHf}7)G;_NUD3@18 zH5;EtugNT0*RXVK*by>WS>jaDDfe!A61Da=VpIK?mcp^W?!1S2oah^wowRnrYjl~`lgP-mv$?yb6{{S55CCu{R z$9;`dyf0Y>uM1=XSl_$01Lc1Iy68IosWN8Q9Op=~I(F<0+_kKfgC*JggjxNgK6 z-3gQm6;sm?J&;bYe&(dx4BEjvq}b`OT^RqF$J4enP1YkeBK#>l1@-K`ajbn05`0J?0daOtnzh@l3^=BkedW1EahZlRp;`j*CaT;-21&f2wU z+Nh-gc4I36Cw+;3UAc<%ySb`#+c@5y ze~en&bYV|kn?Cn|@fqmGxgfz}U!98$=drjAkMi`43I4R%&H0GKEgx-=7PF}y`+j>r zg&JF`jomnu2G{%QV~Gf_-1gx<3Ky=Md9Q3VnK=;;u0lyTBCuf^aUi?+1+`4lLE6ZK zT#(Bf`5rmr(tgTbIt?yA@y`(Ar=f>-aZ}T~>G32EM%XyFvhn&@PWCm#-<&ApLDCXT zD#(9m|V(OOo7PmE@`vD4$S5;+9IQm19dd zvMEU`)E1_F+0o0-z>YCWqg0u8ciIknU#{q02{~YX)gc_u;8;i233D66pf(IkTDxeN zL=4z2)?S$TV9=ORVr&AkZMl<4tTh(v;Ix1{`pPVqI3n2ci&4Dg+W|N8TBUfZ*WeLF zqCH_1Q0W&f9T$lx3CFJ$o@Lz$99 zW!G&@zFHxTaP!o#z^~xgF|(vrHz8R_r9eo;TX9}2ZyjslrtH=%6O)?1?cL&BT(Amp zTGFU1%%#xl&6sH-UIJk_PGk_McFn7=%yd6tAjm|lnmr8bE2le3I~L{0(ffo}TQjyo zHZZI{-}{E4ohYTlZaS$blB!h$Jq^Rf#(ch}@S+Ww&$b);8+>g84IJcLU%B-W?+IY& zslcZIR>+U4v3O9RFEW;8NpCM0w1ROG84=WpKxQ^R`{=0MZCubg3st z48AyJNEvyxn-jCPTlTwp4EKvyEwD3e%kpdY?^BH0!3n6Eb57_L%J1=a*3>|k68A}v zaW`*4YitylfD}ua8V)vb79)N_Ixw_mpp}yJGbNu+5YYOP9K-7nf*jA1#<^rb4#AcS zKg%zCI)7cotx}L&J8Bqo8O1b0q;B1J#B5N5Z$Zq=wX~nQFgUfAE{@u0+EnmK{1hg> zC{vMfFLD;L8b4L+B51&LCm|scVLPe6h02rws@kGv@R+#IqE8>Xn8i|vRq_Z`V;x6F zNeot$1Zsu`lLS92QlLWF54za6vOEKGYQMdX($0JN*cjG7HP&qZ#3+bEN$8O_PfeAb z0R5;=zXac2IZ?fxu59?Nka;1lKm|;0)6|#RxkD05P5qz;*AL@ig!+f=lW5^Jbag%2 z%9@iM0ph$WFlxS!`p31t92z~TB}P-*CS+1Oo_g;7`6k(Jyj8m8U|Q3Sh7o-Icp4kV zK}%qri5>?%IPfamXIZ8pXbm-#{ytiam<{a5A+3dVP^xz!Pvirsq7Btv?*d7eYgx7q zWFxrzb3-%^lDgMc=Vl7^={=VDEKabTG?VWqOngE`Kt7hs236QKidsoeeUQ_^FzsXjprCDd@pW25rNx#6x&L6ZEpoX9Ffzv@olnH3rGOSW( zG-D|cV0Q~qJ>-L}NIyT?T-+x+wU%;+_GY{>t(l9dI%Ximm+Kmwhee;FK$%{dnF;C% zFjM2&$W68Sz#d*wtfX?*WIOXwT;P6NUw}IHdk|)fw*YnGa0rHx#paG!m=Y6GkS4VX zX`T$4eW9k1W!=q8!(#8A9h67fw))k_G)Q9~Q1e3f`aV@kbcSv7!priDUN}gX(iXTy zr$|kU0Vn%*ylmyDCO&G0Z3g>%JeEPFAW!5*H2Ydl>39w3W+gEUjL&vrRs(xGP{(ze zy7EMWF14@Qh>X>st8_029||TP0>7SG9on_xxeR2Iam3G~Em$}aGsNt$iES9zFa<3W zxtOF*!G@=PhfHO!=9pVPXMUVi30WmkPoy$02w}&6A7mF)G6-`~EVq5CwD2`9Zu`kd)52``#V zNSb`9dG~8(dooi1*-aSMf!fun7Sc`-C$-E(3BoSC$2kKrVcI!&yC*+ff2+C-@!AT_ zsvlAIV+%bRDfd{R*TMF><1&_a%@yZ0G0lg2K;F>7b+7A6pv3-S7qWIgx+Z?dt8}|S z>Qbb6x(+^aoV7FQ!Ph8|RUA6vXWQH*1$GJC+wXLXizNIc9p2yLzw9 z0=MdQ!{NnOwIICJc8!+Jp!zG}**r#E!<}&Te&}|B4q;U57$+pQI^}{qj669zMMe_I z&z0uUCqG%YwtUc8HVN7?0GHpu=bL7&{C>hcd5d(iFV{I5c~jpX&!(a{yS*4MEoYXh z*X4|Y@RVfn;piRm-C%b@{0R;aXrjBtvx^HO;6(>i*RnoG0Rtcd25BT6edxTNOgUAOjn zJ2)l{ipj8IP$KID2}*#F=M%^n&=bA0tY98@+2I+7~A&T-tw%W#3GV>GTmkHaqftl)#+E zMU*P(Rjo>8%P@_@#UNq(_L{}j(&-@1iY0TRizhiATJrnvwSH0v>lYfCI2ex^><3$q znzZgpW0JlQx?JB#0^^s-Js1}}wKh6f>(e%NrMwS`Q(FhazkZb|uyB@d%_9)_xb$6T zS*#-Bn)9gmobhAtvBmL+9H-+0_0US?g6^TOvE8f3v=z3o%NcPjOaf{5EMRnn(_z8- z$|m0D$FTU zDy;21v-#0i)9%_bZ7eo6B9@Q@&XprR&oKl4m>zIj-fiRy4Dqy@VVVs?rscG| zmzaDQ%>AQTi<^vYCmv#KOTd@l7#2VIpsj?nm_WfRZzJako`^uU%Nt3e;cU*y*|$7W zLm%fX#i_*HoUXu!NI$ey>BA<5HQB=|nRAwK!$L#n-Qz;~`zACig0PhAq#^5QS<8L2 zS3A+8%vbVMa7LOtTEM?55apt(DcWh#L}R^P2AY*c8B}Cx=6OFAdMPj1f>k3#^#+Hk z6uW1WJW&RlBRh*1DLb7mJ+KO>!t^t8hX1#_Wk`gjDio9)9IGbyCAGI4DJ~orK+YRv znjxRMtshZQHc$#Y-<-JOV6g^Cr@odj&Xw5B(FmI)*qJ9NHmIz_r{t)TxyB`L-%q5l ztzHgD;S6cw?7Atg*6E1!c6*gPRCb%t7D%z<(xm+K{%EJNiI2N0l8ud0Ch@_av_RW? zIr!nO4dL5466WslE6MsfMss7<)-S!e)2@r2o=7_W)OO`~CwklRWzHTfpB)_HYwgz=BzLhgZ9S<{nLBOwOIgJU=94uj6r!m>Xyn9>&xP+=5!zG_*yEoRgM0`aYts z^)&8(>z5C-QQ*o_s(8E4*?AX#S^0)aqB)OTyX>4BMy8h(cHjA8ji1PRlox@jB*1n? zDIfyDjzeg91Ao(;Q;KE@zei$}>EnrF6I}q&Xd=~&$WdDsyH0H7fJX|E+O~%LS*7^Q zYzZ4`pBdY{b7u72gZm6^5~O-57HwzwAz{)NvVaowo`X02tL3PpgLjwA`^i9F^vSpN zAqH3mRjG8VeJNHZ(1{%!XqC+)Z%D}58Qel{_weSEHoygT9pN@i zi=G;!Vj6XQk2tuJC>lza%ywz|`f7TIz*EN2Gdt!s199Dr4Tfd_%~fu8gXo~|ogt5Q zlEy_CXEe^BgsYM^o@L?s33WM14}7^T(kqohOX_iN@U?u;$l|rAvn{rwy>!yfZw13U zB@X9)qt&4;(C6dP?yRsoTMI!j-f1KC!<%~i1}u7yLXYn)(#a;Z6~r>hp~kfP));mi zcG%kdaB9H)z9M=H!f>kM->fTjRVOELNwh1amgKQT=I8J66kI)u_?0@$$~5f`u%;zl zC?pkr^p2Fe=J~WK%4ItSzKA+QHqJ@~m|Cduv=Q&-P8I5rQ-#G@bYH}YJr zUS(~(w|vKyU(T(*py}jTUp%I%{2!W!K(i$uvotcPjVddW z8_5HKY!oBCwGZcs-q`4Yt`Zk~>K?mcxg51wkZlX5e#B08I75F7#dgn5yf&Hrp`*%$ zQ;_Qg>TYRzBe$x=T(@WI9SC!ReSas9vDm(yslQjBJZde5z8GDU``r|N(MHcxNopGr z_}u39W_zwWDL*XYYt>#Xo!9kL#97|EAGyGBcRXtLTd59x%m=3i zL^9joWYA)HfL15l9%H?q`$mY27!<9$7GH(kxb%MV>`}hR4a?+*LH6aR{dzrX@?6X4 z3e`9L;cjqYb`cJmophbm(OX0b)!AFG?5`c#zLagzMW~o)?-!@e80lvk!p#&CD8u5_r&wp4O0zQ>y!k5U$h_K;rWGk=U)zX!#@Q%|9g*A zWx)qS1?fq6X<$mQTB$#3g;;5tHOYuAh;YKSBz%il3Ui6fPRv#v62SsrCdMRTav)Sg zTq1WOu&@v$Ey;@^+_!)cf|w_X<@RC>!=~+A1-65O0bOFYiH-)abINwZvFB;hJjL_$ z(9iScmUdMp2O$WW!520Hd0Q^Yj?DK%YgJD^ez$Z^?@9@Ab-=KgW@n8nC&88)TDC+E zlJM)L3r+ZJfZW_T$;Imq*#2<(j+FIk8ls7)WJ6CjUu#r5PoXxQs4b)mZza<8=v{o)VlLRM<9yw^0En#tXAj`Sylxvki{<1DPe^ zhjHwx^;c8tb?Vr$6ZB;$Ff$+3(*oinbwpN-#F)bTsXq@Sm?43MC#jQ~`F|twI=7oC zH4TJtu#;ngRA|Y~w5N=UfMZi?s0%ZmKUFTAye&6Y*y-%c1oD3yQ%IF2q2385Zl+=> zfz=o`Bedy|U;oxbyb^rB9ixG{Gb-{h$U0hVe`J;{ql!s_OJ_>>eoQn(G6h7+b^P48 zG<=Wg2;xGD-+d@UMZ!c;0>#3nws$9kIDkK13IfloGT@s14AY>&>>^#>`PT7GV$2Hp zN<{bN*ztlZu_%W=&3+=#3bE(mka6VoHEs~0BjZ$+=0`a@R$iaW)6>wp2w)=v2@|2d z%?34!+iOc5S@;AAC4hELWLH56RGxo4jw8MDMU0Wk2k_G}=Vo(>eRFo(g3@HjG|`H3 zm8b*dK=moM*oB<)*A$M9!!5o~4U``e)wxavm@O_R(`P|u%9^LGi(_%IF<6o;NLp*0 zKsfZ0#24GT8(G`i4UvoMh$^;kOhl?`0yNiyrC#HJH=tqOH^T_d<2Z+ zeN>Y9Zn!X4*DMCK^o75Zk2621bdmV7Rx@AX^alBG4%~;G_vUoxhfhFRlR&+3WwF^T zaL)8xPq|wCZoNT^>3J0K?e{J-kl+hu2rZI>CUv#-z&u@`hjeb+bBZ>bcciQVZ{SbW zez04s9oFEgc8Z+Kp{XFX`MVf-s&w9*dx7wLen(_@y34}Qz@&`$2+osqfxz4&d}{Ql z*g1ag00Gu+$C`0avds{Q65BfGsu9`_`dML*rX~hyWIe$T>CsPRoLIr%MTk3pJ^2zH1qub1MBzPG}PO;Wmav9w%F7?%l=xIf#LlP`! z_Nw;xBQY9anH5-c8A4mME}?{iewjz(Sq-29r{fV;Fc>fv%0!W@(+{={Xl-sJ6aMoc z)9Q+$bchoTGTyWU_oI19!)bD=IG&OImfy;VxNXoIO2hYEfO~MkE#IXTK(~?Z&!ae! zl8z{D&2PC$Q*OBC(rS~-*-GHNJ6AC$@eve>LB@Iq;jbBZj`wk4|LGogE||Ie=M5g= z9d`uYQ1^Sr_q2wmZE>w2WG)!F%^KiqyaDtIAct?}D~JP4shTJy5Bg+-(EA8aXaxbd~BKMtTf2iQ69jD1o* zZF9*S3!v-TdqwK$%&?91Sh2=e63;X0Lci@n7y3XOu2ofyL9^-I767eHESAq{m+@*r zbVDx!FQ|AjT;!bYsXv8ilQjy~Chiu&HNhFXt3R_6kMC8~ChEFqG@MWu#1Q1#=~#ix zrkHpJre_?#r=N0wv`-7cHHqU`phJX2M_^{H0~{VP79Dv{6YP)oA1&TSfKPEPZn2)G z9o{U1huZBLL;Tp_0OYw@+9z(jkrwIGdUrOhKJUbwy?WBt zlIK)*K0lQCY0qZ!$%1?3A#-S70F#YyUnmJF*`xx?aH5;gE5pe-15w)EB#nuf6B*c~ z8Z25NtY%6Wlb)bUA$w%HKs5$!Z*W?YKV-lE0@w^{4vw;J>=rn?u!rv$&eM+rpU6rc=j9>N2Op+C{D^mospMCjF2ZGhe4eADA#skp2EA26%p3Ex9wHW8l&Y@HX z$Qv)mHM}4*@M*#*ll5^hE9M^=q~eyWEai*P;4z<9ZYy!SlNE5nlc7gm;M&Q zKhKE4d*%A>^m0R?{N}y|i6i^k>^n4(wzKvlQeHq{l&JuFD~sTsdhs`(?lFK@Q{pU~ zb!M3c@*3IwN1RUOVjY5>uT+s-2QLWY z4T2>fiSn>>Fob+%B868-v9D@AfWr#M8eM6w#eAlhc#zk6jkLxGBGk`E3$!A@*am!R zy>29&ptYK6>cvP`b!syNp)Q$0UOW|-O@)8!?94GOYF_}+zlW%fCEl|Tep_zx05g6q z>tp47e-&R*hSNe{6{H!mL?+j$c^TXT{C&@T-xIaesNCl05 z9SLb@q&mSb)I{VXMaiWa3PWj=Ed!>*GwUe;^|uk=Pz$njNnfFY^MM>E?zqhf6^{}0 zx&~~dA5#}1ig~7HvOQ#;d9JZBeEQ+}-~v$at`m!(ai z$w(H&mWCC~;PQ1$%iuz3`>dWeb3_p}X>L2LK%2l59Tyc}4m0>9A!8rhoU3m>i2+hl zx?*qs*c^j}+WPs>&v1%1Ko8_ivAGIn@QK7A`hDz-Emkcgv2@wTbYhkiwX2l=xz*XG zaiNg+j4F-I>9v+LjosI-QECrtKjp&0T@xIMKVr+&)gyb4@b3y?2CA?=ooN zT#;rU86WLh(e@#mF*rk(NV-qSIZyr z$6!ZUmzD)%yO-ot`rw3rp6?*_l*@Z*IB0xn4|BGPWHNc-1ZUnNSMWmDh=EzWJRP`) zl%d%J613oXzh5;VY^XWJi{lB`f#u+ThvtP7 zq(HK<4>tw(=yzSBWtYO}XI`S1pMBe3!jFxBHIuwJ(@%zdQFi1Q_hU2eDuHqXte7Ki zOV55H2D6u#4oTfr7|u*3p75KF&jaLEDpxk!4*bhPc%mpfj)Us3XIG3 zIKMX^s^1wt8YK7Ky^UOG=w!o5e7W-<&c|fw2{;Q11vm@J{)@N3-p1U>!0~sKWHaL= zWV(0}1IIyt1p%=_-Fe5Kfzc71wg}`RDDntVZv;4!=&XXF-$48jS0Sc;eDy@Sg;+{A zFStc{dXT}kcIjMXb4F7MbX~2%i;UrBxm%qmLKb|2=?uPr00-$MEUIGR5+JG2l2Nq` zkM{{1RO_R)+8oQ6x&-^kCj)W8Z}TJjS*Wm4>hf+4#VJP)OBaDF%3pms7DclusBUw} z{ND#!*I6h85g6DzNvdAmnwWY{&+!KZM4DGzeHI?MR@+~|su0{y-5-nICz_MIT_#FE zm<5f3zlaKq!XyvY3H`9s&T};z!cK}G%;~!rpzk9-6L}4Rg7vXtKFsl}@sT#U#7)x- z7UWue5sa$R>N&b{J61&gvKcKlozH*;OjoDR+elkh|4bJ!_3AZNMOu?n9&|L>OTD78 z^i->ah_Mqc|Ev)KNDzfu1P3grBIM#%`QZqj5W{qu(HocQhjyS;UINoP`{J+DvV?|1 z_sw6Yr3z6%e7JKVDY<$P=M)dbk@~Yw9|2!Cw!io3%j92wTD!c^e9Vj+7VqXo3>u#= zv#M{HHJ=e$X5vQ>>ML?E8#UlmvJgTnb73{PSPTf*0)mcj6C z{KsfUbDK|F$E(k;ER%8HMdDi`=BfpZzP3cl5yJHu;v^o2FkHNk;cXc17tL8T!CsYI zfeZ6sw@;8ia|mY_AXjCS?kUfxdjDB28)~Tz1dGE|{VfBS9`0m2!m1yG?hR})er^pl4c@9Aq+|}ZlDaHL)K$O| z%9Jp-imI-Id0|(d5{v~w6mx)tUKfbuVD`xNt04Mry%M+jXzE>4(TBsx#&=@wT2Vh) z1yeEY&~17>0%P(eHP0HB^|7C+WJxQBTG$uyOWY@iDloRIb-Cf!p<{WQHR!422#F34 zG`v|#CJ^G}y9U*7jgTlD{D&y$Iv{6&PYG>{Ixg$pGk?lWrE#PJ8KunQC@}^6OP!|< zS;}p3to{S|uZz%kKe|;A0bL0XxPB&Q{J(9PyX`+Kr`k~r2}yP^ND{8!v7Q1&vtk& z2Y}l@J@{|2`oA%sxvM9i0V+8IXrZ4;tey)d;LZI70Kbim<4=WoTPZy=Yd|34v#$Kh zx|#YJ8s`J>W&jt#GcMpx84w2Z3ur-rK7gf-p5cE)=w1R2*|0mj12hvapuUWM0b~dG zMg9p8FmAZI@i{q~0@QuY44&mMUNXd7z>U58shA3o`p5eVLpq>+{(<3->DWuSFVZwC zxd50Uz(w~LxC4}bgag#q#NNokK@yNc+Q|Ap!u>Ddy+df>v;j@I12CDNN9do+0^n8p zMQs7X#+FVF0C5muGfN{r0|Nkql%BQT|K(DDNdR2pzM=_ea5+GO|J67`05AV92t@4l z0Qno0078PIHdaQGHZ~Scw!dzgqjK~3B7kf>BcP__&lLyU(cu3B^uLo%{j|Mb0NR)tkeT7Hcwp4O# z)yzu>cvG(d9~0a^)eZ;;%3ksk@F&1eEBje~ zW+-_s)&RgiweQc!otF>4%vbXKaOU41{!hw?|2`Ld3I8$&#WOsq>EG)1ANb!{N4z9@ zsU!bPG-~-bqCeIDzo^Q;gnucB{tRzm{ZH^Orphm2U+REA!*<*J6YQV83@&xoDl%#wnl5qcBqCcAF-vX5{30}(oJrnSH z{RY85hylK2dMOh2%oO1J8%)0?8TOL%rS8)+CsDv}aQ>4D)Jv+DLK)9gI^n-T^$)Tc zFPUD75qJm!Y-KBqj;JP4dV4 z`X{lGmn<)1IGz330}s}Jrjtf{(lnuuNHe5(ezA(pYa=1|Ff-LhPFK8 zyJh_b{yzu0yll6ZkpRzRjezyYivjyjW7QwO;@6X`m;2Apn2EK2!~7S}-*=;5*7K$B z`x(=!^?zgj(-`&ApZJXI09aDLXaT@<;CH=?fBOY5d|b~wBA@@p^K#nxr`)?i?SqTupI_PJ(A3cx`z~9mX_*)>L F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.properties b/wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9a4163a --- /dev/null +++ b/wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/wrappers/android/mynteye/gradlew b/wrappers/android/mynteye/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/wrappers/android/mynteye/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/wrappers/android/mynteye/gradlew.bat b/wrappers/android/mynteye/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/wrappers/android/mynteye/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/wrappers/android/mynteye/settings.gradle b/wrappers/android/mynteye/settings.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/wrappers/android/mynteye/settings.gradle @@ -0,0 +1 @@ +include ':app' From 9aefabf76b30e1e132103b15499f010c9aa2d4b8 Mon Sep 17 00:00:00 2001 From: John Zhao Date: Mon, 14 Jan 2019 12:03:18 +0800 Subject: [PATCH 02/21] feat(android): add libmynteye to android --- wrappers/android/mynteye/app/build.gradle | 5 ++ .../mynteye/gradle/dependencies.gradle | 3 + .../android/mynteye/libmynteye/.gitignore | 1 + .../android/mynteye/libmynteye/build.gradle | 46 +++++++++++++ .../mynteye/libmynteye/proguard-rules.pro | 21 ++++++ .../mynteye/ExampleInstrumentedTest.java | 26 +++++++ .../libmynteye/src/main/AndroidManifest.xml | 2 + .../libmynteye/src/main/cpp/CMakeLists.txt | 67 +++++++++++++++++++ .../slightech/mynteye/ExampleUnitTest.java | 17 +++++ wrappers/android/mynteye/settings.gradle | 2 +- 10 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 wrappers/android/mynteye/libmynteye/.gitignore create mode 100644 wrappers/android/mynteye/libmynteye/build.gradle create mode 100644 wrappers/android/mynteye/libmynteye/proguard-rules.pro create mode 100644 wrappers/android/mynteye/libmynteye/src/androidTest/java/com/slightech/mynteye/ExampleInstrumentedTest.java create mode 100644 wrappers/android/mynteye/libmynteye/src/main/AndroidManifest.xml create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/CMakeLists.txt create mode 100644 wrappers/android/mynteye/libmynteye/src/test/java/com/slightech/mynteye/ExampleUnitTest.java diff --git a/wrappers/android/mynteye/app/build.gradle b/wrappers/android/mynteye/app/build.gradle index cc86012..229eac5 100644 --- a/wrappers/android/mynteye/app/build.gradle +++ b/wrappers/android/mynteye/app/build.gradle @@ -9,6 +9,9 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + ndk { + abiFilters xabis + } } buildTypes { release { @@ -24,6 +27,8 @@ dependencies { implementation xdeps.support.appcompat implementation xdeps.support.constraint + implementation project(':libmynteye') + testImplementation xdeps.junit androidTestImplementation xdeps.support.test.runner androidTestImplementation xdeps.support.test.espresso diff --git a/wrappers/android/mynteye/gradle/dependencies.gradle b/wrappers/android/mynteye/gradle/dependencies.gradle index da2d862..c4d7278 100644 --- a/wrappers/android/mynteye/gradle/dependencies.gradle +++ b/wrappers/android/mynteye/gradle/dependencies.gradle @@ -41,4 +41,7 @@ ext { // JUnit4: https://github.com/junit-team/junit4 'junit': 'junit:junit:4.12', ] + + xabis = ['arm64-v8a', 'armeabi-v7a'] as String[] + //xabis = ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'] as String[] } diff --git a/wrappers/android/mynteye/libmynteye/.gitignore b/wrappers/android/mynteye/libmynteye/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/.gitignore @@ -0,0 +1 @@ +/build diff --git a/wrappers/android/mynteye/libmynteye/build.gradle b/wrappers/android/mynteye/libmynteye/build.gradle new file mode 100644 index 0000000..97f0e29 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion xversions.compileSdk + + defaultConfig { + minSdkVersion xversions.minSdk + targetSdkVersion xversions.targetSdk + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + externalNativeBuild { + // https://developer.android.com/ndk/guides/cmake + cmake { + cppFlags "-std=c++11 -frtti -fexceptions" + } + } + + ndk { + abiFilters xabis + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + testImplementation xdeps.junit + androidTestImplementation xdeps.support.test.runner + androidTestImplementation xdeps.support.test.espresso +} diff --git a/wrappers/android/mynteye/libmynteye/proguard-rules.pro b/wrappers/android/mynteye/libmynteye/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/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 diff --git a/wrappers/android/mynteye/libmynteye/src/androidTest/java/com/slightech/mynteye/ExampleInstrumentedTest.java b/wrappers/android/mynteye/libmynteye/src/androidTest/java/com/slightech/mynteye/ExampleInstrumentedTest.java new file mode 100644 index 0000000..2b84b62 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/androidTest/java/com/slightech/mynteye/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.slightech.mynteye; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.slightech.mynteye.test", appContext.getPackageName()); + } +} diff --git a/wrappers/android/mynteye/libmynteye/src/main/AndroidManifest.xml b/wrappers/android/mynteye/libmynteye/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7db87f0 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/CMakeLists.txt b/wrappers/android/mynteye/libmynteye/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..104bdf3 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/CMakeLists.txt @@ -0,0 +1,67 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +# Sets the minimum version of CMake required to build the native library. + +cmake_minimum_required(VERSION 3.4.1) + +get_filename_component(MYNTETE_ROOT "${PROJECT_SOURCE_DIR}/../../../../../../.." ABSOLUTE) +message(STATUS "MYNTETE_ROOT: ${MYNTETE_ROOT}") + +set(MYNTEYE_NAME "mynteye") + +set(MYNTEYE_NAMESPACE "${MYNTEYE_NAME}") +message(STATUS "Namespace: ${MYNTEYE_NAMESPACE}") + +configure_file( + ${MYNTETE_ROOT}/include/mynteye/mynteye.h.in + include/mynteye/mynteye.h @ONLY +) + +## libs + +find_library(log-lib log) + +## targets + +# libmynteye + +add_definitions(-DMYNTEYE_EXPORTS) + +set(MYNTEYE_SRCS + ${MYNTETE_ROOT}/src/mynteye/uvc/linux/uvc-v4l2.cc + ${MYNTETE_ROOT}/src/mynteye/types.cc + ${MYNTETE_ROOT}/src/mynteye/util/files.cc + ${MYNTETE_ROOT}/src/mynteye/util/strings.cc + ${MYNTETE_ROOT}/src/mynteye/device/channel/bytes.cc + ${MYNTETE_ROOT}/src/mynteye/device/channel/channels.cc + ${MYNTETE_ROOT}/src/mynteye/device/channel/file_channel.cc + ${MYNTETE_ROOT}/src/mynteye/device/config.cc + ${MYNTETE_ROOT}/src/mynteye/device/context.cc + ${MYNTETE_ROOT}/src/mynteye/device/device.cc + ${MYNTETE_ROOT}/src/mynteye/device/motions.cc + ${MYNTETE_ROOT}/src/mynteye/device/standard/channels_adapter_s.cc + ${MYNTETE_ROOT}/src/mynteye/device/standard/device_s.cc + ${MYNTETE_ROOT}/src/mynteye/device/standard/streams_adapter_s.cc + ${MYNTETE_ROOT}/src/mynteye/device/standard2/channels_adapter_s2.cc + ${MYNTETE_ROOT}/src/mynteye/device/standard2/device_s2.cc + ${MYNTETE_ROOT}/src/mynteye/device/standard2/streams_adapter_s2.cc + ${MYNTETE_ROOT}/src/mynteye/device/standard2/channels_adapter_s210a.cc + ${MYNTETE_ROOT}/src/mynteye/device/standard2/device_s210a.cc + ${MYNTETE_ROOT}/src/mynteye/device/standard2/streams_adapter_s210a.cc + ${MYNTETE_ROOT}/src/mynteye/device/streams.cc + ${MYNTETE_ROOT}/src/mynteye/device/types.cc + ${MYNTETE_ROOT}/src/mynteye/device/utils.cc +) + +list(APPEND MYNTEYE_SRCS ${MYNTETE_ROOT}/src/mynteye/miniglog.cc) + +add_library(${MYNTEYE_NAME} SHARED ${MYNTEYE_SRCS}) +target_link_libraries(${MYNTEYE_NAME} ${log-lib}) + +target_include_directories(${MYNTEYE_NAME} PUBLIC + "$" + "$" + "$" + "$" +) diff --git a/wrappers/android/mynteye/libmynteye/src/test/java/com/slightech/mynteye/ExampleUnitTest.java b/wrappers/android/mynteye/libmynteye/src/test/java/com/slightech/mynteye/ExampleUnitTest.java new file mode 100644 index 0000000..6d3ba42 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/test/java/com/slightech/mynteye/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.slightech.mynteye; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/wrappers/android/mynteye/settings.gradle b/wrappers/android/mynteye/settings.gradle index e7b4def..f04decd 100644 --- a/wrappers/android/mynteye/settings.gradle +++ b/wrappers/android/mynteye/settings.gradle @@ -1 +1 @@ -include ':app' +include ':app', ':libmynteye' From a8796478e6c924f6a8d738a649f4c67f76d2ee80 Mon Sep 17 00:00:00 2001 From: John Zhao Date: Tue, 15 Jan 2019 15:10:59 +0800 Subject: [PATCH 03/21] feat(*): update to androidx and add mynteye jni --- wrappers/android/mynteye/app/build.gradle | 21 +++-- .../mynteye/demo/ExampleInstrumentedTest.java | 4 +- .../mynteye/app/src/main/AndroidManifest.xml | 1 + .../slightech/mynteye/demo/MainActivity.java | 9 +- .../slightech/mynteye/demo/MyApplication.java | 32 +++++++ .../app/src/main/res/layout/activity_main.xml | 4 +- wrappers/android/mynteye/build.gradle | 2 +- wrappers/android/mynteye/gradle.properties | 2 + .../mynteye/gradle/dependencies.gradle | 39 +------- .../gradle/wrapper/gradle-wrapper.properties | 3 +- .../{src/main/cpp => }/CMakeLists.txt | 74 +++++++++++---- .../android/mynteye/libmynteye/build.gradle | 17 +++- .../mynteye/ExampleInstrumentedTest.java | 4 +- .../src/main/cpp/mynteye/cpp/device.hpp | 31 ++++++ .../main/cpp/mynteye/cpp/device_usb_info.hpp | 26 +++++ .../src/main/cpp/mynteye/cpp/format.hpp | 28 ++++++ .../main/cpp/mynteye/cpp/stream_request.hpp | 32 +++++++ .../src/main/cpp/mynteye/impl/device_impl.cpp | 77 +++++++++++++++ .../src/main/cpp/mynteye/impl/device_impl.hpp | 31 ++++++ .../main/cpp/mynteye/impl/type_conversion.hpp | 37 ++++++++ .../src/main/cpp/mynteye/jni/NativeDevice.cpp | 79 ++++++++++++++++ .../src/main/cpp/mynteye/jni/NativeDevice.hpp | 32 +++++++ .../cpp/mynteye/jni/NativeDeviceUsbInfo.cpp | 32 +++++++ .../cpp/mynteye/jni/NativeDeviceUsbInfo.hpp | 34 +++++++ .../src/main/cpp/mynteye/jni/NativeFormat.hpp | 26 +++++ .../cpp/mynteye/jni/NativeStreamRequest.cpp | 37 ++++++++ .../cpp/mynteye/jni/NativeStreamRequest.hpp | 36 +++++++ .../java/com/slightech/mynteye/Device.java | 94 +++++++++++++++++++ .../com/slightech/mynteye/DeviceUsbInfo.java | 50 ++++++++++ .../java/com/slightech/mynteye/Format.java | 15 +++ .../com/slightech/mynteye/StreamRequest.java | 67 +++++++++++++ wrappers/android/mynteye/scripts/.gitignore | 1 + .../android/mynteye/scripts/mynteye.djinni | 33 +++++++ .../android/mynteye/scripts/run_djinni.sh | 83 ++++++++++++++++ 34 files changed, 1018 insertions(+), 75 deletions(-) create mode 100644 wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MyApplication.java rename wrappers/android/mynteye/libmynteye/{src/main/cpp => }/CMakeLists.txt (64%) create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/device.hpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/device_usb_info.hpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/format.hpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/stream_request.hpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.hpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/type_conversion.hpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDevice.cpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDevice.hpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDeviceUsbInfo.cpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDeviceUsbInfo.hpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeFormat.hpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeStreamRequest.cpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeStreamRequest.hpp create mode 100644 wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/Device.java create mode 100644 wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/DeviceUsbInfo.java create mode 100644 wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/Format.java create mode 100644 wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/StreamRequest.java create mode 100644 wrappers/android/mynteye/scripts/.gitignore create mode 100644 wrappers/android/mynteye/scripts/mynteye.djinni create mode 100755 wrappers/android/mynteye/scripts/run_djinni.sh diff --git a/wrappers/android/mynteye/app/build.gradle b/wrappers/android/mynteye/app/build.gradle index 229eac5..3e74552 100644 --- a/wrappers/android/mynteye/app/build.gradle +++ b/wrappers/android/mynteye/app/build.gradle @@ -8,7 +8,7 @@ android { targetSdkVersion xversions.targetSdk versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk { abiFilters xabis } @@ -19,17 +19,26 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation xdeps.support.appcompat - implementation xdeps.support.constraint + implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3' + + implementation 'com.jakewharton.timber:timber:4.7.1' + + implementation 'com.jakewharton:butterknife:10.0.0' + annotationProcessor 'com.jakewharton:butterknife-compiler:10.0.0' implementation project(':libmynteye') - testImplementation xdeps.junit - androidTestImplementation xdeps.support.test.runner - androidTestImplementation xdeps.support.test.espresso + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/wrappers/android/mynteye/app/src/androidTest/java/com/slightech/mynteye/demo/ExampleInstrumentedTest.java b/wrappers/android/mynteye/app/src/androidTest/java/com/slightech/mynteye/demo/ExampleInstrumentedTest.java index a7c3f03..e30780b 100644 --- a/wrappers/android/mynteye/app/src/androidTest/java/com/slightech/mynteye/demo/ExampleInstrumentedTest.java +++ b/wrappers/android/mynteye/app/src/androidTest/java/com/slightech/mynteye/demo/ExampleInstrumentedTest.java @@ -1,8 +1,8 @@ package com.slightech.mynteye.demo; import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/wrappers/android/mynteye/app/src/main/AndroidManifest.xml b/wrappers/android/mynteye/app/src/main/AndroidManifest.xml index 76abdcc..0e03278 100644 --- a/wrappers/android/mynteye/app/src/main/AndroidManifest.xml +++ b/wrappers/android/mynteye/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java index e4578d3..ef9edd7 100644 --- a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java +++ b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java @@ -1,7 +1,10 @@ package com.slightech.mynteye.demo; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; +import com.slightech.mynteye.Device; +import com.slightech.mynteye.DeviceUsbInfo; +import timber.log.Timber; public class MainActivity extends AppCompatActivity { @@ -9,5 +12,9 @@ public class MainActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + + for (DeviceUsbInfo info : Device.query()) { + Timber.i(info.toString()); + } } } diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MyApplication.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MyApplication.java new file mode 100644 index 0000000..b72b43d --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MyApplication.java @@ -0,0 +1,32 @@ +package com.slightech.mynteye.demo; + +import android.app.Application; +import timber.log.Timber; + +public class MyApplication extends Application { + + static { + try { + System.loadLibrary("mynteye_jni"); + } catch (UnsatisfiedLinkError e) { + System.err.println("mynteye_jni library failed to load.\n" + e); + } + } + + @Override public void onCreate() { + super.onCreate(); + Timber.plant(new Timber.DebugTree()); + } + + @Override public void onLowMemory() { + super.onLowMemory(); + } + + @Override public void onTrimMemory(int level) { + super.onTrimMemory(level); + } + + @Override public void onTerminate() { + super.onTerminate(); + } +} diff --git a/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml b/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml index 3b779f6..8a0f7e4 100644 --- a/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml +++ b/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/wrappers/android/mynteye/build.gradle b/wrappers/android/mynteye/build.gradle index e7a3ada..76a5905 100644 --- a/wrappers/android/mynteye/build.gradle +++ b/wrappers/android/mynteye/build.gradle @@ -8,7 +8,7 @@ buildscript { jcenter() } dependencies { - classpath xplugins.android.buildGradle + classpath 'com.android.tools.build:gradle:3.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/wrappers/android/mynteye/gradle.properties b/wrappers/android/mynteye/gradle.properties index 82618ce..d546dea 100644 --- a/wrappers/android/mynteye/gradle.properties +++ b/wrappers/android/mynteye/gradle.properties @@ -6,6 +6,8 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. +android.enableJetifier=true +android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/wrappers/android/mynteye/gradle/dependencies.gradle b/wrappers/android/mynteye/gradle/dependencies.gradle index c4d7278..9811a55 100644 --- a/wrappers/android/mynteye/gradle/dependencies.gradle +++ b/wrappers/android/mynteye/gradle/dependencies.gradle @@ -1,45 +1,8 @@ ext { - xplugins = [ - 'android': [ - // Gradle: https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google - buildGradle: 'com.android.tools.build:gradle:3.2.1', - ], - ] - xversions = [ 'compileSdk': 28, - 'minSdk': 21, + 'minSdk': 24, 'targetSdk': 28, - - 'androidSupport': '28.0.0', - ] - - xdeps = [ - // Support Library - // https://developer.android.com/topic/libraries/support-library/ - 'support': [ - // Android AppCompat Library V7 - // https://mvnrepository.com/artifact/com.android.support/appcompat-v7?repo=google - 'appcompat': "com.android.support:appcompat-v7:${xversions.androidSupport}", - - // Android ConstraintLayout Solver - // https://mvnrepository.com/artifact/com.android.support.constraint/constraint-layout-solver - 'constraint': 'com.android.support.constraint:constraint-layout:1.1.3', - // Android Multi Dex Library - // https://mvnrepository.com/artifact/com.android.support/multidex - 'multidex': 'com.android.support:multidex:1.0.3', - - // Test apps on Android - // https://developer.android.com/training/testing/ - 'test': [ - 'espresso': 'com.android.support.test.espresso:espresso-core:3.0.2', - 'rules': 'com.android.support.test:rules:1.0.2', - 'runner': 'com.android.support.test:runner:1.0.2', - ], - ], - - // JUnit4: https://github.com/junit-team/junit4 - 'junit': 'junit:junit:4.12', ] xabis = ['arm64-v8a', 'armeabi-v7a'] as String[] diff --git a/wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.properties b/wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.properties index 9a4163a..0c93efd 100644 --- a/wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.properties +++ b/wrappers/android/mynteye/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Tue Jan 15 14:54:17 CST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/CMakeLists.txt b/wrappers/android/mynteye/libmynteye/CMakeLists.txt similarity index 64% rename from wrappers/android/mynteye/libmynteye/src/main/cpp/CMakeLists.txt rename to wrappers/android/mynteye/libmynteye/CMakeLists.txt index 104bdf3..31afdd5 100644 --- a/wrappers/android/mynteye/libmynteye/src/main/cpp/CMakeLists.txt +++ b/wrappers/android/mynteye/libmynteye/CMakeLists.txt @@ -5,29 +5,46 @@ cmake_minimum_required(VERSION 3.4.1) -get_filename_component(MYNTETE_ROOT "${PROJECT_SOURCE_DIR}/../../../../../../.." ABSOLUTE) +get_filename_component(MYNTETE_ROOT "${PROJECT_SOURCE_DIR}/../../../.." ABSOLUTE) message(STATUS "MYNTETE_ROOT: ${MYNTETE_ROOT}") -set(MYNTEYE_NAME "mynteye") +if(NOT DJINNI_DIR) + if(DEFINED ENV{DJINNI_DIR}) + set(DJINNI_DIR $ENV{DJINNI_DIR}) + else() + set(DJINNI_DIR "$ENV{HOME}/Workspace/Fever/Dropbox/djinni") + endif() +endif() -set(MYNTEYE_NAMESPACE "${MYNTEYE_NAME}") -message(STATUS "Namespace: ${MYNTEYE_NAMESPACE}") +# libs + +## log + +find_library(log-lib log) + +## djinni_jni + +include_directories( + ${DJINNI_DIR}/support-lib/jni +) +add_library(djinni_jni STATIC + ${DJINNI_DIR}/support-lib/jni/djinni_support.cpp +) + +# targets + +## libmynteye + +add_definitions(-DMYNTEYE_EXPORTS) + +set(MYNTEYE_NAMESPACE "mynteye") +#message(STATUS "Namespace: ${MYNTEYE_NAMESPACE}") configure_file( ${MYNTETE_ROOT}/include/mynteye/mynteye.h.in include/mynteye/mynteye.h @ONLY ) -## libs - -find_library(log-lib log) - -## targets - -# libmynteye - -add_definitions(-DMYNTEYE_EXPORTS) - set(MYNTEYE_SRCS ${MYNTETE_ROOT}/src/mynteye/uvc/linux/uvc-v4l2.cc ${MYNTETE_ROOT}/src/mynteye/types.cc @@ -56,12 +73,35 @@ set(MYNTEYE_SRCS list(APPEND MYNTEYE_SRCS ${MYNTETE_ROOT}/src/mynteye/miniglog.cc) -add_library(${MYNTEYE_NAME} SHARED ${MYNTEYE_SRCS}) -target_link_libraries(${MYNTEYE_NAME} ${log-lib}) +add_library(mynteye STATIC ${MYNTEYE_SRCS}) +target_link_libraries(mynteye ${log-lib}) -target_include_directories(${MYNTEYE_NAME} PUBLIC +target_include_directories(mynteye PUBLIC "$" "$" "$" "$" ) + +## libmynteye_jni + +set(CPP_DIR "${PROJECT_SOURCE_DIR}/src/main/cpp") + +include_directories( + ${CPP_DIR}/mynteye/cpp + ${CPP_DIR}/mynteye/impl + ${CPP_DIR}/mynteye/jni +) + +set(MYNTEYE_JNI_SRCS "") +foreach(__dir cpp impl jni) + file(GLOB __srcs "${CPP_DIR}/mynteye/${__dir}/*.cpp") + list(APPEND MYNTEYE_JNI_SRCS ${__srcs}) +endforeach() +#message(STATUS "MYNTEYE_JNI_SRCS: ${MYNTEYE_JNI_SRCS}") + +add_library(mynteye_jni SHARED + ${DJINNI_DIR}/support-lib/jni/djinni_main.cpp + ${MYNTEYE_JNI_SRCS} +) +target_link_libraries(mynteye_jni ${log-lib} djinni_jni mynteye) diff --git a/wrappers/android/mynteye/libmynteye/build.gradle b/wrappers/android/mynteye/libmynteye/build.gradle index 97f0e29..b5c3507 100644 --- a/wrappers/android/mynteye/libmynteye/build.gradle +++ b/wrappers/android/mynteye/libmynteye/build.gradle @@ -9,7 +9,7 @@ android { versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { // https://developer.android.com/ndk/guides/cmake @@ -32,15 +32,22 @@ android { externalNativeBuild { cmake { - path "src/main/cpp/CMakeLists.txt" + path "CMakeLists.txt" } } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation xdeps.junit - androidTestImplementation xdeps.support.test.runner - androidTestImplementation xdeps.support.test.espresso + implementation 'androidx.annotation:annotation:1.0.1' + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/wrappers/android/mynteye/libmynteye/src/androidTest/java/com/slightech/mynteye/ExampleInstrumentedTest.java b/wrappers/android/mynteye/libmynteye/src/androidTest/java/com/slightech/mynteye/ExampleInstrumentedTest.java index 2b84b62..8348283 100644 --- a/wrappers/android/mynteye/libmynteye/src/androidTest/java/com/slightech/mynteye/ExampleInstrumentedTest.java +++ b/wrappers/android/mynteye/libmynteye/src/androidTest/java/com/slightech/mynteye/ExampleInstrumentedTest.java @@ -1,8 +1,8 @@ package com.slightech.mynteye; import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/device.hpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/device.hpp new file mode 100644 index 0000000..6e0214f --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/device.hpp @@ -0,0 +1,31 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#pragma once + +#include +#include + +namespace mynteye_jni { + +struct DeviceUsbInfo; +struct StreamRequest; + +class Device { +public: + virtual ~Device() {} + + static std::vector Query(); + + static std::shared_ptr Create(const DeviceUsbInfo & info); + + virtual std::vector GetStreamRequests() = 0; + + virtual void ConfigStreamRequest(const StreamRequest & request) = 0; + + virtual void Start() = 0; + + virtual void Stop() = 0; +}; + +} // namespace mynteye_jni diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/device_usb_info.hpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/device_usb_info.hpp new file mode 100644 index 0000000..5f93d25 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/device_usb_info.hpp @@ -0,0 +1,26 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#pragma once + +#include +#include +#include + +namespace mynteye_jni { + +struct DeviceUsbInfo final { + int32_t index; + std::string name; + std::string sn; + + DeviceUsbInfo(int32_t index_, + std::string name_, + std::string sn_) + : index(std::move(index_)) + , name(std::move(name_)) + , sn(std::move(sn_)) + {} +}; + +} // namespace mynteye_jni diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/format.hpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/format.hpp new file mode 100644 index 0000000..55febf5 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/format.hpp @@ -0,0 +1,28 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#pragma once + +#include + +namespace mynteye_jni { + +enum class Format : int { + GREY, + YUYV, + BGR888, + RGB888, +}; + +} // namespace mynteye_jni + +namespace std { + +template <> +struct hash<::mynteye_jni::Format> { + size_t operator()(::mynteye_jni::Format type) const { + return std::hash()(static_cast(type)); + } +}; + +} // namespace std diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/stream_request.hpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/stream_request.hpp new file mode 100644 index 0000000..0238c74 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/cpp/stream_request.hpp @@ -0,0 +1,32 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#pragma once + +#include "format.hpp" +#include +#include + +namespace mynteye_jni { + +struct StreamRequest final { + int32_t index; + int32_t width; + int32_t height; + Format format; + int32_t fps; + + StreamRequest(int32_t index_, + int32_t width_, + int32_t height_, + Format format_, + int32_t fps_) + : index(std::move(index_)) + , width(std::move(width_)) + , height(std::move(height_)) + , format(std::move(format_)) + , fps(std::move(fps_)) + {} +}; + +} // namespace mynteye_jni diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp new file mode 100644 index 0000000..f9cb061 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp @@ -0,0 +1,77 @@ +#include "device_impl.hpp" + +#include "mynteye/device/context.h" +#include "mynteye/device/device.h" + +#include "device_usb_info.hpp" +#include "stream_request.hpp" +#include "type_conversion.hpp" + +MYNTEYE_USE_NAMESPACE + +namespace mynteye_jni { + +std::vector Device::Query() { + std::vector infos; + + Context context; + int32_t i = 0; + for (auto&& d : context.devices()) { + infos.emplace_back(i, + d->GetInfo(Info::DEVICE_NAME), + d->GetInfo(Info::SERIAL_NUMBER)); + ++i; + } + + return infos; +} + +std::shared_ptr Device::Create(const DeviceUsbInfo & info) { + Context context; + int32_t i = 0; + for (auto&& d : context.devices()) { + if (i == info.index) { + return std::make_shared(d); + } + ++i; + } + return nullptr; +} + +DeviceImpl::DeviceImpl(const device_t & device) : Device(), device_(device) { +} + +DeviceImpl::~DeviceImpl() { +} + +std::vector DeviceImpl::GetStreamRequests() { + std::vector requests; + + int32_t i = 0; + for (auto&& req : device_->GetStreamRequests()) { + requests.emplace_back(i, + req.width, req.height, to_jni(req.format), req.fps); + ++i; + } + + return requests; +} + +void DeviceImpl::ConfigStreamRequest(const StreamRequest & request) { + int32_t i = 0; + for (auto&& req : device_->GetStreamRequests()) { + if (i == request.index) { + device_->ConfigStreamRequest(req); + return; + } + ++i; + } +} + +void DeviceImpl::Start() { +} + +void DeviceImpl::Stop() { +} + +} // namespace mynteye_jni diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.hpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.hpp new file mode 100644 index 0000000..9462704 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "mynteye/device/device.h" + +#include "device.hpp" + +namespace mynteye_jni { + +class DeviceImpl : public Device { + public: + using device_t = std::shared_ptr; + + explicit DeviceImpl(const device_t & device); + ~DeviceImpl(); + + std::vector GetStreamRequests() override; + + void ConfigStreamRequest(const StreamRequest & request) override; + + void Start() override; + + void Stop() override; + + private: + device_t device_; +}; + +} // namespace mynteye_jni diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/type_conversion.hpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/type_conversion.hpp new file mode 100644 index 0000000..b705f26 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/type_conversion.hpp @@ -0,0 +1,37 @@ +#include + +#include "mynteye/logger.h" +#include "mynteye/types.h" + +#include "format.hpp" + +namespace mynteye_jni { + +using RawFormat = MYNTEYE_NAMESPACE::Format; +using JniFormat = mynteye_jni::Format; + +inline +RawFormat from_jni(const JniFormat& format) { + switch (format) { + case JniFormat::GREY: return RawFormat::GREY; + case JniFormat::YUYV: return RawFormat::YUYV; + case JniFormat::BGR888: return RawFormat::BGR888; + case JniFormat::RGB888: return RawFormat::RGB888; + default: + LOG(FATAL) << "Format is unknown"; + } +} + +inline +JniFormat to_jni(const RawFormat& format) { + switch (format) { + case RawFormat::GREY: return JniFormat::GREY; + case RawFormat::YUYV: return JniFormat::YUYV; + case RawFormat::BGR888: return JniFormat::BGR888; + case RawFormat::RGB888: return JniFormat::RGB888; + default: + LOG(FATAL) << "Format is unknown"; + } +} + +} // namespace mynteye_jni diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDevice.cpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDevice.cpp new file mode 100644 index 0000000..a551ac0 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDevice.cpp @@ -0,0 +1,79 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#include "NativeDevice.hpp" // my header +#include "Marshal.hpp" +#include "NativeDeviceUsbInfo.hpp" +#include "NativeStreamRequest.hpp" + +namespace djinni_generated { + +NativeDevice::NativeDevice() : ::djinni::JniInterface<::mynteye_jni::Device, NativeDevice>("com/slightech/mynteye/Device$CppProxy") {} + +NativeDevice::~NativeDevice() = default; + + +CJNIEXPORT void JNICALL Java_com_slightech_mynteye_Device_00024CppProxy_nativeDestroy(JNIEnv* jniEnv, jobject /*this*/, jlong nativeRef) +{ + try { + DJINNI_FUNCTION_PROLOGUE1(jniEnv, nativeRef); + delete reinterpret_cast<::djinni::CppProxyHandle<::mynteye_jni::Device>*>(nativeRef); + } JNI_TRANSLATE_EXCEPTIONS_RETURN(jniEnv, ) +} + +CJNIEXPORT jobject JNICALL Java_com_slightech_mynteye_Device_00024CppProxy_query(JNIEnv* jniEnv, jobject /*this*/) +{ + try { + DJINNI_FUNCTION_PROLOGUE0(jniEnv); + auto r = ::mynteye_jni::Device::Query(); + return ::djinni::release(::djinni::List<::djinni_generated::NativeDeviceUsbInfo>::fromCpp(jniEnv, r)); + } JNI_TRANSLATE_EXCEPTIONS_RETURN(jniEnv, 0 /* value doesn't matter */) +} + +CJNIEXPORT jobject JNICALL Java_com_slightech_mynteye_Device_00024CppProxy_create(JNIEnv* jniEnv, jobject /*this*/, jobject j_info) +{ + try { + DJINNI_FUNCTION_PROLOGUE0(jniEnv); + auto r = ::mynteye_jni::Device::Create(::djinni_generated::NativeDeviceUsbInfo::toCpp(jniEnv, j_info)); + return ::djinni::release(::djinni_generated::NativeDevice::fromCpp(jniEnv, r)); + } JNI_TRANSLATE_EXCEPTIONS_RETURN(jniEnv, 0 /* value doesn't matter */) +} + +CJNIEXPORT jobject JNICALL Java_com_slightech_mynteye_Device_00024CppProxy_native_1getStreamRequests(JNIEnv* jniEnv, jobject /*this*/, jlong nativeRef) +{ + try { + DJINNI_FUNCTION_PROLOGUE1(jniEnv, nativeRef); + const auto& ref = ::djinni::objectFromHandleAddress<::mynteye_jni::Device>(nativeRef); + auto r = ref->GetStreamRequests(); + return ::djinni::release(::djinni::List<::djinni_generated::NativeStreamRequest>::fromCpp(jniEnv, r)); + } JNI_TRANSLATE_EXCEPTIONS_RETURN(jniEnv, 0 /* value doesn't matter */) +} + +CJNIEXPORT void JNICALL Java_com_slightech_mynteye_Device_00024CppProxy_native_1configStreamRequest(JNIEnv* jniEnv, jobject /*this*/, jlong nativeRef, jobject j_request) +{ + try { + DJINNI_FUNCTION_PROLOGUE1(jniEnv, nativeRef); + const auto& ref = ::djinni::objectFromHandleAddress<::mynteye_jni::Device>(nativeRef); + ref->ConfigStreamRequest(::djinni_generated::NativeStreamRequest::toCpp(jniEnv, j_request)); + } JNI_TRANSLATE_EXCEPTIONS_RETURN(jniEnv, ) +} + +CJNIEXPORT void JNICALL Java_com_slightech_mynteye_Device_00024CppProxy_native_1start(JNIEnv* jniEnv, jobject /*this*/, jlong nativeRef) +{ + try { + DJINNI_FUNCTION_PROLOGUE1(jniEnv, nativeRef); + const auto& ref = ::djinni::objectFromHandleAddress<::mynteye_jni::Device>(nativeRef); + ref->Start(); + } JNI_TRANSLATE_EXCEPTIONS_RETURN(jniEnv, ) +} + +CJNIEXPORT void JNICALL Java_com_slightech_mynteye_Device_00024CppProxy_native_1stop(JNIEnv* jniEnv, jobject /*this*/, jlong nativeRef) +{ + try { + DJINNI_FUNCTION_PROLOGUE1(jniEnv, nativeRef); + const auto& ref = ::djinni::objectFromHandleAddress<::mynteye_jni::Device>(nativeRef); + ref->Stop(); + } JNI_TRANSLATE_EXCEPTIONS_RETURN(jniEnv, ) +} + +} // namespace djinni_generated diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDevice.hpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDevice.hpp new file mode 100644 index 0000000..98629b8 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDevice.hpp @@ -0,0 +1,32 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#pragma once + +#include "device.hpp" +#include "djinni_support.hpp" + +namespace djinni_generated { + +class NativeDevice final : ::djinni::JniInterface<::mynteye_jni::Device, NativeDevice> { +public: + using CppType = std::shared_ptr<::mynteye_jni::Device>; + using CppOptType = std::shared_ptr<::mynteye_jni::Device>; + using JniType = jobject; + + using Boxed = NativeDevice; + + ~NativeDevice(); + + static CppType toCpp(JNIEnv* jniEnv, JniType j) { return ::djinni::JniClass::get()._fromJava(jniEnv, j); } + static ::djinni::LocalRef fromCppOpt(JNIEnv* jniEnv, const CppOptType& c) { return {jniEnv, ::djinni::JniClass::get()._toJava(jniEnv, c)}; } + static ::djinni::LocalRef fromCpp(JNIEnv* jniEnv, const CppType& c) { return fromCppOpt(jniEnv, c); } + +private: + NativeDevice(); + friend ::djinni::JniClass; + friend ::djinni::JniInterface<::mynteye_jni::Device, NativeDevice>; + +}; + +} // namespace djinni_generated diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDeviceUsbInfo.cpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDeviceUsbInfo.cpp new file mode 100644 index 0000000..755af55 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDeviceUsbInfo.cpp @@ -0,0 +1,32 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#include "NativeDeviceUsbInfo.hpp" // my header +#include "Marshal.hpp" + +namespace djinni_generated { + +NativeDeviceUsbInfo::NativeDeviceUsbInfo() = default; + +NativeDeviceUsbInfo::~NativeDeviceUsbInfo() = default; + +auto NativeDeviceUsbInfo::fromCpp(JNIEnv* jniEnv, const CppType& c) -> ::djinni::LocalRef { + const auto& data = ::djinni::JniClass::get(); + auto r = ::djinni::LocalRef{jniEnv->NewObject(data.clazz.get(), data.jconstructor, + ::djinni::get(::djinni::I32::fromCpp(jniEnv, c.index)), + ::djinni::get(::djinni::String::fromCpp(jniEnv, c.name)), + ::djinni::get(::djinni::String::fromCpp(jniEnv, c.sn)))}; + ::djinni::jniExceptionCheck(jniEnv); + return r; +} + +auto NativeDeviceUsbInfo::toCpp(JNIEnv* jniEnv, JniType j) -> CppType { + ::djinni::JniLocalScope jscope(jniEnv, 4); + assert(j != nullptr); + const auto& data = ::djinni::JniClass::get(); + return {::djinni::I32::toCpp(jniEnv, jniEnv->GetIntField(j, data.field_mIndex)), + ::djinni::String::toCpp(jniEnv, (jstring)jniEnv->GetObjectField(j, data.field_mName)), + ::djinni::String::toCpp(jniEnv, (jstring)jniEnv->GetObjectField(j, data.field_mSn))}; +} + +} // namespace djinni_generated diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDeviceUsbInfo.hpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDeviceUsbInfo.hpp new file mode 100644 index 0000000..0d8b5f3 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeDeviceUsbInfo.hpp @@ -0,0 +1,34 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#pragma once + +#include "device_usb_info.hpp" +#include "djinni_support.hpp" + +namespace djinni_generated { + +class NativeDeviceUsbInfo final { +public: + using CppType = ::mynteye_jni::DeviceUsbInfo; + using JniType = jobject; + + using Boxed = NativeDeviceUsbInfo; + + ~NativeDeviceUsbInfo(); + + static CppType toCpp(JNIEnv* jniEnv, JniType j); + static ::djinni::LocalRef fromCpp(JNIEnv* jniEnv, const CppType& c); + +private: + NativeDeviceUsbInfo(); + friend ::djinni::JniClass; + + const ::djinni::GlobalRef clazz { ::djinni::jniFindClass("com/slightech/mynteye/DeviceUsbInfo") }; + const jmethodID jconstructor { ::djinni::jniGetMethodID(clazz.get(), "", "(ILjava/lang/String;Ljava/lang/String;)V") }; + const jfieldID field_mIndex { ::djinni::jniGetFieldID(clazz.get(), "mIndex", "I") }; + const jfieldID field_mName { ::djinni::jniGetFieldID(clazz.get(), "mName", "Ljava/lang/String;") }; + const jfieldID field_mSn { ::djinni::jniGetFieldID(clazz.get(), "mSn", "Ljava/lang/String;") }; +}; + +} // namespace djinni_generated diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeFormat.hpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeFormat.hpp new file mode 100644 index 0000000..36cf938 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeFormat.hpp @@ -0,0 +1,26 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#pragma once + +#include "djinni_support.hpp" +#include "format.hpp" + +namespace djinni_generated { + +class NativeFormat final : ::djinni::JniEnum { +public: + using CppType = ::mynteye_jni::Format; + using JniType = jobject; + + using Boxed = NativeFormat; + + static CppType toCpp(JNIEnv* jniEnv, JniType j) { return static_cast(::djinni::JniClass::get().ordinal(jniEnv, j)); } + static ::djinni::LocalRef fromCpp(JNIEnv* jniEnv, CppType c) { return ::djinni::JniClass::get().create(jniEnv, static_cast(c)); } + +private: + NativeFormat() : JniEnum("com/slightech/mynteye/Format") {} + friend ::djinni::JniClass; +}; + +} // namespace djinni_generated diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeStreamRequest.cpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeStreamRequest.cpp new file mode 100644 index 0000000..b20eb93 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeStreamRequest.cpp @@ -0,0 +1,37 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#include "NativeStreamRequest.hpp" // my header +#include "Marshal.hpp" +#include "NativeFormat.hpp" + +namespace djinni_generated { + +NativeStreamRequest::NativeStreamRequest() = default; + +NativeStreamRequest::~NativeStreamRequest() = default; + +auto NativeStreamRequest::fromCpp(JNIEnv* jniEnv, const CppType& c) -> ::djinni::LocalRef { + const auto& data = ::djinni::JniClass::get(); + auto r = ::djinni::LocalRef{jniEnv->NewObject(data.clazz.get(), data.jconstructor, + ::djinni::get(::djinni::I32::fromCpp(jniEnv, c.index)), + ::djinni::get(::djinni::I32::fromCpp(jniEnv, c.width)), + ::djinni::get(::djinni::I32::fromCpp(jniEnv, c.height)), + ::djinni::get(::djinni_generated::NativeFormat::fromCpp(jniEnv, c.format)), + ::djinni::get(::djinni::I32::fromCpp(jniEnv, c.fps)))}; + ::djinni::jniExceptionCheck(jniEnv); + return r; +} + +auto NativeStreamRequest::toCpp(JNIEnv* jniEnv, JniType j) -> CppType { + ::djinni::JniLocalScope jscope(jniEnv, 6); + assert(j != nullptr); + const auto& data = ::djinni::JniClass::get(); + return {::djinni::I32::toCpp(jniEnv, jniEnv->GetIntField(j, data.field_mIndex)), + ::djinni::I32::toCpp(jniEnv, jniEnv->GetIntField(j, data.field_mWidth)), + ::djinni::I32::toCpp(jniEnv, jniEnv->GetIntField(j, data.field_mHeight)), + ::djinni_generated::NativeFormat::toCpp(jniEnv, jniEnv->GetObjectField(j, data.field_mFormat)), + ::djinni::I32::toCpp(jniEnv, jniEnv->GetIntField(j, data.field_mFps))}; +} + +} // namespace djinni_generated diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeStreamRequest.hpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeStreamRequest.hpp new file mode 100644 index 0000000..bdc80b1 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/jni/NativeStreamRequest.hpp @@ -0,0 +1,36 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +#pragma once + +#include "djinni_support.hpp" +#include "stream_request.hpp" + +namespace djinni_generated { + +class NativeStreamRequest final { +public: + using CppType = ::mynteye_jni::StreamRequest; + using JniType = jobject; + + using Boxed = NativeStreamRequest; + + ~NativeStreamRequest(); + + static CppType toCpp(JNIEnv* jniEnv, JniType j); + static ::djinni::LocalRef fromCpp(JNIEnv* jniEnv, const CppType& c); + +private: + NativeStreamRequest(); + friend ::djinni::JniClass; + + const ::djinni::GlobalRef clazz { ::djinni::jniFindClass("com/slightech/mynteye/StreamRequest") }; + const jmethodID jconstructor { ::djinni::jniGetMethodID(clazz.get(), "", "(IIILcom/slightech/mynteye/Format;I)V") }; + const jfieldID field_mIndex { ::djinni::jniGetFieldID(clazz.get(), "mIndex", "I") }; + const jfieldID field_mWidth { ::djinni::jniGetFieldID(clazz.get(), "mWidth", "I") }; + const jfieldID field_mHeight { ::djinni::jniGetFieldID(clazz.get(), "mHeight", "I") }; + const jfieldID field_mFormat { ::djinni::jniGetFieldID(clazz.get(), "mFormat", "Lcom/slightech/mynteye/Format;") }; + const jfieldID field_mFps { ::djinni::jniGetFieldID(clazz.get(), "mFps", "I") }; +}; + +} // namespace djinni_generated diff --git a/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/Device.java b/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/Device.java new file mode 100644 index 0000000..725cd1b --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/Device.java @@ -0,0 +1,94 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +package com.slightech.mynteye; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +public interface Device { + @NonNull + public ArrayList getStreamRequests(); + + public void configStreamRequest(@NonNull StreamRequest request); + + public void start(); + + public void stop(); + + @NonNull + public static ArrayList query() + { + return CppProxy.query(); + } + + @Nullable + public static Device create(@NonNull DeviceUsbInfo info) + { + return CppProxy.create(info); + } + + static final class CppProxy implements Device + { + private final long nativeRef; + private final AtomicBoolean destroyed = new AtomicBoolean(false); + + private CppProxy(long nativeRef) + { + if (nativeRef == 0) throw new RuntimeException("nativeRef is zero"); + this.nativeRef = nativeRef; + } + + private native void nativeDestroy(long nativeRef); + public void _djinni_private_destroy() + { + boolean destroyed = this.destroyed.getAndSet(true); + if (!destroyed) nativeDestroy(this.nativeRef); + } + protected void finalize() throws java.lang.Throwable + { + _djinni_private_destroy(); + super.finalize(); + } + + @Override + public ArrayList getStreamRequests() + { + assert !this.destroyed.get() : "trying to use a destroyed object"; + return native_getStreamRequests(this.nativeRef); + } + private native ArrayList native_getStreamRequests(long _nativeRef); + + @Override + public void configStreamRequest(StreamRequest request) + { + assert !this.destroyed.get() : "trying to use a destroyed object"; + native_configStreamRequest(this.nativeRef, request); + } + private native void native_configStreamRequest(long _nativeRef, StreamRequest request); + + @Override + public void start() + { + assert !this.destroyed.get() : "trying to use a destroyed object"; + native_start(this.nativeRef); + } + private native void native_start(long _nativeRef); + + @Override + public void stop() + { + assert !this.destroyed.get() : "trying to use a destroyed object"; + native_stop(this.nativeRef); + } + private native void native_stop(long _nativeRef); + + @NonNull + public static native ArrayList query(); + + @Nullable + public static native Device create(@NonNull DeviceUsbInfo info); + } +} diff --git a/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/DeviceUsbInfo.java b/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/DeviceUsbInfo.java new file mode 100644 index 0000000..d190c89 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/DeviceUsbInfo.java @@ -0,0 +1,50 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +package com.slightech.mynteye; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public final class DeviceUsbInfo { + + + /*package*/ final int mIndex; + + /*package*/ final String mName; + + /*package*/ final String mSn; + + public DeviceUsbInfo( + int index, + @NonNull String name, + @NonNull String sn) { + this.mIndex = index; + this.mName = name; + this.mSn = sn; + } + + public int getIndex() { + return mIndex; + } + + @NonNull + public String getName() { + return mName; + } + + @NonNull + public String getSn() { + return mSn; + } + + @Override + public String toString() { + return "DeviceUsbInfo{" + + "mIndex=" + mIndex + + "," + "mName=" + mName + + "," + "mSn=" + mSn + + "}"; + } + +} diff --git a/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/Format.java b/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/Format.java new file mode 100644 index 0000000..860daf6 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/Format.java @@ -0,0 +1,15 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +package com.slightech.mynteye; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public enum Format { + GREY, + YUYV, + BGR888, + RGB888, + ; +} diff --git a/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/StreamRequest.java b/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/StreamRequest.java new file mode 100644 index 0000000..a653e54 --- /dev/null +++ b/wrappers/android/mynteye/libmynteye/src/main/java/com/slightech/mynteye/StreamRequest.java @@ -0,0 +1,67 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file generated by Djinni from mynteye.djinni + +package com.slightech.mynteye; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public final class StreamRequest { + + + /*package*/ final int mIndex; + + /*package*/ final int mWidth; + + /*package*/ final int mHeight; + + /*package*/ final Format mFormat; + + /*package*/ final int mFps; + + public StreamRequest( + int index, + int width, + int height, + @NonNull Format format, + int fps) { + this.mIndex = index; + this.mWidth = width; + this.mHeight = height; + this.mFormat = format; + this.mFps = fps; + } + + public int getIndex() { + return mIndex; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + @NonNull + public Format getFormat() { + return mFormat; + } + + public int getFps() { + return mFps; + } + + @Override + public String toString() { + return "StreamRequest{" + + "mIndex=" + mIndex + + "," + "mWidth=" + mWidth + + "," + "mHeight=" + mHeight + + "," + "mFormat=" + mFormat + + "," + "mFps=" + mFps + + "}"; + } + +} diff --git a/wrappers/android/mynteye/scripts/.gitignore b/wrappers/android/mynteye/scripts/.gitignore new file mode 100644 index 0000000..0c9146c --- /dev/null +++ b/wrappers/android/mynteye/scripts/.gitignore @@ -0,0 +1 @@ +/djinni-output-temp/ diff --git a/wrappers/android/mynteye/scripts/mynteye.djinni b/wrappers/android/mynteye/scripts/mynteye.djinni new file mode 100644 index 0000000..236cfc5 --- /dev/null +++ b/wrappers/android/mynteye/scripts/mynteye.djinni @@ -0,0 +1,33 @@ + +format = enum { + grey; + yuyv; + bgr888; + rgb888; +} + +stream_request = record { + index: i32; + width: i32; + height: i32; + format: format; + fps: i32; +} + +device_usb_info = record { + index: i32; + name: string; + sn: string; +} + +device = interface +c { + static query(): list; + + static create(info: device_usb_info): device; + + get_stream_requests(): list; + config_stream_request(request: stream_request); + + start(); + stop(); +} diff --git a/wrappers/android/mynteye/scripts/run_djinni.sh b/wrappers/android/mynteye/scripts/run_djinni.sh new file mode 100755 index 0000000..675cb15 --- /dev/null +++ b/wrappers/android/mynteye/scripts/run_djinni.sh @@ -0,0 +1,83 @@ +#! /usr/bin/env bash +set -e +shopt -s nullglob + +base_dir=$(cd "$(dirname "$0")" && pwd) + +# options + +while getopts "d:i:" opt; do + case "$opt" in + d) djinni_dir="$OPTARG" ;; + i) in_idl="$OPTARG" ;; + ?) echo "Usage: $0 <-d DJINNI_DIR> [-i IN_IDL]" + exit 2 ;; + esac +done + +if [ -z "$djinni_dir" ]; then + echo "<-d DJINNI_DIR> option is required" 1>&2 + exit 2 +fi + +[ -n "$in_idl" ] || in_idl="$base_dir/mynteye.djinni" + +# generate + +djinni_run="$djinni_dir/src/run-assume-built" +if [ ! -x "$djinni_run" ]; then + echo "djinni run file not found: $djinni_run" 1>&2 + exit 2 +fi + +temp_out="$base_dir/djinni-output-temp" + +java_package="com.slightech.mynteye" +cpp_namespace="mynteye_jni" + +[ ! -e "$temp_out" ] || rm -r "$temp_out" +"$djinni_run" \ +--java-out "$temp_out/java" \ +--java-package "$java_package" \ +--java-class-access-modifier "public" \ +--java-generate-interfaces true \ +--java-nullable-annotation "androidx.annotation.Nullable" \ +--java-nonnull-annotation "androidx.annotation.NonNull" \ +--ident-java-field mFooBar \ +\ +--cpp-out "$temp_out/cpp" \ +--cpp-namespace "$cpp_namespace" \ +--ident-cpp-enum-type FooBar \ +--ident-cpp-method FooBar \ +\ +--jni-out "$temp_out/jni" \ +--ident-jni-class NativeFooBar \ +--ident-jni-file NativeFooBar \ +\ +--idl "$in_idl" + +# copy + +mirror() { + local prefix="$1" ; shift + local src="$1" ; shift + local dest="$1" ; shift + mkdir -p "$dest" + rsync -r --delete --checksum --itemize-changes "$src"/ "$dest" | sed "s/^/[$prefix]/" +} + +dst_dir="$base_dir/../libmynteye/src/main" +cpp_out="$dst_dir/cpp/mynteye/cpp" +jni_out="$dst_dir/cpp/mynteye/jni" +java_out="$dst_dir/java/com/slightech/mynteye" + +gen_stamp="$temp_out/gen.stamp" + +echo "Copying generated code to final directories..." +mirror "cpp" "$temp_out/cpp" "$cpp_out" +mirror "jni" "$temp_out/jni" "$jni_out" +mirror "java" "$temp_out/java" "$java_out" + +date > "$gen_stamp" + +echo "djinni completed." From 5105b5ea82b1100751d8d62556a8ee56e01f2657 Mon Sep 17 00:00:00 2001 From: John Zhao Date: Wed, 16 Jan 2019 10:38:46 +0800 Subject: [PATCH 04/21] feat(miniglog): remove log_severity_global --- include/mynteye/logger.h | 1 + include/mynteye/miniglog.h | 8 ++------ src/mynteye/miniglog.cc | 2 -- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/include/mynteye/logger.h b/include/mynteye/logger.h index 91b83fb..823fac7 100644 --- a/include/mynteye/logger.h +++ b/include/mynteye/logger.h @@ -86,6 +86,7 @@ struct glog_init { #else #define MYNTEYE_MAX_LOG_LEVEL google::INFO +// #define MYNTEYE_MAX_LOG_LEVEL 2 #include "mynteye/miniglog.h" diff --git a/include/mynteye/miniglog.h b/include/mynteye/miniglog.h index 02d377a..83ca179 100644 --- a/include/mynteye/miniglog.h +++ b/include/mynteye/miniglog.h @@ -157,9 +157,6 @@ class MYNTEYE_API LogSink { // Global set of log sinks. The actual object is defined in logging.cc. MYNTEYE_API extern std::set log_sinks_global; -// Added by chachi - a runtime global maximum log level. Defined in logging.cc -MYNTEYE_API extern int log_severity_global; - inline void InitGoogleLogging(char */*argv*/) { // Do nothing; this is ignored. } @@ -315,9 +312,8 @@ class MYNTEYE_API LoggerVoidify { // Log only if condition is met. Otherwise evaluates to void. #define LOG_IF(severity, condition) \ - (static_cast(severity) > google::log_severity_global || !(condition)) ? \ - (void) 0 : LoggerVoidify() & \ - MessageLogger((char *)__FILE__, __LINE__, "native", severity).stream() + !(condition) ? (void) 0 : LoggerVoidify() & \ + MessageLogger((char *)__FILE__, __LINE__, "native", severity).stream() // Log only if condition is NOT met. Otherwise evaluates to void. #define LOG_IF_FALSE(severity, condition) LOG_IF(severity, !(condition)) diff --git a/src/mynteye/miniglog.cc b/src/mynteye/miniglog.cc index 0bda4be..8825038 100644 --- a/src/mynteye/miniglog.cc +++ b/src/mynteye/miniglog.cc @@ -36,6 +36,4 @@ namespace google { // that there is only one instance of this across the entire program. std::set log_sinks_global; -int log_severity_global(INFO); - } // namespace google From d5311120757a21db572f52c596f47620c53ff4b4 Mon Sep 17 00:00:00 2001 From: John Zhao Date: Wed, 16 Jan 2019 10:41:12 +0800 Subject: [PATCH 05/21] feat(android): add libshell and list devices --- wrappers/android/mynteye/app/build.gradle | 1 + .../mynteye/app/src/main/AndroidManifest.xml | 6 +- .../slightech/mynteye/demo/MainActivity.java | 20 - .../mynteye/demo/ui/BaseActivity.java | 63 + .../mynteye/demo/ui/MainActivity.java | 105 ++ .../mynteye/demo/util/RootUtils.java | 76 ++ .../app/src/main/res/layout/activity_main.xml | 3 +- .../app/src/main/res/menu/menu_main.xml | 9 + .../app/src/main/res/values/strings.xml | 2 + .../src/main/cpp/mynteye/impl/device_impl.cpp | 9 + wrappers/android/mynteye/libshell/.gitignore | 1 + wrappers/android/mynteye/libshell/README.md | 2 + .../android/mynteye/libshell/build.gradle | 29 + .../mynteye/libshell/proguard-rules.pro | 21 + .../libshell/src/main/AndroidManifest.xml | 2 + .../com/stericson/RootShell/RootShell.java | 610 ++++++++++ .../RootShell/containers/RootClass.java | 348 ++++++ .../exceptions/RootDeniedException.java | 32 + .../RootShell/execution/Command.java | 350 ++++++ .../RootShell/execution/JavaCommand.java | 58 + .../stericson/RootShell/execution/Shell.java | 1038 +++++++++++++++++ wrappers/android/mynteye/settings.gradle | 2 +- 22 files changed, 2764 insertions(+), 23 deletions(-) delete mode 100644 wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java create mode 100644 wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/BaseActivity.java create mode 100644 wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java create mode 100644 wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/util/RootUtils.java create mode 100644 wrappers/android/mynteye/app/src/main/res/menu/menu_main.xml create mode 100644 wrappers/android/mynteye/libshell/.gitignore create mode 100644 wrappers/android/mynteye/libshell/README.md create mode 100644 wrappers/android/mynteye/libshell/build.gradle create mode 100644 wrappers/android/mynteye/libshell/proguard-rules.pro create mode 100644 wrappers/android/mynteye/libshell/src/main/AndroidManifest.xml create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/RootShell.java create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/containers/RootClass.java create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/exceptions/RootDeniedException.java create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Command.java create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/JavaCommand.java create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Shell.java diff --git a/wrappers/android/mynteye/app/build.gradle b/wrappers/android/mynteye/app/build.gradle index 3e74552..5ba3d55 100644 --- a/wrappers/android/mynteye/app/build.gradle +++ b/wrappers/android/mynteye/app/build.gradle @@ -37,6 +37,7 @@ dependencies { annotationProcessor 'com.jakewharton:butterknife-compiler:10.0.0' implementation project(':libmynteye') + implementation project(':libshell') testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' diff --git a/wrappers/android/mynteye/app/src/main/AndroidManifest.xml b/wrappers/android/mynteye/app/src/main/AndroidManifest.xml index 0e03278..3633dae 100644 --- a/wrappers/android/mynteye/app/src/main/AndroidManifest.xml +++ b/wrappers/android/mynteye/app/src/main/AndroidManifest.xml @@ -3,6 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" package="com.slightech.mynteye.demo"> + + + + - + diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java deleted file mode 100644 index ef9edd7..0000000 --- a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.slightech.mynteye.demo; - -import androidx.appcompat.app.AppCompatActivity; -import android.os.Bundle; -import com.slightech.mynteye.Device; -import com.slightech.mynteye.DeviceUsbInfo; -import timber.log.Timber; - -public class MainActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - for (DeviceUsbInfo info : Device.query()) { - Timber.i(info.toString()); - } - } -} diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/BaseActivity.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/BaseActivity.java new file mode 100644 index 0000000..aa35d74 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/BaseActivity.java @@ -0,0 +1,63 @@ +package com.slightech.mynteye.demo.ui; + +import android.annotation.SuppressLint; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import static android.Manifest.permission.CAMERA; +import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; + +@SuppressLint("Registered") +public class BaseActivity extends AppCompatActivity { + + private final int REQ_PERMISSIONS = 1; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestPermissions(); + } + + private void requestPermissions() { + final String[] permissions = new String[]{WRITE_EXTERNAL_STORAGE, CAMERA}; + + boolean granted = true; + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(this, permission) + != PackageManager.PERMISSION_GRANTED) { + granted = false; + } + } + if (granted) return; + + ActivityCompat.requestPermissions(this, permissions, REQ_PERMISSIONS); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (requestCode == REQ_PERMISSIONS) { + boolean granted = true; + if (grantResults.length < 1) { + granted = false; + } else { + for (int result : grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + granted = false; + } + } + } + if (!granted) { + Toast.makeText(this, "Permission denied :(", Toast.LENGTH_LONG).show(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } +} diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java new file mode 100644 index 0000000..23abf24 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java @@ -0,0 +1,105 @@ +package com.slightech.mynteye.demo.ui; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; +import butterknife.ButterKnife; +import com.slightech.mynteye.Device; +import com.slightech.mynteye.DeviceUsbInfo; +import com.slightech.mynteye.demo.R; +import com.slightech.mynteye.demo.util.RootUtils; +import java.util.ArrayList; +import java.util.Locale; + +public class MainActivity extends BaseActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + ButterKnife.bind(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + MenuItem item = menu.findItem(R.id.action_open); + if (item != null) { + item.setEnabled(false); + actionOpen(() -> item.setEnabled(true)); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_open: + item.setEnabled(false); + actionOpen(() -> item.setEnabled(true)); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void actionOpen(final Runnable completeEvent) { + if (!RootUtils.isRooted()) { + alert("Warning", "Root denied :("); + return; + } + RootUtils.requestAccessible(ok -> { + if (completeEvent != null) completeEvent.run(); + if (ok) { + toast("Root granted :)"); + showDevices(); + } else { + alert("Warning", "There are no devices accessible."); + } + }); + } + + private void showDevices() { + ArrayList infos = Device.query(); + if (infos.isEmpty()) { + alert("Warning", "There are no devices :("); + } else { + ArrayList items = new ArrayList<>(); + for (DeviceUsbInfo info : infos) { + items.add(String.format(Locale.getDefault(), "%d, %s, SN: %s", + info.getIndex(), info.getName(), info.getSn())); + } + + AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle("Devices") + .create(); + ListView listView = new ListView(this); + listView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items)); + listView.setOnItemClickListener((parent, view, position, id) -> { + dialog.dismiss(); + openDevice(infos.get(position)); + }); + dialog.setView(listView); + dialog.show(); + } + } + + private void openDevice(DeviceUsbInfo info) { + } + + private void toast(CharSequence text) { + Toast.makeText(this, text, Toast.LENGTH_LONG).show(); + } + + private void alert(CharSequence title, CharSequence message) { + new AlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } +} diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/util/RootUtils.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/util/RootUtils.java new file mode 100644 index 0000000..36c19a4 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/util/RootUtils.java @@ -0,0 +1,76 @@ +package com.slightech.mynteye.demo.util; + +import com.stericson.RootShell.RootShell; +import com.stericson.RootShell.exceptions.RootDeniedException; +import com.stericson.RootShell.execution.Command; +import com.stericson.RootShell.execution.Shell; +import java.io.IOException; +import java.util.concurrent.TimeoutException; +import timber.log.Timber; + +public final class RootUtils { + + public interface OnRequestAccessibleListener { + void onRequestAccessible(boolean ok); + } + + public static boolean isRooted() { + if (!RootShell.isRootAvailable()) { + Timber.e("Root not found"); + return false; + } + + try { + RootShell.getShell(true); + } catch (IOException e) { + e.printStackTrace(); + return false; + } catch (TimeoutException e) { + Timber.e("TIMEOUT EXCEPTION!"); + e.printStackTrace(); + return false; + } catch (RootDeniedException e) { + Timber.e("ROOT DENIED EXCEPTION!"); + e.printStackTrace(); + return false; + } + + try { + if (!RootShell.isAccessGiven()) { + Timber.e("ERROR: No root access to this device."); + return false; + } + } catch (Exception e) { + Timber.e("ERROR: could not determine root access to this device."); + return false; + } + + return true; + } + + public static void requestAccessible(OnRequestAccessibleListener l) { + try { + Shell sh = RootShell.getShell(true); + sh.add(new Command(1, "chmod 666 /dev/video*") { + @Override + public void commandOutput(int id, String line) { + Timber.d("commandOutput: %s", line); + super.commandOutput(id, line); + } + @Override + public void commandTerminated(int id, String reason) { + Timber.d("commandTerminated: %s", reason); + } + @Override + public void commandCompleted(int id, int exitcode) { + Timber.d("commandCompleted: %s", ((exitcode == 0) ? "ok" : "fail")); + if (l != null) l.onRequestAccessible(exitcode == 0); + } + }); + sh.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml b/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml index 8a0f7e4..32dae9a 100644 --- a/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml +++ b/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml @@ -5,10 +5,11 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity" + tools:context=".ui.MainActivity" > +

+ + diff --git a/wrappers/android/mynteye/app/src/main/res/values/strings.xml b/wrappers/android/mynteye/app/src/main/res/values/strings.xml index d69f619..4cb552d 100644 --- a/wrappers/android/mynteye/app/src/main/res/values/strings.xml +++ b/wrappers/android/mynteye/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ mynteye + + Open diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp index f9cb061..8677b10 100644 --- a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp @@ -2,6 +2,7 @@ #include "mynteye/device/context.h" #include "mynteye/device/device.h" +#include "mynteye/logger.h" #include "device_usb_info.hpp" #include "stream_request.hpp" @@ -12,6 +13,7 @@ MYNTEYE_USE_NAMESPACE namespace mynteye_jni { std::vector Device::Query() { + VLOG(2) << __func__; std::vector infos; Context context; @@ -27,6 +29,7 @@ std::vector Device::Query() { } std::shared_ptr Device::Create(const DeviceUsbInfo & info) { + VLOG(2) << __func__; Context context; int32_t i = 0; for (auto&& d : context.devices()) { @@ -39,12 +42,15 @@ std::shared_ptr Device::Create(const DeviceUsbInfo & info) { } DeviceImpl::DeviceImpl(const device_t & device) : Device(), device_(device) { + VLOG(2) << __func__; } DeviceImpl::~DeviceImpl() { + VLOG(2) << __func__; } std::vector DeviceImpl::GetStreamRequests() { + VLOG(2) << __func__; std::vector requests; int32_t i = 0; @@ -58,6 +64,7 @@ std::vector DeviceImpl::GetStreamRequests() { } void DeviceImpl::ConfigStreamRequest(const StreamRequest & request) { + VLOG(2) << __func__; int32_t i = 0; for (auto&& req : device_->GetStreamRequests()) { if (i == request.index) { @@ -69,9 +76,11 @@ void DeviceImpl::ConfigStreamRequest(const StreamRequest & request) { } void DeviceImpl::Start() { + VLOG(2) << __func__; } void DeviceImpl::Stop() { + VLOG(2) << __func__; } } // namespace mynteye_jni diff --git a/wrappers/android/mynteye/libshell/.gitignore b/wrappers/android/mynteye/libshell/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/wrappers/android/mynteye/libshell/.gitignore @@ -0,0 +1 @@ +/build diff --git a/wrappers/android/mynteye/libshell/README.md b/wrappers/android/mynteye/libshell/README.md new file mode 100644 index 0000000..d9948a7 --- /dev/null +++ b/wrappers/android/mynteye/libshell/README.md @@ -0,0 +1,2 @@ + +* [RootShell](https://github.com/Stericson/RootShell) diff --git a/wrappers/android/mynteye/libshell/build.gradle b/wrappers/android/mynteye/libshell/build.gradle new file mode 100644 index 0000000..f996e9a --- /dev/null +++ b/wrappers/android/mynteye/libshell/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion xversions.compileSdk + + defaultConfig { + minSdkVersion xversions.minSdk + targetSdkVersion xversions.targetSdk + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/wrappers/android/mynteye/libshell/proguard-rules.pro b/wrappers/android/mynteye/libshell/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/wrappers/android/mynteye/libshell/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 diff --git a/wrappers/android/mynteye/libshell/src/main/AndroidManifest.xml b/wrappers/android/mynteye/libshell/src/main/AndroidManifest.xml new file mode 100644 index 0000000..11442a8 --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/RootShell.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/RootShell.java new file mode 100644 index 0000000..d959e88 --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/RootShell.java @@ -0,0 +1,610 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ +package com.stericson.RootShell; + + +import com.stericson.RootShell.exceptions.RootDeniedException; +import com.stericson.RootShell.execution.Command; +import com.stericson.RootShell.execution.Shell; + +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeoutException; + +public class RootShell { + + // -------------------- + // # Public Variables # + // -------------------- + + public static boolean debugMode = false; + + public static final String version = "RootShell v1.6"; + + /** + * Setting this to false will disable the handler that is used + * by default for the 3 callback methods for Command. + *

+ * By disabling this all callbacks will be called from a thread other than + * the main UI thread. + */ + public static boolean handlerEnabled = true; + + + /** + * Setting this will change the default command timeout. + *

+ * The default is 20000ms + */ + public static int defaultCommandTimeout = 20000; + + public static enum LogLevel { + VERBOSE, + ERROR, + DEBUG, + WARN + } + // -------------------- + // # Public Methods # + // -------------------- + + /** + * This will close all open shells. + */ + public static void closeAllShells() throws IOException { + Shell.closeAll(); + } + + /** + * This will close the custom shell that you opened. + */ + public static void closeCustomShell() throws IOException { + Shell.closeCustomShell(); + } + + /** + * This will close either the root shell or the standard shell depending on what you specify. + * + * @param root a boolean to specify whether to close the root shell or the standard shell. + */ + public static void closeShell(boolean root) throws IOException { + if (root) { + Shell.closeRootShell(); + } else { + Shell.closeShell(); + } + } + + /** + * Use this to check whether or not a file exists on the filesystem. + * + * @param file String that represent the file, including the full path to the + * file and its name. + * @return a boolean that will indicate whether or not the file exists. + */ + public static boolean exists(final String file) { + return exists(file, false); + } + + /** + * Use this to check whether or not a file OR directory exists on the filesystem. + * + * @param file String that represent the file OR the directory, including the full path to the + * file and its name. + * @param isDir boolean that represent whether or not we are looking for a directory + * @return a boolean that will indicate whether or not the file exists. + */ + public static boolean exists(final String file, boolean isDir) { + final List result = new ArrayList(); + + String cmdToExecute = "ls " + (isDir ? "-d " : " "); + + Command command = new Command(0, false, cmdToExecute + file) { + @Override + public void commandOutput(int id, String line) { + RootShell.log(line); + result.add(line); + + super.commandOutput(id, line); + } + }; + + try { + //Try without root... + RootShell.getShell(false).add(command); + commandWait(RootShell.getShell(false), command); + + } catch (Exception e) { + RootShell.log("Exception: " + e); + return false; + } + + for (String line : result) { + if (line.trim().equals(file)) { + return true; + } + } + + result.clear(); + + command = new Command(0, false, cmdToExecute + file) { + @Override + public void commandOutput(int id, String line) { + RootShell.log(line); + result.add(line); + + super.commandOutput(id, line); + } + }; + + try { + RootShell.getShell(true).add(command); + commandWait(RootShell.getShell(true), command); + + } catch (Exception e) { + RootShell.log("Exception: " + e); + return false; + } + + //Avoid concurrent modification... + List final_result = new ArrayList(); + final_result.addAll(result); + + for (String line : final_result) { + if (line.trim().equals(file)) { + return true; + } + } + + return false; + + } + + /** + * @param binaryName String that represent the binary to find. + * @param singlePath boolean that represents whether to return a single path or multiple. + * + * @return List containing the locations the binary was found at. + */ + public static List findBinary(String binaryName, boolean singlePath) { + return findBinary(binaryName, null, singlePath); + } + + /** + * @param binaryName String that represent the binary to find. + * @param searchPaths List which contains the paths to search for this binary in. + * @param singlePath boolean that represents whether to return a single path or multiple. + * + * @return List containing the locations the binary was found at. + */ + public static List findBinary(final String binaryName, List searchPaths, boolean singlePath) { + + final List foundPaths = new ArrayList(); + + boolean found = false; + + if(searchPaths == null) + { + searchPaths = RootShell.getPath(); + } + + RootShell.log("Checking for " + binaryName); + + //Try to use stat first + try { + for (String path : searchPaths) { + + if(!path.endsWith("/")) + { + path += "/"; + } + + final String currentPath = path; + + Command cc = new Command(0, false, "stat " + path + binaryName) { + @Override + public void commandOutput(int id, String line) { + if (line.contains("File: ") && line.contains(binaryName)) { + foundPaths.add(currentPath); + + RootShell.log(binaryName + " was found here: " + currentPath); + } + + RootShell.log(line); + + super.commandOutput(id, line); + } + }; + + cc = RootShell.getShell(false).add(cc); + commandWait(RootShell.getShell(false), cc); + + if(foundPaths.size() > 0 && singlePath) { + break; + } + } + + found = !foundPaths.isEmpty(); + + } catch (Exception e) { + RootShell.log(binaryName + " was not found, more information MAY be available with Debugging on."); + } + + if (!found) { + RootShell.log("Trying second method"); + + for (String path : searchPaths) { + + if(!path.endsWith("/")) + { + path += "/"; + } + + if (RootShell.exists(path + binaryName)) { + RootShell.log(binaryName + " was found here: " + path); + foundPaths.add(path); + + if(foundPaths.size() > 0 && singlePath) { + break; + } + + } else { + RootShell.log(binaryName + " was NOT found here: " + path); + } + } + } + + Collections.reverse(foundPaths); + + return foundPaths; + } + + /** + * This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param shellPath a String to Indicate the path to the shell that you want to open. + * @param timeout an int to Indicate the length of time before giving up on opening a shell. + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException + { + return Shell.startCustomShell(shellPath, timeout); + } + + /** + * This will return the environment variable PATH + * + * @return List A List of Strings representing the environment variable $PATH + */ + public static List getPath() { + return Arrays.asList(System.getenv("PATH").split(":")); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @param shellContext the context to execute the shell with + * @param retry a int to indicate how many times the ROOT shell should try to open with root priviliges... + */ + public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException { + if (root) { + return Shell.startRootShell(timeout, shellContext, retry); + } else { + return Shell.startShell(timeout); + } + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @param shellContext the context to execute the shell with + */ + public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, timeout, shellContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param shellContext the context to execute the shell with + */ + public static Shell getShell(boolean root, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, 0, shellContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + */ + public static Shell getShell(boolean root, int timeout) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, timeout, Shell.defaultContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + */ + public static Shell getShell(boolean root) throws IOException, TimeoutException, RootDeniedException { + return RootShell.getShell(root, 0); + } + + /** + * @return true if your app has been given root access. + * @throws TimeoutException if this operation times out. (cannot determine if access is given) + */ + public static boolean isAccessGiven() { + return isAccessGiven(0, 3); + } + + /** + * Control how many time of retries should request + * + * @param timeout The timeout + * @param retries The number of retries + * + * @return true if your app has been given root access. + * @throws TimeoutException if this operation times out. (cannot determine if access is given) + */ + public static boolean isAccessGiven(int timeout, int retries) { + final Set ID = new HashSet(); + final int IAG = 158; + + try { + RootShell.log("Checking for Root access"); + + Command command = new Command(IAG, false, "id") { + @Override + public void commandOutput(int id, String line) { + if (id == IAG) { + ID.addAll(Arrays.asList(line.split(" "))); + } + + super.commandOutput(id, line); + } + }; + + Shell shell = Shell.startRootShell(timeout, retries); + shell.add(command); + commandWait(shell, command); + + //parse the userid + for (String userid : ID) { + RootShell.log(userid); + + if (userid.toLowerCase().contains("uid=0")) { + RootShell.log("Access Given"); + return true; + } + } + + return false; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * @return true if BusyBox was found. + */ + public static boolean isBusyboxAvailable() + { + return isBusyboxAvailable(false); + } + + /** + * @return true if BusyBox or Toybox was found. + */ + public static boolean isBusyboxAvailable(boolean includeToybox) + { + if(includeToybox) { + return (findBinary("busybox", true)).size() > 0 || (findBinary("toybox", true)).size() > 0; + } else { + return (findBinary("busybox", true)).size() > 0; + } + } + + /** + * @return true if su was found. + */ + public static boolean isRootAvailable() { + return (findBinary("su", true)).size() > 0; + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param msg The message to output. + */ + public static void log(String msg) { + log(null, msg, LogLevel.DEBUG, null); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param TAG Optional parameter to define the tag that the Log will use. + * @param msg The message to output. + */ + public static void log(String TAG, String msg) { + log(TAG, msg, LogLevel.DEBUG, null); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param msg The message to output. + * @param type The type of log, 1 for verbose, 2 for error, 3 for debug, 4 for warn + * @param e The exception that was thrown (Needed for errors) + */ + public static void log(String msg, LogLevel type, Exception e) { + log(null, msg, type, e); + } + + /** + * This method allows you to check whether logging is enabled. + * Yes, it has a goofy name, but that's to keep it as short as possible. + * After all writing logging calls should be painless. + * This method exists to save Android going through the various Java layers + * that are traversed any time a string is created (i.e. what you are logging) + *

+ * Example usage: + * if(islog) { + * StrinbBuilder sb = new StringBuilder(); + * // ... + * // build string + * // ... + * log(sb.toString()); + * } + * + * @return true if logging is enabled + */ + public static boolean islog() { + return debugMode; + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param TAG Optional parameter to define the tag that the Log will use. + * @param msg The message to output. + * @param type The type of log, 1 for verbose, 2 for error, 3 for debug + * @param e The exception that was thrown (Needed for errors) + */ + public static void log(String TAG, String msg, LogLevel type, Exception e) { + if (msg != null && !msg.equals("")) { + if (debugMode) { + if (TAG == null) { + TAG = version; + } + + switch (type) { + case VERBOSE: + Log.v(TAG, msg); + break; + case ERROR: + Log.e(TAG, msg, e); + break; + case DEBUG: + Log.d(TAG, msg); + break; + case WARN: + Log.w(TAG, msg); + break; + } + } + } + } + + // -------------------- + // # Public Methods # + // -------------------- + + private static void commandWait(Shell shell, Command cmd) throws Exception { + while (!cmd.isFinished()) { + + RootShell.log(version, shell.getCommandQueuePositionString(cmd)); + RootShell.log(version, "Processed " + cmd.totalOutputProcessed + " of " + cmd.totalOutput + " output from command."); + + synchronized (cmd) { + try { + if (!cmd.isFinished()) { + cmd.wait(2000); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + if (!cmd.isExecuting() && !cmd.isFinished()) { + if (!shell.isExecuting && !shell.isReading) { + RootShell.log(version, "Waiting for a command to be executed in a shell that is not executing and not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } else if (shell.isExecuting && !shell.isReading) { + RootShell.log(version, "Waiting for a command to be executed in a shell that is executing but not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } else { + RootShell.log(version, "Waiting for a command to be executed in a shell that is not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } + } + + } + } +} diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/containers/RootClass.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/containers/RootClass.java new file mode 100644 index 0000000..9cf7659 --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/containers/RootClass.java @@ -0,0 +1,348 @@ +package com.stericson.RootShell.containers; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FilenameFilter; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* #ANNOTATIONS @SupportedAnnotationTypes("com.stericson.RootShell.containers.RootClass.Candidate") */ +/* #ANNOTATIONS @SupportedSourceVersion(SourceVersion.RELEASE_6) */ +public class RootClass /* #ANNOTATIONS extends AbstractProcessor */ { + + /* #ANNOTATIONS + @Override + public boolean process(Set typeElements, RoundEnvironment roundEnvironment) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "I was invoked!!!"); + + return false; + } + */ + + static String PATH_TO_DX = "/Users/Chris/Projects/android-sdk-macosx/build-tools/18.0.1/dx"; + + enum READ_STATE { + STARTING, FOUND_ANNOTATION; + } + + public RootClass(String[] args) throws ClassNotFoundException, NoSuchMethodException, + IllegalAccessException, InvocationTargetException, InstantiationException { + + // Note: rather than calling System.load("/system/lib/libandroid_runtime.so"); + // which would leave a bunch of unresolved JNI references, + // we are using the 'withFramework' class as a preloader. + // So, yeah, russian dolls: withFramework > RootClass > actual method + + String className = args[0]; + RootArgs actualArgs = new RootArgs(); + actualArgs.args = new String[args.length - 1]; + System.arraycopy(args, 1, actualArgs.args, 0, args.length - 1); + Class classHandler = Class.forName(className); + Constructor classConstructor = classHandler.getConstructor(RootArgs.class); + classConstructor.newInstance(actualArgs); + } + + public @interface Candidate { + + } + + ; + + public class RootArgs { + + public String args[]; + } + + static void displayError(Exception e) { + // Not using system.err to make it easier to capture from + // calling library. + System.out.println("##ERR##" + e.getMessage() + "##"); + e.printStackTrace(); + } + + // I reckon it would be better to investigate classes using getAttribute() + // however this method allows the developer to simply select "Run" on RootClass + // and immediately re-generate the necessary jar file. + static public class AnnotationsFinder { + + private final String AVOIDDIRPATH = "stericson" + File.separator + "RootShell" + File.separator; + + private List classFiles; + + public AnnotationsFinder() throws IOException { + System.out.println("Discovering root class annotations..."); + classFiles = new ArrayList(); + lookup(new File("src"), classFiles); + System.out.println("Done discovering annotations. Building jar file."); + File builtPath = getBuiltPath(); + if (null != builtPath) { + // Android! Y U no have com.google.common.base.Joiner class? + String rc1 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass.class"; + String rc2 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$RootArgs.class"; + String rc3 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder.class"; + String rc4 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder$1.class"; + String rc5 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder$2.class"; + String[] cmd; + boolean onWindows = (-1 != System.getProperty("os.name").toLowerCase().indexOf("win")); + if (onWindows) { + StringBuilder sb = new StringBuilder( + " " + rc1 + " " + rc2 + " " + rc3 + " " + rc4 + " " + rc5 + ); + for (File file : classFiles) { + sb.append(" " + file.getPath()); + } + cmd = new String[]{ + "cmd", "/C", + "jar cvf" + + " anbuild.jar" + + sb.toString() + }; + } else { + ArrayList al = new ArrayList(); + al.add("jar"); + al.add("cf"); + al.add("anbuild.jar"); + al.add(rc1); + al.add(rc2); + al.add(rc3); + al.add(rc4); + al.add(rc5); + for (File file : classFiles) { + al.add(file.getPath()); + } + cmd = al.toArray(new String[al.size()]); + } + ProcessBuilder jarBuilder = new ProcessBuilder(cmd); + jarBuilder.directory(builtPath); + try { + jarBuilder.start().waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + } + + String strRawFolder = "res" + File.separator + "raw"; + if (builtPath.toString().startsWith("build")); //Check if running in AndroidStudio + strRawFolder = "src" + File.separator + "main" + File.separator + "res" + File.separator + "raw"; + + File rawFolder = new File(strRawFolder); + if (!rawFolder.exists()) { + rawFolder.mkdirs(); + } + + System.out.println("Done building jar file. Creating dex file."); + if (onWindows) { + cmd = new String[]{ + "cmd", "/C", + "dx --dex --output=" + strRawFolder + File.separator + "anbuild.dex " + + builtPath + File.separator + "anbuild.jar" + }; + } else { + cmd = new String[]{ + getPathToDx(), + "--dex", + "--output=" + strRawFolder + File.separator + "anbuild.dex", + builtPath + File.separator + "anbuild.jar" + }; + } + ProcessBuilder dexBuilder = new ProcessBuilder(cmd); + try { + dexBuilder.start().waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + } + } + System.out.println("All done. ::: anbuild.dex should now be in your project's src" + File.separator + "main" + File.separator + "res" + File.separator + "raw" + File.separator + " folder :::"); + } + + protected void lookup(File path, List fileList) { + String desourcedPath = path.toString().replace("src" + File.separator, "").replace("main" + File.separator + "java" + File.separator, ""); + + File[] files = path.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + return true; + } + }); + for (File file : files) { + if (file.isDirectory()) { + if (-1 == file.getAbsolutePath().indexOf(AVOIDDIRPATH)) { + lookup(file, fileList); + } + } else { + if (file.getName().endsWith(".java")) { + if (hasClassAnnotation(file)) { + final String fileNamePrefix = file.getName().replace(".java", ""); + final File compiledPath = new File(getBuiltPath().toString() + File.separator + desourcedPath); + File[] classAndInnerClassFiles = compiledPath.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String filename) { + return filename.startsWith(fileNamePrefix); + } + }); + for (final File matchingFile : classAndInnerClassFiles) { + fileList.add(new File(desourcedPath + File.separator + matchingFile.getName())); + } + + } + } + } + } + } + + protected boolean hasClassAnnotation(File file) { + READ_STATE readState = READ_STATE.STARTING; + Pattern p = Pattern.compile(" class ([A-Za-z0-9_]+)"); + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + String line; + while (null != (line = reader.readLine())) { + switch (readState) { + case STARTING: + if (-1 < line.indexOf("@RootClass.Candidate")) { + readState = READ_STATE.FOUND_ANNOTATION; + } + break; + case FOUND_ANNOTATION: + Matcher m = p.matcher(line); + if (m.find()) { + System.out.println(" Found annotated class: " + m.group(0)); + return true; + } else { + System.err.println("Error: unmatched annotation in " + + file.getAbsolutePath()); + readState = READ_STATE.STARTING; + } + break; + } + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + protected String getPathToDx() throws IOException { + String androidHome = System.getenv("ANDROID_HOME"); + if (null == androidHome) { + throw new IOException("Error: you need to set $ANDROID_HOME globally"); + } + String dxPath = null; + File[] files = new File(androidHome + File.separator + "build-tools").listFiles(); + int recentSdkVersion = 0; + for (File file : files) { + + String fileName = null; + if (file.getName().contains("-")) { + String[] splitFileName = file.getName().split("-"); + if (splitFileName[1].contains("W")) { + char[] fileNameChars = splitFileName[1].toCharArray(); + fileName = String.valueOf(fileNameChars[0]); + } else if (splitFileName[1].contains("rc")) { + continue; //Do not use release candidates + } else { + fileName = splitFileName[1]; + } + } else { + fileName = file.getName(); + } + + int sdkVersion; + + String[] sdkVersionBits = fileName.split("[.]"); + sdkVersion = Integer.parseInt(sdkVersionBits[0]) * 10000; + if (sdkVersionBits.length > 1) { + sdkVersion += Integer.parseInt(sdkVersionBits[1]) * 100; + if (sdkVersionBits.length > 2) { + sdkVersion += Integer.parseInt(sdkVersionBits[2]); + } + } + if (sdkVersion > recentSdkVersion) { + String tentativePath = file.getAbsolutePath() + File.separator + "dx"; + if (new File(tentativePath).exists()) { + recentSdkVersion = sdkVersion; + dxPath = tentativePath; + } + } + } + if (dxPath == null) { + throw new IOException("Error: unable to find dx binary in $ANDROID_HOME"); + } + return dxPath; + } + + protected File getBuiltPath() { + File foundPath = null; + + File ideaPath = new File("out" + File.separator + "production"); // IntelliJ + if (ideaPath.isDirectory()) { + File[] children = ideaPath.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory(); + } + }); + if (children.length > 0) { + foundPath = new File(ideaPath.getAbsolutePath() + File.separator + children[0].getName()); + } + } + if (null == foundPath) { + File eclipsePath = new File("bin" + File.separator + "classes"); // Eclipse IDE + if (eclipsePath.isDirectory()) { + foundPath = eclipsePath; + } + } + if (null == foundPath) { + File androidStudioPath = new File("build" + File.separator + "intermediates" + File.separator + "classes" + File.separator + "debug"); // Android Studio + if (androidStudioPath.isDirectory()) { + foundPath = androidStudioPath; + } + } + + return foundPath; + } + + + } + + public static void main(String[] args) { + try { + if (args.length == 0) { + new AnnotationsFinder(); + } else { + new RootClass(args); + } + } catch (Exception e) { + displayError(e); + } + } +} \ No newline at end of file diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/exceptions/RootDeniedException.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/exceptions/RootDeniedException.java new file mode 100644 index 0000000..758ba8c --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/exceptions/RootDeniedException.java @@ -0,0 +1,32 @@ +/* + * This file is part of the RootShell Project: https://github.com/Stericson/RootShell + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.RootShell.exceptions; + +public class RootDeniedException extends Exception { + + private static final long serialVersionUID = -8713947214162841310L; + + public RootDeniedException(String error) { + super(error); + } +} diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Command.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Command.java new file mode 100644 index 0000000..edb7bad --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Command.java @@ -0,0 +1,350 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.RootShell.execution; + +import com.stericson.RootShell.RootShell; + +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.io.IOException; + +public class Command { + + //directly modified by JavaCommand + protected boolean javaCommand = false; + protected Context context = null; + + public int totalOutput = 0; + + public int totalOutputProcessed = 0; + + ExecutionMonitor executionMonitor = null; + + Handler mHandler = null; + + //Has this command already been used? + protected boolean used = false; + + boolean executing = false; + + String[] command = {}; + + boolean finished = false; + + boolean terminated = false; + + boolean handlerEnabled = true; + + int exitCode = -1; + + int id = 0; + + int timeout = RootShell.defaultCommandTimeout; + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param command the command, or commands, to be executed. + */ + public Command(int id, String... command) { + this.command = command; + this.id = id; + + createHandler(RootShell.handlerEnabled); + } + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param handlerEnabled when true the handler will be used to call the + * callback methods if possible. + * @param command the command, or commands, to be executed. + */ + public Command(int id, boolean handlerEnabled, String... command) { + this.command = command; + this.id = id; + + createHandler(handlerEnabled); + } + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param timeout the time allowed before the shell will give up executing the command + * and throw a TimeoutException. + * @param command the command, or commands, to be executed. + */ + public Command(int id, int timeout, String... command) { + this.command = command; + this.id = id; + this.timeout = timeout; + + createHandler(RootShell.handlerEnabled); + } + + //If you override this you MUST make a final call + //to the super method. The super call should be the last line of this method. + public void commandOutput(int id, String line) { + RootShell.log("Command", "ID: " + id + ", " + line); + totalOutputProcessed++; + } + + public void commandTerminated(int id, String reason) { + //pass + } + + public void commandCompleted(int id, int exitcode) { + //pass + } + + protected final void commandFinished() { + if (!terminated) { + synchronized (this) { + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_COMPLETED); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandCompleted(id, exitCode); + } + + RootShell.log("Command " + id + " finished."); + finishCommand(); + } + } + } + + private void createHandler(boolean handlerEnabled) { + + this.handlerEnabled = handlerEnabled; + + if (Looper.myLooper() != null && handlerEnabled) { + RootShell.log("CommandHandler created"); + mHandler = new CommandHandler(); + } else { + RootShell.log("CommandHandler not created"); + } + } + + public final void finish() + { + RootShell.log("Command finished at users request!"); + commandFinished(); + } + + protected final void finishCommand() { + this.executing = false; + this.finished = true; + this.notifyAll(); + } + + + public final String getCommand() { + StringBuilder sb = new StringBuilder(); + + if(javaCommand) { + String filePath = context.getFilesDir().getPath(); + + for (int i = 0; i < command.length; i++) { + /* + * TODO Make withFramework optional for applications + * that do not require access to the fw. -CFR + */ + //export CLASSPATH=/data/user/0/ch.masshardt.emailnotification/files/anbuild.dex ; app_process /system/bin + if (Build.VERSION.SDK_INT > 22) { + //dalvikvm command is not working in Android Marshmallow + sb.append( + "export CLASSPATH=" + filePath + "/anbuild.dex;" + + " app_process /system/bin " + + command[i]); + } else { + sb.append( + "dalvikvm -cp " + filePath + "/anbuild.dex" + + " com.android.internal.util.WithFramework" + + " com.stericson.RootTools.containers.RootClass " + + command[i]); + } + + sb.append('\n'); + } + } + else { + for (int i = 0; i < command.length; i++) { + sb.append(command[i]); + sb.append('\n'); + } + } + + return sb.toString(); + } + + public final boolean isExecuting() { + return executing; + } + + public final boolean isHandlerEnabled() { + return handlerEnabled; + } + + public final boolean isFinished() { + return finished; + } + + public final int getExitCode() { + return this.exitCode; + } + + protected final void setExitCode(int code) { + synchronized (this) { + exitCode = code; + } + } + + protected final void startExecution() { + this.used = true; + executionMonitor = new ExecutionMonitor(this); + executionMonitor.setPriority(Thread.MIN_PRIORITY); + executionMonitor.start(); + executing = true; + } + + public final void terminate() + { + RootShell.log("Terminating command at users request!"); + terminated("Terminated at users request!"); + } + + protected final void terminate(String reason) { + try { + Shell.closeAll(); + RootShell.log("Terminating all shells."); + terminated(reason); + } catch (IOException e) { + } + } + + protected final void terminated(String reason) { + synchronized (Command.this) { + + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_TERMINATED); + bundle.putString(CommandHandler.TEXT, reason); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandTerminated(id, reason); + } + + RootShell.log("Command " + id + " did not finish because it was terminated. Termination reason: " + reason); + setExitCode(-1); + terminated = true; + finishCommand(); + } + } + + protected final void output(int id, String line) { + totalOutput++; + + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_OUTPUT); + bundle.putString(CommandHandler.TEXT, line); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandOutput(id, line); + } + } + + private class ExecutionMonitor extends Thread { + + private final Command command; + + public ExecutionMonitor(Command command) { + this.command = command; + } + + public void run() { + + if(command.timeout > 0) + { + synchronized (command) { + try { + RootShell.log("Command " + command.id + " is waiting for: " + command.timeout); + command.wait(command.timeout); + } catch (InterruptedException e) { + RootShell.log("Exception: " + e); + } + + if (!command.isFinished()) { + RootShell.log("Timeout Exception has occurred for command: " + command.id + "."); + terminate("Timeout Exception"); + } + } + } + } + } + + private class CommandHandler extends Handler { + + static final public String ACTION = "action"; + + static final public String TEXT = "text"; + + static final public int COMMAND_OUTPUT = 0x01; + + static final public int COMMAND_COMPLETED = 0x02; + + static final public int COMMAND_TERMINATED = 0x03; + + public final void handleMessage(Message msg) { + int action = msg.getData().getInt(ACTION); + String text = msg.getData().getString(TEXT); + + switch (action) { + case COMMAND_OUTPUT: + commandOutput(id, text); + break; + case COMMAND_COMPLETED: + commandCompleted(id, exitCode); + break; + case COMMAND_TERMINATED: + commandTerminated(id, text); + break; + } + } + } +} diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/JavaCommand.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/JavaCommand.java new file mode 100644 index 0000000..032569e --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/JavaCommand.java @@ -0,0 +1,58 @@ +package com.stericson.RootShell.execution; + +import android.content.Context; + +public class JavaCommand extends Command +{ + /** + * Constructor for executing Java commands rather than binaries + * + * @param context needed to execute java command. + */ + public JavaCommand(int id, Context context, String... command) { + super(id, command); + this.context = context; + this.javaCommand = true; + } + + /** + * Constructor for executing Java commands rather than binaries + * + * @param context needed to execute java command. + */ + public JavaCommand(int id, boolean handlerEnabled, Context context, String... command) { + super(id, handlerEnabled, command); + this.context = context; + this.javaCommand = true; + } + + /** + * Constructor for executing Java commands rather than binaries + * + * @param context needed to execute java command. + */ + public JavaCommand(int id, int timeout, Context context, String... command) { + super(id, timeout, command); + this.context = context; + this.javaCommand = true; + } + + + @Override + public void commandOutput(int id, String line) + { + super.commandOutput(id, line); + } + + @Override + public void commandTerminated(int id, String reason) + { + // pass + } + + @Override + public void commandCompleted(int id, int exitCode) + { + // pass + } +} diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Shell.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Shell.java new file mode 100644 index 0000000..b64923e --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Shell.java @@ -0,0 +1,1038 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ +package com.stericson.RootShell.execution; + +import com.stericson.RootShell.RootShell; +import com.stericson.RootShell.exceptions.RootDeniedException; + +import android.content.Context; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; + +public class Shell { + + public static enum ShellType { + NORMAL, + ROOT, + CUSTOM + } + + //this is only used with root shells + public static enum ShellContext { + NORMAL("normal"), //The normal context... + SHELL("u:r:shell:s0"), //unprivileged shell (such as an adb shell) + SYSTEM_SERVER("u:r:system_server:s0"), // system_server, u:r:system:s0 on some firmwares + SYSTEM_APP("u:r:system_app:s0"), // System apps + PLATFORM_APP("u:r:platform_app:s0"), // System apps + UNTRUSTED_APP("u:r:untrusted_app:s0"), // Third-party apps + RECOVERY("u:r:recovery:s0"), //Recovery + SUPERSU("u:r:supersu:s0"); //SUPER SU default + + private String value; + + private ShellContext(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + } + + //Statics -- visible to all + private static final String token = "F*D^W@#FGF"; + + private static Shell rootShell = null; + + private static Shell shell = null; + + private static Shell customShell = null; + + private static String[] suVersion = new String[]{ + null, null + }; + + //the default context for root shells... + public static ShellContext defaultContext = ShellContext.NORMAL; + + //per shell + private int shellTimeout = 25000; + + private ShellType shellType = null; + + private ShellContext shellContext = Shell.ShellContext.NORMAL; + + private String error = ""; + + private final Process proc; + + private final BufferedReader inputStream; + + private final BufferedReader errorStream; + + private final OutputStreamWriter outputStream; + + private final List commands = new ArrayList(); + + //indicates whether or not to close the shell + private boolean close = false; + + private Boolean isSELinuxEnforcing = null; + + public boolean isExecuting = false; + + public boolean isReading = false; + + public boolean isClosed = false; + + private int maxCommands = 5000; + + private int read = 0; + + private int write = 0; + + private int totalExecuted = 0; + + private int totalRead = 0; + + private boolean isCleaning = false; + + private Shell(String cmd, ShellType shellType, ShellContext shellContext, int shellTimeout) throws IOException, TimeoutException, RootDeniedException { + + RootShell.log("Starting shell: " + cmd); + RootShell.log("Context: " + shellContext.getValue()); + RootShell.log("Timeout: " + shellTimeout); + + this.shellType = shellType; + this.shellTimeout = shellTimeout > 0 ? shellTimeout : this.shellTimeout; + this.shellContext = shellContext; + + if (this.shellContext == ShellContext.NORMAL) { + this.proc = Runtime.getRuntime().exec(cmd); + } else { + String display = getSuVersion(false); + String internal = getSuVersion(true); + + //only done for root shell... + //Right now only SUPERSU supports the --context switch + if (isSELinuxEnforcing() && + (display != null) && + (internal != null) && + (display.endsWith("SUPERSU")) && + (Integer.valueOf(internal) >= 190)) { + cmd += " --context " + this.shellContext.getValue(); + } else { + RootShell.log("Su binary --context switch not supported!"); + RootShell.log("Su binary display version: " + display); + RootShell.log("Su binary internal version: " + internal); + RootShell.log("SELinuxEnforcing: " + isSELinuxEnforcing()); + } + + this.proc = Runtime.getRuntime().exec(cmd); + + } + + this.inputStream = new BufferedReader(new InputStreamReader(this.proc.getInputStream(), "UTF-8")); + this.errorStream = new BufferedReader(new InputStreamReader(this.proc.getErrorStream(), "UTF-8")); + this.outputStream = new OutputStreamWriter(this.proc.getOutputStream(), "UTF-8"); + + /** + * Thread responsible for carrying out the requested operations + */ + Worker worker = new Worker(this); + worker.start(); + + try { + /** + * The flow of execution will wait for the thread to die or wait until the + * given timeout has expired. + * + * The result of the worker, which is determined by the exit code of the worker, + * will tell us if the operation was completed successfully or it the operation + * failed. + */ + worker.join(this.shellTimeout); + + /** + * The operation could not be completed before the timeout occurred. + */ + if (worker.exit == -911) { + + try { + this.proc.destroy(); + } catch (Exception e) { + } + + closeQuietly(this.inputStream); + closeQuietly(this.errorStream); + closeQuietly(this.outputStream); + + throw new TimeoutException(this.error); + } + /** + * Root access denied? + */ + else if (worker.exit == -42) { + + try { + this.proc.destroy(); + } catch (Exception e) { + } + + closeQuietly(this.inputStream); + closeQuietly(this.errorStream); + closeQuietly(this.outputStream); + + throw new RootDeniedException("Root Access Denied"); + } + /** + * Normal exit + */ + else { + /** + * The shell is open. + * + * Start two threads, one to handle the input and one to handle the output. + * + * input, and output are runnables that the threads execute. + */ + Thread si = new Thread(this.input, "Shell Input"); + si.setPriority(Thread.NORM_PRIORITY); + si.start(); + + Thread so = new Thread(this.output, "Shell Output"); + so.setPriority(Thread.NORM_PRIORITY); + so.start(); + } + } catch (InterruptedException ex) { + worker.interrupt(); + Thread.currentThread().interrupt(); + throw new TimeoutException(); + } + } + + + public Command add(Command command) throws IOException { + if (this.close) { + throw new IllegalStateException( + "Unable to add commands to a closed shell"); + } + + if(command.used) { + //The command has been used, don't re-use... + throw new IllegalStateException( + "This command has already been executed. (Don't re-use command instances.)"); + } + + while (this.isCleaning) { + //Don't add commands while cleaning + ; + } + + this.commands.add(command); + + this.notifyThreads(); + + return command; + } + + public final void useCWD(Context context) throws IOException, TimeoutException, RootDeniedException { + add( + new Command( + -1, + false, + "cd " + context.getApplicationInfo().dataDir) + ); + } + + private void cleanCommands() { + this.isCleaning = true; + int toClean = Math.abs(this.maxCommands - (this.maxCommands / 4)); + RootShell.log("Cleaning up: " + toClean); + + for (int i = 0; i < toClean; i++) { + this.commands.remove(0); + } + + this.read = this.commands.size() - 1; + this.write = this.commands.size() - 1; + this.isCleaning = false; + } + + private void closeQuietly(final Reader input) { + try { + if (input != null) { + input.close(); + } + } catch (Exception ignore) { + } + } + + private void closeQuietly(final Writer output) { + try { + if (output != null) { + output.close(); + } + } catch (Exception ignore) { + } + } + + public void close() throws IOException { + RootShell.log("Request to close shell!"); + + int count = 0; + while (isExecuting) { + RootShell.log("Waiting on shell to finish executing before closing..."); + count++; + + //fail safe + if (count > 10000) { + break; + } + + } + + synchronized (this.commands) { + /** + * instruct the two threads monitoring input and output + * of the shell to close. + */ + this.close = true; + this.notifyThreads(); + } + + RootShell.log("Shell Closed!"); + + if (this == Shell.rootShell) { + Shell.rootShell = null; + } else if (this == Shell.shell) { + Shell.shell = null; + } else if (this == Shell.customShell) { + Shell.customShell = null; + } + } + + public static void closeCustomShell() throws IOException { + RootShell.log("Request to close custom shell!"); + + if (Shell.customShell == null) { + return; + } + + Shell.customShell.close(); + } + + public static void closeRootShell() throws IOException { + RootShell.log("Request to close root shell!"); + + if (Shell.rootShell == null) { + return; + } + Shell.rootShell.close(); + } + + public static void closeShell() throws IOException { + RootShell.log("Request to close normal shell!"); + + if (Shell.shell == null) { + return; + } + Shell.shell.close(); + } + + public static void closeAll() throws IOException { + RootShell.log("Request to close all shells!"); + + Shell.closeShell(); + Shell.closeRootShell(); + Shell.closeCustomShell(); + } + + public int getCommandQueuePosition(Command cmd) { + return this.commands.indexOf(cmd); + } + + public String getCommandQueuePositionString(Command cmd) { + return "Command is in position " + getCommandQueuePosition(cmd) + " currently executing command at position " + this.write + " and the number of commands is " + commands.size(); + } + + public static Shell getOpenShell() { + if (Shell.customShell != null) { + return Shell.customShell; + } else if (Shell.rootShell != null) { + return Shell.rootShell; + } else { + return Shell.shell; + } + } + + /** + * From libsuperuser. + * + *

+ * Detects the version of the su binary installed (if any), if supported + * by the binary. Most binaries support two different version numbers, + * the public version that is displayed to users, and an internal + * version number that is used for version number comparisons. Returns + * null if su not available or retrieving the version isn't supported. + *

+ *

+ * Note that su binary version and GUI (APK) version can be completely + * different. + *

+ *

+ * This function caches its result to improve performance on multiple + * calls + *

+ * + * @param internal Request human-readable version or application + * internal version + * @return String containing the su version or null + */ + private synchronized String getSuVersion(boolean internal) { + int idx = internal ? 0 : 1; + if (suVersion[idx] == null) { + String version = null; + + // Replace libsuperuser:Shell.run with manual process execution + Process process; + try { + process = Runtime.getRuntime().exec(internal ? "su -V" : "su -v", null); + process.waitFor(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (InterruptedException e) { + e.printStackTrace(); + return null; + } + + // From libsuperuser:StreamGobbler + List stdout = new ArrayList(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + try { + String line = null; + while ((line = reader.readLine()) != null) { + stdout.add(line); + } + } catch (IOException e) { + } + // make sure our stream is closed and resources will be freed + try { + reader.close(); + } catch (IOException e) { + } + + process.destroy(); + + List ret = stdout; + + if (ret != null) { + for (String line : ret) { + if (!internal) { + if (line.contains(".")) { + version = line; + break; + } + } else { + try { + if (Integer.parseInt(line) > 0) { + version = line; + break; + } + } catch (NumberFormatException e) { + } + } + } + } + + suVersion[idx] = version; + } + return suVersion[idx]; + } + + public static boolean isShellOpen() { + return Shell.shell == null; + } + + public static boolean isCustomShellOpen() { + return Shell.customShell == null; + } + + public static boolean isRootShellOpen() { + return Shell.rootShell == null; + } + + public static boolean isAnyShellOpen() { + return Shell.shell != null || Shell.rootShell != null || Shell.customShell != null; + } + + /** + * From libsuperuser. + * + * Detect if SELinux is set to enforcing, caches result + * + * @return true if SELinux set to enforcing, or false in the case of + * permissive or not present + */ + public synchronized boolean isSELinuxEnforcing() { + if (isSELinuxEnforcing == null) { + Boolean enforcing = null; + + // First known firmware with SELinux built-in was a 4.2 (17) + // leak + if (android.os.Build.VERSION.SDK_INT >= 17) { + + // Detect enforcing through sysfs, not always present + File f = new File("/sys/fs/selinux/enforce"); + if (f.exists()) { + try { + InputStream is = new FileInputStream("/sys/fs/selinux/enforce"); + try { + enforcing = (is.read() == '1'); + } finally { + is.close(); + } + } catch (Exception e) { + } + } + + // 4.4+ builds are enforcing by default, take the gamble + if (enforcing == null) { + enforcing = (android.os.Build.VERSION.SDK_INT >= 19); + } + } + + if (enforcing == null) { + enforcing = false; + } + + isSELinuxEnforcing = enforcing; + } + return isSELinuxEnforcing; + } + + /** + * Runnable to write commands to the open shell. + *

+ * When writing commands we stay in a loop and wait for new + * commands to added to "commands" + *

+ * The notification of a new command is handled by the method add in this class + */ + private Runnable input = new Runnable() { + public void run() { + + try { + while (true) { + + synchronized (commands) { + /** + * While loop is used in the case that notifyAll is called + * and there are still no commands to be written, a rare + * case but one that could happen. + */ + while (!close && write >= commands.size()) { + isExecuting = false; + commands.wait(); + } + } + + if (write >= maxCommands) { + + /** + * wait for the read to catch up. + */ + while (read != write) { + RootShell.log("Waiting for read and write to catch up before cleanup."); + } + /** + * Clean up the commands, stay neat. + */ + cleanCommands(); + } + + /** + * Write the new command + * + * We write the command followed by the token to indicate + * the end of the command execution + */ + if (write < commands.size()) { + isExecuting = true; + Command cmd = commands.get(write); + + if(null != cmd) + { + cmd.startExecution(); + RootShell.log("Executing: " + cmd.getCommand() + " with context: " + shellContext); + + //write the command + outputStream.write(cmd.getCommand()); + outputStream.flush(); + + //write the token... + String line = "\necho " + token + " " + totalExecuted + " $?\n"; + outputStream.write(line); + outputStream.flush(); + + write++; + totalExecuted++; + } + } else if (close) { + /** + * close the thread, the shell is closing. + */ + isExecuting = false; + outputStream.write("\nexit 0\n"); + outputStream.flush(); + RootShell.log("Closing shell"); + return; + } + } + } catch (IOException | InterruptedException e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } + finally { + write = 0; + closeQuietly(outputStream); + } + } + }; + + protected void notifyThreads() { + Thread t = new Thread() { + public void run() { + synchronized (commands) { + commands.notifyAll(); + } + } + }; + + t.start(); + } + + /** + * Runnable to monitor the responses from the open shell. + * + * This include the output and error stream + */ + private Runnable output = new Runnable() { + public void run() { + try { + Command command = null; + + //as long as there is something to read, we will keep reading. + while (!close || inputStream.ready() || read < commands.size()) { + isReading = false; + String outputLine = inputStream.readLine(); + isReading = true; + + /** + * If we receive EOF then the shell closed? + */ + if (outputLine == null) { + break; + } + + if (command == null) { + if (read >= commands.size()) { + if (close) { + break; + } + + continue; + } + + command = commands.get(read); + } + + /** + * trying to determine if all commands have been completed. + * + * if the token is present then the command has finished execution. + */ + int pos = -1; + + pos = outputLine.indexOf(token); + + if (pos == -1) { + /** + * send the output for the implementer to process + */ + command.output(command.id, outputLine); + } else if (pos > 0) { + /** + * token is suffix of output, send output part to implementer + */ + RootShell.log("Found token, line: " + outputLine); + command.output(command.id, outputLine.substring(0, pos)); + } + + if (pos >= 0) { + outputLine = outputLine.substring(pos); + String fields[] = outputLine.split(" "); + + if (fields.length >= 2 && fields[1] != null) { + int id = 0; + + try { + id = Integer.parseInt(fields[1]); + } catch (NumberFormatException e) { + } + + int exitCode = -1; + + try { + exitCode = Integer.parseInt(fields[2]); + } catch (NumberFormatException e) { + } + + if (id == totalRead) { + processErrors(command); + + + /** + * wait for output to be processed... + * + */ + int iterations = 0; + while (command.totalOutput > command.totalOutputProcessed) { + + if(iterations == 0) + { + iterations++; + RootShell.log("Waiting for output to be processed. " + command.totalOutputProcessed + " Of " + command.totalOutput); + } + + try { + + synchronized (this) + { + this.wait(2000); + } + } catch (Exception e) { + RootShell.log(e.getMessage()); + } + } + + RootShell.log("Read all output"); + + command.setExitCode(exitCode); + command.commandFinished(); + + command = null; + + read++; + totalRead++; + continue; + } + } + } + } + + try { + proc.waitFor(); + proc.destroy(); + } catch (Exception e) { + } + + while (read < commands.size()) { + if (command == null) { + command = commands.get(read); + } + + if(command.totalOutput < command.totalOutputProcessed) + { + command.terminated("All output not processed!"); + command.terminated("Did you forget the super.commandOutput call or are you waiting on the command object?"); + } + else + { + command.terminated("Unexpected Termination."); + } + + command = null; + read++; + } + + read = 0; + + } catch (IOException e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } finally { + closeQuietly(outputStream); + closeQuietly(errorStream); + closeQuietly(inputStream); + + RootShell.log("Shell destroyed"); + isClosed = true; + isReading = false; + } + } + }; + + public void processErrors(Command command) { + try { + while (errorStream.ready() && command != null) { + String line = errorStream.readLine(); + + /** + * If we recieve EOF then the shell closed? + */ + if (line == null) { + break; + } + + /** + * send the output for the implementer to process + */ + command.output(command.id, line); + } + } catch (Exception e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } + } + + public static Command runRootCommand(Command command) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell().add(command); + } + + public static Command runCommand(Command command) throws IOException, TimeoutException { + return Shell.startShell().add(command); + } + + public static Shell startRootShell() throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(0, 3); + } + + public static Shell startRootShell(int timeout) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(timeout, 3); + } + + public static Shell startRootShell(int timeout, int retry) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(timeout, Shell.defaultContext, retry); + } + + public static Shell startRootShell(int timeout, ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException { + // keep prompting the user until they accept for x amount of times... + int retries = 0; + + if (Shell.rootShell == null) { + + RootShell.log("Starting Root Shell!"); + String cmd = "su"; + while (Shell.rootShell == null) { + try { + RootShell.log("Trying to open Root Shell, attempt #" + retries); + Shell.rootShell = new Shell(cmd, ShellType.ROOT, shellContext, timeout); + } catch (IOException e) { + if (retries++ >= retry) { + RootShell.log("IOException, could not start shell"); + throw e; + } + } catch (RootDeniedException e) { + if (retries++ >= retry) { + RootShell.log("RootDeniedException, could not start shell"); + throw e; + } + } catch (TimeoutException e) { + if (retries++ >= retry) { + RootShell.log("TimeoutException, could not start shell"); + throw e; + } + } + } + } else if (Shell.rootShell.shellContext != shellContext) { + try { + RootShell.log("Context is different than open shell, switching context... " + Shell.rootShell.shellContext + " VS " + shellContext); + Shell.rootShell.switchRootShellContext(shellContext); + } catch (IOException e) { + if (retries++ >= retry) { + RootShell.log("IOException, could not switch context!"); + throw e; + } + } catch (RootDeniedException e) { + if (retries++ >= retry) { + RootShell.log("RootDeniedException, could not switch context!"); + throw e; + } + } catch (TimeoutException e) { + if (retries++ >= retry) { + RootShell.log("TimeoutException, could not switch context!"); + throw e; + } + } + } else { + RootShell.log("Using Existing Root Shell!"); + } + + return Shell.rootShell; + } + + public static Shell startCustomShell(String shellPath) throws IOException, TimeoutException, RootDeniedException { + return Shell.startCustomShell(shellPath, 0); + } + + public static Shell startCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException { + + if (Shell.customShell == null) { + RootShell.log("Starting Custom Shell!"); + Shell.customShell = new Shell(shellPath, ShellType.CUSTOM, ShellContext.NORMAL, timeout); + } else { + RootShell.log("Using Existing Custom Shell!"); + } + + return Shell.customShell; + } + + public static Shell startShell() throws IOException, TimeoutException { + return Shell.startShell(0); + } + + public static Shell startShell(int timeout) throws IOException, TimeoutException { + + try { + if (Shell.shell == null) { + RootShell.log("Starting Shell!"); + Shell.shell = new Shell("/system/bin/sh", ShellType.NORMAL, ShellContext.NORMAL, timeout); + } else { + RootShell.log("Using Existing Shell!"); + } + return Shell.shell; + } catch (RootDeniedException e) { + //Root Denied should never be thrown. + throw new IOException(); + } + } + + public Shell switchRootShellContext(ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + if (this.shellType == ShellType.ROOT) { + try { + Shell.closeRootShell(); + } catch (Exception e) { + RootShell.log("Problem closing shell while trying to switch context..."); + } + + //create new root shell with new context... + + return Shell.startRootShell(this.shellTimeout, shellContext, 3); + } else { + //can only switch context on a root shell... + RootShell.log("Can only switch context on a root shell!"); + return this; + } + } + + protected static class Worker extends Thread { + + public int exit = -911; + + public Shell shell; + + private Worker(Shell shell) { + this.shell = shell; + } + + public void run() { + + /** + * Trying to open the shell. + * + * We echo "Started" and we look for it in the output. + * + * If we find the output then the shell is open and we return. + * + * If we do not find it then we determine the error and report + * it by setting the value of the variable exit + */ + try { + shell.outputStream.write("echo Started\n"); + shell.outputStream.flush(); + + while (true) { + String line = shell.inputStream.readLine(); + + if (line == null) { + throw new EOFException(); + } else if ("".equals(line)) { + continue; + } else if ("Started".equals(line)) { + this.exit = 1; + setShellOom(); + break; + } + + shell.error = "unknown error occurred."; + } + } catch (IOException e) { + exit = -42; + if (e.getMessage() != null) { + shell.error = e.getMessage(); + } else { + shell.error = "RootAccess denied?."; + } + } + + } + + /* + * setOom for shell processes (sh and su if root shell) and discard outputs + * Negative values make the process LESS likely to be killed in an OOM situation + * Positive values make the process MORE likely to be killed in an OOM situation + */ + private void setShellOom() { + try { + Class processClass = shell.proc.getClass(); + Field field; + try { + field = processClass.getDeclaredField("pid"); + } catch (NoSuchFieldException e) { + field = processClass.getDeclaredField("id"); + } + field.setAccessible(true); + int pid = (Integer) field.get(shell.proc); + shell.outputStream.write("(echo -17 > /proc/" + pid + "/oom_adj) &> /dev/null\n"); + shell.outputStream.write("(echo -17 > /proc/$$/oom_adj) &> /dev/null\n"); + shell.outputStream.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/wrappers/android/mynteye/settings.gradle b/wrappers/android/mynteye/settings.gradle index f04decd..7804b01 100644 --- a/wrappers/android/mynteye/settings.gradle +++ b/wrappers/android/mynteye/settings.gradle @@ -1 +1 @@ -include ':app', ':libmynteye' +include ':app', ':libmynteye', ':libshell' From d673a151c961cee72d0f38a561bdd04bd5bd23bf Mon Sep 17 00:00:00 2001 From: John Zhao Date: Wed, 16 Jan 2019 19:42:00 +0800 Subject: [PATCH 06/21] fix(android): fix call complete event --- .../main/java/com/slightech/mynteye/demo/ui/MainActivity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java index 23abf24..2eec261 100644 --- a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java +++ b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java @@ -49,6 +49,7 @@ public class MainActivity extends BaseActivity { private void actionOpen(final Runnable completeEvent) { if (!RootUtils.isRooted()) { + if (completeEvent != null) completeEvent.run(); alert("Warning", "Root denied :("); return; } From 5eb3174edb5a13216a8da9ea457ae2c81608ee4b Mon Sep 17 00:00:00 2001 From: John Zhao Date: Thu, 17 Jan 2019 11:58:55 +0800 Subject: [PATCH 07/21] refactor(android): move djinni jni files to thrid_party --- .../android/mynteye/libmynteye/CMakeLists.txt | 8 +- .../djinni/support-lib/djinni_common.hpp | 33 + .../djinni/support-lib/jni/Marshal.hpp | 536 ++++++++++++++ .../djinni/support-lib/jni/djinni_main.cpp | 31 + .../djinni/support-lib/jni/djinni_support.cpp | 686 ++++++++++++++++++ .../djinni/support-lib/jni/djinni_support.hpp | 658 +++++++++++++++++ .../djinni/support-lib/proxy_cache_impl.hpp | 176 +++++ .../support-lib/proxy_cache_interface.hpp | 185 +++++ 8 files changed, 2312 insertions(+), 1 deletion(-) create mode 100644 wrappers/android/mynteye/third_party/djinni/support-lib/djinni_common.hpp create mode 100644 wrappers/android/mynteye/third_party/djinni/support-lib/jni/Marshal.hpp create mode 100644 wrappers/android/mynteye/third_party/djinni/support-lib/jni/djinni_main.cpp create mode 100644 wrappers/android/mynteye/third_party/djinni/support-lib/jni/djinni_support.cpp create mode 100644 wrappers/android/mynteye/third_party/djinni/support-lib/jni/djinni_support.hpp create mode 100644 wrappers/android/mynteye/third_party/djinni/support-lib/proxy_cache_impl.hpp create mode 100644 wrappers/android/mynteye/third_party/djinni/support-lib/proxy_cache_interface.hpp diff --git a/wrappers/android/mynteye/libmynteye/CMakeLists.txt b/wrappers/android/mynteye/libmynteye/CMakeLists.txt index 31afdd5..4f84689 100644 --- a/wrappers/android/mynteye/libmynteye/CMakeLists.txt +++ b/wrappers/android/mynteye/libmynteye/CMakeLists.txt @@ -8,11 +8,17 @@ cmake_minimum_required(VERSION 3.4.1) get_filename_component(MYNTETE_ROOT "${PROJECT_SOURCE_DIR}/../../../.." ABSOLUTE) message(STATUS "MYNTETE_ROOT: ${MYNTETE_ROOT}") +get_filename_component(PRO_ROOT "${PROJECT_SOURCE_DIR}/.." ABSOLUTE) +message(STATUS "PRO_ROOT: ${PRO_ROOT}") + +set(LIB_ROOT "${PROJECT_SOURCE_DIR}") +message(STATUS "LIB_ROOT: ${LIB_ROOT}") + if(NOT DJINNI_DIR) if(DEFINED ENV{DJINNI_DIR}) set(DJINNI_DIR $ENV{DJINNI_DIR}) else() - set(DJINNI_DIR "$ENV{HOME}/Workspace/Fever/Dropbox/djinni") + set(DJINNI_DIR "${PRO_ROOT}/third_party/djinni") endif() endif() diff --git a/wrappers/android/mynteye/third_party/djinni/support-lib/djinni_common.hpp b/wrappers/android/mynteye/third_party/djinni/support-lib/djinni_common.hpp new file mode 100644 index 0000000..0892a32 --- /dev/null +++ b/wrappers/android/mynteye/third_party/djinni/support-lib/djinni_common.hpp @@ -0,0 +1,33 @@ +// +// Copyright 2015 Dropbox, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#pragma once + +#ifdef _MSC_VER + #define DJINNI_WEAK_DEFINITION // weak attribute not supported by MSVC + #define DJINNI_NORETURN_DEFINITION __declspec(noreturn) + #if _MSC_VER < 1900 // snprintf not implemented prior to VS2015 + #define DJINNI_SNPRINTF snprintf + #define noexcept _NOEXCEPT // work-around for missing noexcept VS2015 + #define constexpr // work-around for missing constexpr VS2015 + #else + #define DJINNI_SNPRINTF _snprintf + #endif +#else + #define DJINNI_WEAK_DEFINITION __attribute__((weak)) + #define DJINNI_NORETURN_DEFINITION __attribute__((noreturn)) + #define DJINNI_SNPRINTF snprintf +#endif diff --git a/wrappers/android/mynteye/third_party/djinni/support-lib/jni/Marshal.hpp b/wrappers/android/mynteye/third_party/djinni/support-lib/jni/Marshal.hpp new file mode 100644 index 0000000..fcced9b --- /dev/null +++ b/wrappers/android/mynteye/third_party/djinni/support-lib/jni/Marshal.hpp @@ -0,0 +1,536 @@ +// +// Copyright 2014 Dropbox, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#pragma once + +#include "djinni_support.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace djinni +{ + template + class Primitive + { + public: + using CppType = CppT; + using JniType = JniT; + + static CppType toCpp(JNIEnv* /*jniEnv*/, JniType j) noexcept { return static_cast(j); } + static JniType fromCpp(JNIEnv* /*jniEnv*/, CppType c) noexcept { return static_cast(c); } + + struct Boxed + { + using JniType = jobject; + static CppType toCpp(JNIEnv* jniEnv, JniType j) + { + assert(j != nullptr); + const auto& data = JniClass::get(); + assert(jniEnv->IsInstanceOf(j, data.clazz.get())); + auto ret = Primitive::toCpp(jniEnv, Self::unbox(jniEnv, data.method_unbox, j)); + jniExceptionCheck(jniEnv); + return ret; + } + static LocalRef fromCpp(JNIEnv* jniEnv, CppType c) + { + const auto& data = JniClass::get(); + auto ret = jniEnv->CallStaticObjectMethod(data.clazz.get(), data.method_box, Primitive::fromCpp(jniEnv, c)); + jniExceptionCheck(jniEnv); + return {jniEnv, ret}; + } + }; + + protected: + Primitive(const char* javaClassSpec, + const char* staticBoxMethod, + const char* staticBoxMethodSignature, + const char* unboxMethod, + const char* unboxMethodSignature) + : clazz(jniFindClass(javaClassSpec)) + , method_box(jniGetStaticMethodID(clazz.get(), staticBoxMethod, staticBoxMethodSignature)) + , method_unbox(jniGetMethodID(clazz.get(), unboxMethod, unboxMethodSignature)) + {} + + private: + const GlobalRef clazz; + const jmethodID method_box; + const jmethodID method_unbox; + }; + + class Bool : public Primitive + { + Bool() : Primitive("java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", "booleanValue", "()Z") {} + friend JniClass; + friend Primitive; + static JniType unbox(JNIEnv* jniEnv, jmethodID method, jobject j) { + auto result = jniEnv->CallBooleanMethod(j, method); + jniExceptionCheck(jniEnv); + return result; + } + }; + + class I8 : public Primitive + { + I8() : Primitive("java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", "byteValue", "()B") {} + friend JniClass; + friend Primitive; + static JniType unbox(JNIEnv* jniEnv, jmethodID method, jobject j) { + auto result = jniEnv->CallByteMethod(j, method); + jniExceptionCheck(jniEnv); + return result; + } + }; + + class I16 : public Primitive + { + I16() : Primitive("java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", "shortValue", "()S") {} + friend JniClass; + friend Primitive; + static JniType unbox(JNIEnv* jniEnv, jmethodID method, jobject j) { + auto result = jniEnv->CallShortMethod(j, method); + jniExceptionCheck(jniEnv); + return result; + } + }; + + class I32 : public Primitive + { + I32() : Primitive("java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", "intValue", "()I") {} + friend JniClass; + friend Primitive; + static JniType unbox(JNIEnv* jniEnv, jmethodID method, jobject j) { + auto result = jniEnv->CallIntMethod(j, method); + jniExceptionCheck(jniEnv); + return result; + } + }; + + class I64 : public Primitive + { + I64() : Primitive("java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", "longValue", "()J") {} + friend JniClass; + friend Primitive; + static JniType unbox(JNIEnv* jniEnv, jmethodID method, jobject j) { + auto result = jniEnv->CallLongMethod(j, method); + jniExceptionCheck(jniEnv); + return result; + } + }; + + class F32 : public Primitive + { + F32() : Primitive("java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", "floatValue", "()F") {} + friend JniClass; + friend Primitive; + static JniType unbox(JNIEnv* jniEnv, jmethodID method, jobject j) { + auto result = jniEnv->CallFloatMethod(j, method); + jniExceptionCheck(jniEnv); + return result; + } + }; + + class F64 : public Primitive + { + F64() : Primitive("java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", "doubleValue", "()D") {} + friend JniClass; + friend Primitive; + static JniType unbox(JNIEnv* jniEnv, jmethodID method, jobject j) { + auto result = jniEnv->CallDoubleMethod(j, method); + jniExceptionCheck(jniEnv); + return result; + } + }; + + struct String + { + using CppType = std::string; + using JniType = jstring; + + using Boxed = String; + + static CppType toCpp(JNIEnv* jniEnv, JniType j) + { + assert(j != nullptr); + return jniUTF8FromString(jniEnv, j); + } + + static LocalRef fromCpp(JNIEnv* jniEnv, const CppType& c) + { + return {jniEnv, jniStringFromUTF8(jniEnv, c)}; + } + }; + + struct WString + { + using CppType = std::wstring; + using JniType = jstring; + + using Boxed = WString; + + static CppType toCpp(JNIEnv* jniEnv, JniType j) + { + assert(j != nullptr); + return jniWStringFromString(jniEnv, j); + } + + static LocalRef fromCpp(JNIEnv* jniEnv, const CppType& c) + { + return {jniEnv, jniStringFromWString(jniEnv, c)}; + } + }; + + struct Binary + { + using CppType = std::vector; + using JniType = jbyteArray; + + using Boxed = Binary; + + static CppType toCpp(JNIEnv* jniEnv, JniType j) + { + assert(j != nullptr); + + std::vector ret; + jsize length = jniEnv->GetArrayLength(j); + jniExceptionCheck(jniEnv); + + if (!length) { + return ret; + } + + { + auto deleter = [jniEnv, j] (void* c) { + if (c) { + jniEnv->ReleasePrimitiveArrayCritical(j, c, JNI_ABORT); + } + }; + + std::unique_ptr ptr( + reinterpret_cast(jniEnv->GetPrimitiveArrayCritical(j, nullptr)), + deleter + ); + + if (ptr) { + // Construct and then move-assign. This copies the elements only once, + // and avoids having to initialize before filling (as with resize()) + ret = std::vector{ptr.get(), ptr.get() + length}; + } else { + // Something failed... + jniExceptionCheck(jniEnv); + } + } + + return ret; + } + + static LocalRef fromCpp(JNIEnv* jniEnv, const CppType& c) + { + assert(c.size() <= std::numeric_limits::max()); + auto j = LocalRef(jniEnv, jniEnv->NewByteArray(static_cast(c.size()))); + jniExceptionCheck(jniEnv); + // Using .data() on an empty vector is UB + if(!c.empty()) + { + jniEnv->SetByteArrayRegion(j.get(), 0, jsize(c.size()), reinterpret_cast(c.data())); + } + return j; + } + }; + + struct Date + { + using CppType = std::chrono::system_clock::time_point; + using JniType = jobject; + + using Boxed = Date; + + static CppType toCpp(JNIEnv* jniEnv, JniType j) + { + static const auto POSIX_EPOCH = std::chrono::system_clock::from_time_t(0); + assert(j != nullptr); + const auto & data = JniClass::get(); + assert(jniEnv->IsInstanceOf(j, data.clazz.get())); + auto time_millis = jniEnv->CallLongMethod(j, data.method_get_time); + jniExceptionCheck(jniEnv); + return POSIX_EPOCH + std::chrono::milliseconds{time_millis}; + } + + static LocalRef fromCpp(JNIEnv* jniEnv, const CppType& c) + { + static const auto POSIX_EPOCH = std::chrono::system_clock::from_time_t(0); + const auto & data = JniClass::get(); + const auto cpp_millis = std::chrono::duration_cast(c - POSIX_EPOCH); + const jlong millis = static_cast(cpp_millis.count()); + auto j = LocalRef(jniEnv, jniEnv->NewObject(data.clazz.get(), data.constructor, millis)); + jniExceptionCheck(jniEnv); + return j; + } + + private: + Date() = default; + friend ::djinni::JniClass; + + const GlobalRef clazz { jniFindClass("java/util/Date") }; + const jmethodID constructor { jniGetMethodID(clazz.get(), "", "(J)V") }; + const jmethodID method_get_time { jniGetMethodID(clazz.get(), "getTime", "()J") }; + }; + + template