├── .eslintignore ├── .gitignore ├── .prettierignore ├── CONTRIBUTING.md ├── NativescriptCapacitor.podspec ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── proguard-rules.pro ├── settings.gradle └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── getcapacitor │ │ └── android │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── org │ │ │ └── nativescript │ │ │ └── capacitor │ │ │ ├── NativeScriptCap.java │ │ │ ├── NativeScriptCapPlugin.java │ │ │ └── NativeScriptCapPluginListener.java │ └── res │ │ └── .gitkeep │ └── test │ └── java │ └── com │ └── getcapacitor │ └── ExampleUnitTest.java ├── bin ├── build-nativescript.mjs ├── dev-nativescript.mjs └── uninstall-nativescript.mjs ├── embed ├── android │ ├── app-gradle-helpers │ │ ├── AnalyticsCollector.gradle │ │ ├── BuildToolTask.gradle │ │ └── CustomExecutionLogger.gradle │ ├── build-tools │ │ ├── android-metadata-generator.jar │ │ ├── dts-generator.jar │ │ ├── jsparser │ │ │ └── js_parser.js │ │ └── static-binding-generator.jar │ ├── debug │ │ ├── java │ │ │ └── com │ │ │ │ └── tns │ │ │ │ ├── ErrorReport.java │ │ │ │ ├── ErrorReportActivity.java │ │ │ │ ├── NativeScriptSyncService.java │ │ │ │ └── NativeScriptSyncServiceSocketImpl.java │ │ └── res │ │ │ └── layout │ │ │ ├── error_activity.xml │ │ │ ├── exception_tab.xml │ │ │ └── logcat_tab.xml │ ├── gradle-helpers │ │ ├── paths.gradle │ │ └── user_properties_reader.gradle │ ├── internal │ │ ├── livesync.js │ │ └── ts_helpers.js │ ├── libs │ │ └── runtime-libs │ │ │ ├── nativescript-optimized-with-inspector.aar │ │ │ ├── nativescript-optimized.aar │ │ │ └── nativescript-regular.aar │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── tns │ │ │ ├── AndroidJsV8Inspector.java │ │ │ ├── DefaultExtractPolicy.java │ │ │ ├── EqualityComparator.java │ │ │ ├── LogcatLogger.java │ │ │ ├── NativeScriptApplication.java │ │ │ ├── NativeScriptUncaughtExceptionHandler.java │ │ │ ├── RuntimeHelper.java │ │ │ ├── Util.java │ │ │ └── internal │ │ │ ├── AppBuilderCallback.java │ │ │ └── Plugin.java │ ├── nativescript.build.gradle │ └── nativescript.buildscript.gradle └── ios │ └── NativeScript │ ├── App-Bridging-Header.h │ ├── Runtime.h │ └── Runtime.m ├── ios ├── Plugin.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── Plugin.xcscheme │ │ └── PluginTests.xcscheme ├── Plugin.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Plugin │ ├── Info.plist │ ├── NativeScriptCap.swift │ ├── NativeScriptCapPlugin.h │ ├── NativeScriptCapPlugin.m │ └── NativeScriptCapPlugin.swift ├── PluginTests │ ├── Info.plist │ └── NativeScriptCapPluginTests.swift ├── Podfile └── nativescript.rb ├── package.json ├── rollup.config.js ├── src ├── definitions.ts ├── index.mts ├── nativeapi.d.ts ├── nativescript │ ├── examples │ │ └── modal.ts │ ├── index.ts │ ├── package.json │ ├── references.d.ts │ └── tsconfig.json ├── postinstall.mts └── web.ts ├── src_bridge ├── index.ts ├── legacy-ns-transform-native-classes.js ├── native-custom.d.ts ├── package.json ├── references.d.ts ├── tsconfig.json ├── webpack.config.js └── webpack4.config.js ├── testing ├── browser.ts ├── nativeInterface.ts ├── test.htm ├── testingInterface.ts └── tsconfig.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node files 2 | dist 3 | node_modules 4 | *.tgz 5 | package-lock.json 6 | .npmrc 7 | 8 | # iOS files 9 | Pods 10 | Podfile.lock 11 | Build 12 | xcuserdata 13 | 14 | # macOS files 15 | .DS_Store 16 | 17 | 18 | 19 | # Based on Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore 20 | 21 | # Built application files 22 | *.apk 23 | *.ap_ 24 | 25 | # Files for the ART/Dalvik VM 26 | *.dex 27 | 28 | # Java class files 29 | *.class 30 | 31 | # Generated files 32 | gen 33 | out 34 | bridge 35 | 36 | # Gradle files 37 | .gradle 38 | build 39 | 40 | # Local configuration file (sdk path, etc) 41 | local.properties 42 | 43 | # Proguard folder generated by Eclipse 44 | proguard 45 | 46 | # Log Files 47 | *.log 48 | 49 | # Android Studio Navigation editor temp files 50 | .navigation 51 | 52 | # Android Studio captures folder 53 | captures 54 | 55 | # IntelliJ 56 | *.iml 57 | .idea 58 | 59 | # Keystore files 60 | # Uncomment the following line if you do not want to check your keystore files in. 61 | #*.jks 62 | 63 | # External native build folder generated in Android Studio 2.2 and later 64 | .externalNativeBuild 65 | 66 | # Executables 67 | !bin/build-nativescript.js 68 | !bin/dev-nativescript.js 69 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This guide provides instructions for contributing to this Capacitor plugin. 4 | 5 | ## Developing 6 | 7 | ### Local Setup 8 | 9 | 1. Fork and clone the repo. 10 | 1. Setup the workspace. 11 | 12 | ```shell 13 | npm run setup 14 | ``` 15 | 16 | 1. Install SwiftLint if you're on macOS. 17 | 18 | ```shell 19 | brew install swiftlint 20 | ``` 21 | 22 | ### Scripts 23 | 24 | #### `npm run build` 25 | 26 | Build the plugin web assets and generate plugin API documentation using [`@capacitor/docgen`](https://github.com/ionic-team/capacitor-docgen). 27 | 28 | It will compile the TypeScript code from `src/` into ESM JavaScript in `dist/esm/`. These files are used in apps with bundlers when your plugin is imported. 29 | 30 | Then, Rollup will bundle the code into a single file at `dist/plugin.js`. This file is used in apps without bundlers by including it as a script in `index.html`. 31 | 32 | #### `npm run verify` 33 | 34 | Build and validate the web and native projects. 35 | 36 | This is useful to run in CI to verify that the plugin builds for all platforms. 37 | 38 | #### `npm run lint` / `npm run fmt` 39 | 40 | Check formatting and code quality, autoformat/autofix if possible. 41 | 42 | This template is integrated with ESLint, Prettier, and SwiftLint. Using these tools is completely optional, but the [Capacitor Community](https://github.com/capacitor-community/) strives to have consistent code style and structure for easier cooperation. 43 | 44 | ## Publishing 45 | 46 | There is a `prepublishOnly` hook in `package.json` which prepares the plugin before publishing, so all you need to do is run: 47 | 48 | ```shell 49 | npm publish 50 | ``` 51 | 52 | > **Note**: The [`files`](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#files) array in `package.json` specifies which files get published. If you rename files/directories or add files elsewhere, you may need to update it. 53 | -------------------------------------------------------------------------------- /NativescriptCapacitor.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = 'NativescriptCapacitor' 7 | s.version = package['version'] 8 | s.summary = package['description'] 9 | s.license = package['license'] 10 | s.homepage = package['repository']['url'] 11 | s.author = package['author'] 12 | s.source = { :git => package['repository']['url'], :tag => s.version.to_s } 13 | s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}' 14 | s.ios.deployment_target = '13.0' 15 | s.dependency 'Capacitor' 16 | s.dependency 'NativeScriptSDK', '~> 8.4.2' 17 | s.dependency 'NativeScriptUI', '~> 0.1.2' 18 | s.swift_version = '5.1' 19 | end 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @nativescript/capacitor 2 | 3 | NativeScript for Capacitor 4 | 5 | ## Install 6 | 7 | ```bash 8 | yarn add @nativescript/capacitor 9 | yarn build:mobile 10 | npx cap sync 11 | ``` 12 | 13 | ## Docs 14 | 15 | See https://capacitor.nativescript.org/ 16 | 17 | Contribute to docs [here](https://github.com/NativeScript/capacitor-docs) 18 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.12' 3 | androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.1.0' 4 | androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.1' 5 | androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.2.0' 6 | } 7 | 8 | buildscript { 9 | repositories { 10 | google() 11 | jcenter() 12 | } 13 | dependencies { 14 | classpath 'com.android.tools.build:gradle:4.1.1' 15 | } 16 | } 17 | 18 | apply plugin: 'com.android.library' 19 | 20 | android { 21 | compileSdkVersion project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 30 22 | defaultConfig { 23 | minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 21 24 | targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 30 25 | versionCode 1 26 | versionName "1.0" 27 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 28 | } 29 | buildTypes { 30 | release { 31 | minifyEnabled false 32 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 33 | } 34 | } 35 | lintOptions { 36 | abortOnError false 37 | } 38 | compileOptions { 39 | sourceCompatibility JavaVersion.VERSION_1_8 40 | targetCompatibility JavaVersion.VERSION_1_8 41 | } 42 | } 43 | 44 | repositories { 45 | google() 46 | jcenter() 47 | mavenCentral() 48 | } 49 | 50 | 51 | dependencies { 52 | implementation fileTree(dir: 'libs', include: ['*.jar']) 53 | implementation project(':capacitor-android') 54 | implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" 55 | testImplementation "junit:junit:$junitVersion" 56 | androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" 57 | androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" 58 | } 59 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # AndroidX package structure to make it clearer which packages are bundled with the 20 | # Android operating system, and which are packaged with your app's APK 21 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 22 | android.useAndroidX=true 23 | # Automatically convert third-party libraries to use AndroidX 24 | android.enableJetifier=true 25 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NativeScript/capacitor/203b85f65af1c7e2a6bb16f3de2d369c9f4bda44/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':capacitor' 2 | project(':capacitor').projectDir = file("/Users/teodordermendzhiev/workspace/capacitor-integration-demo/capacitor/android/capacitor") -------------------------------------------------------------------------------- /android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.android; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import android.content.Context; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | import androidx.test.platform.app.InstrumentationRegistry; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * @see Testing documentation 15 | */ 16 | @RunWith(AndroidJUnit4.class) 17 | public class ExampleInstrumentedTest { 18 | 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 23 | 24 | assertEquals("com.getcapacitor.android", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/java/org/nativescript/capacitor/NativeScriptCap.java: -------------------------------------------------------------------------------- 1 | package org.nativescript.capacitor; 2 | 3 | public class NativeScriptCap { } 4 | -------------------------------------------------------------------------------- /android/src/main/java/org/nativescript/capacitor/NativeScriptCapPlugin.java: -------------------------------------------------------------------------------- 1 | package org.nativescript.capacitor; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import com.getcapacitor.Plugin; 6 | import com.getcapacitor.PluginCall; 7 | import com.getcapacitor.PluginMethod; 8 | import com.getcapacitor.annotation.CapacitorPlugin; 9 | 10 | @CapacitorPlugin(name = "NativeScriptCap") 11 | public class NativeScriptCapPlugin extends Plugin { 12 | @Nullable 13 | public static NativeScriptCapPluginListener listener = null; 14 | 15 | @Override 16 | public void load() { 17 | super.load(); 18 | if (listener != null) { 19 | listener.setup(this); 20 | } 21 | } 22 | 23 | @PluginMethod(returnType = PluginMethod.RETURN_NONE) 24 | public void notify(PluginCall call) { 25 | if (listener != null) { 26 | listener.notify(call.getString("value")); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/src/main/java/org/nativescript/capacitor/NativeScriptCapPluginListener.java: -------------------------------------------------------------------------------- 1 | package org.nativescript.capacitor; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | public interface NativeScriptCapPluginListener { 6 | void setup(NativeScriptCapPlugin instance); 7 | void notify(@Nullable String message); 8 | } 9 | -------------------------------------------------------------------------------- /android/src/main/res/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NativeScript/capacitor/203b85f65af1c7e2a6bb16f3de2d369c9f4bda44/android/src/main/res/.gitkeep -------------------------------------------------------------------------------- /android/src/test/java/com/getcapacitor/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.junit.Test; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | 14 | @Test 15 | public void addition_isCorrect() throws Exception { 16 | assertEquals(4, 2 + 2); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bin/build-nativescript.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { join, relative, resolve, sep } from 'path'; 3 | import { spawn } from 'child_process'; 4 | import fs from 'fs'; 5 | import stripJsonComments from 'strip-json-comments'; 6 | 7 | const [,, ...args] = process.argv; 8 | // console.log('args:', args); 9 | // console.log('process.cwd', process.cwd()); 10 | const installOnly = args && args[0] === 'install'; 11 | const projectDir = process.cwd(); 12 | const buildDir = join( 13 | projectDir, 14 | 'src', 15 | 'nativescript' 16 | ); 17 | const capacitorConfigName = 'capacitor.config.json'; 18 | const capacitorConfigPath = join(projectDir, capacitorConfigName); 19 | const capacitorConfigNameTS = 'capacitor.config.ts'; 20 | const capacitorConfigTSPath = join(projectDir, capacitorConfigNameTS); 21 | let distFolder = 'www'; // default 22 | let isReact = false; 23 | let isVue = false; 24 | let isAngular = false; 25 | let webpackInfo; 26 | 27 | const checkFramework = () => { 28 | // default is webpack5 config 29 | webpackInfo = { 30 | name: 'webpack.config.js', 31 | version: 5 32 | }; 33 | 34 | // Note: Left here in case other flavor integrations end up using older webpack 35 | // const packagePath = join(projectDir, 'package.json'); 36 | // const packageContent = fs.readFileSync(packagePath, { 37 | // encoding: 'UTF-8', 38 | // }); 39 | // if (packageContent) { 40 | // const packageJson = JSON.parse( 41 | // stripJsonComments(packageContent), 42 | // ); 43 | // if (packageJson && packageJson.dependencies) { 44 | // isReact = !!packageJson.dependencies['react']; 45 | // isVue = !!packageJson.dependencies['vue']; 46 | // if (isVue) { 47 | // webpackInfo = { 48 | // name: 'webpack4.config.js', 49 | // version: 4 50 | // }; 51 | // } else { 52 | // isAngular = true; 53 | // } 54 | // } 55 | // } 56 | }; 57 | 58 | const buildNativeScript = () => { 59 | // console.log('buildDir:', resolve(buildDir)); 60 | 61 | // console.log('using webpack config:', webpackInfo.name); 62 | 63 | // const configPath = import.meta.resolve(`@nativescript/capacitor/bridge/webpack.config.js`); 64 | 65 | // webpack 5 needs at least one platform set 66 | if (webpackInfo.version === 5) { 67 | if (args && !args.find(a => a.indexOf('env=platform') > -1)) { 68 | // use ios by default when no platform argument is present 69 | // won't matter for most users since they will often use conditional logic 70 | // but for power users that start using .ios and .android files they can split npm scripts to pass either platform for further optimized builds 71 | args.push(`--env=platform=ios`); 72 | } 73 | } 74 | // console.log('configPath:', resolve(configPath)); 75 | // console.log('configPath:', configPath) 76 | const url = new URL(`../bridge/${webpackInfo.name}`, import.meta.url); 77 | const cmdArgs = ['webpack', `--config=${url.pathname}`, ...args]; 78 | // console.log('cmdArgs:', cmdArgs); 79 | const child = spawn(`npx`, cmdArgs, { 80 | cwd: resolve(buildDir), 81 | stdio: 'inherit', 82 | shell: true, 83 | }); 84 | child.on('error', (error) => { 85 | console.log('NativeScript build error:', error); 86 | }); 87 | child.on('close', (res) => { 88 | console.log(`NativeScript build complete:`, `${distFolder}/nativescript/index.js`); 89 | child.kill(); 90 | }); 91 | }; 92 | 93 | const installTsPatch = () => { 94 | // Ensure ts-patch is installed 95 | const child = spawn(`ts-patch`, ['install'], { 96 | cwd: resolve(buildDir), 97 | stdio: 'inherit', 98 | shell: true, 99 | }); 100 | child.on('error', (error) => { 101 | console.log('NativeScript build error:', error); 102 | }); 103 | child.on('close', (res) => { 104 | child.kill(); 105 | if (!installOnly) { 106 | buildNativeScript(); 107 | } 108 | }); 109 | } 110 | 111 | const npmInstallTsPatch = () => { 112 | // Ensure ts-patch is installed 113 | const child = spawn(`npm`, ['install', '--legacy-peer-deps'], { 114 | cwd: resolve(buildDir), 115 | stdio: 'inherit', 116 | shell: true, 117 | }); 118 | child.on('error', (error) => { 119 | console.log('NativeScript build error:', error); 120 | }); 121 | child.on('close', (res) => { 122 | child.kill(); 123 | installTsPatch(); 124 | }); 125 | } 126 | 127 | const npmInstall = () => { 128 | 129 | // Note: Left in case flavor integrations need any different dependencies in future 130 | // if (isReact) { 131 | // // Exception case: React needs ts-loader ^6.2.2 installed 132 | // const child = spawn(`npm`, ['install', 'ts-loader@6.2.2', '-D'], { 133 | // cwd: resolve(buildDir), 134 | // stdio: 'inherit', 135 | // shell: true, 136 | // }); 137 | // child.on('error', (error) => { 138 | // console.log('NativeScript build error:', error); 139 | // }); 140 | // child.on('close', (res) => { 141 | // child.kill(); 142 | // installTsPatch(); 143 | // }); 144 | // } else { 145 | // Vue projects bring in ts-loader 6.2.2 through vue cli dependencies 146 | // Angular project use webpack5 and can use the latest with @nativescript/webpack 147 | // proceed as normal 148 | npmInstallTsPatch(); 149 | // } 150 | } 151 | 152 | if (!installOnly) { 153 | // Determine configured build folder name 154 | if (fs.existsSync(capacitorConfigPath)) { 155 | const capacitorConfigContent = fs.readFileSync(capacitorConfigPath, { 156 | encoding: 'UTF-8', 157 | }); 158 | if (capacitorConfigContent) { 159 | const capacitorConfigJson = JSON.parse( 160 | stripJsonComments(capacitorConfigContent), 161 | ); 162 | if (capacitorConfigJson && capacitorConfigJson.webDir) { 163 | distFolder = capacitorConfigJson.webDir; 164 | } 165 | } 166 | } else if (fs.existsSync(capacitorConfigTSPath)) { 167 | const capacitorConfigTSContent = fs.readFileSync(capacitorConfigTSPath, { 168 | encoding: 'UTF-8', 169 | }); 170 | if (capacitorConfigTSContent) { 171 | // need a AST parser here - ridumentary check for now 172 | // assuming 3 likely values: www, build or dist 173 | if (capacitorConfigTSContent.indexOf(`webDir: 'www'`) === -1) { 174 | // not using default, parse it out 175 | distFolder = capacitorConfigTSContent.split(/webDir:\s*["'`](\w+)["'`]/gim)[1]; 176 | } 177 | } 178 | } else { 179 | console.error(`NativeScript Build Error: could not find ${capacitorConfigName}`); 180 | } 181 | if (args && !args.find(a => a.indexOf('env=distFolder') > -1)) { 182 | args.push(`--env=distFolder=${distFolder}`); 183 | } 184 | } 185 | 186 | // check frontend framework details 187 | checkFramework(); 188 | // ensure various deps are installed and setup properly 189 | npmInstall(); 190 | 191 | export function init() { 192 | 193 | } -------------------------------------------------------------------------------- /bin/dev-nativescript.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import chokidar from 'chokidar'; 3 | import { execSync } from 'child_process'; 4 | import prompts from 'prompts'; 5 | import path from 'path'; 6 | import fs from 'fs'; 7 | 8 | const projectDir = process.cwd(); 9 | const buildDir = path.resolve( 10 | projectDir, 11 | 'src', 12 | 'nativescript' 13 | ); 14 | const platform = process.argv[2]; 15 | 16 | 17 | const getAvailableDevices = () => { 18 | if (platform !== 'ios' && platform !== 'android') { 19 | console.log("Please use valid platform (ios/android)") 20 | return; 21 | } 22 | let devices = []; 23 | const iosRegex = /(iOS \d+)/g; 24 | const androidRegex = /(API \d+)/g; 25 | 26 | const result = execSync(`npx cap run ${platform} --list`).toString() 27 | .split('\n') 28 | .filter(line => line.match(platform === 'ios' ? iosRegex : androidRegex)); 29 | 30 | result.forEach(device => { 31 | const id = device.split(" ").pop(); 32 | devices.push({ 33 | title: device, 34 | value: id 35 | }) 36 | }) 37 | return devices; 38 | } 39 | 40 | const selectTargetDevice = async (devicesAvailable) => { 41 | if (devicesAvailable) { 42 | const targetDevice = await prompts({ 43 | type: 'select', 44 | name: 'value', 45 | message: 'Please choose a target device', 46 | choices: devicesAvailable, 47 | initial: 0 48 | }); 49 | return targetDevice.value; 50 | } 51 | } 52 | 53 | const buildApp = () => { 54 | execSync('npm run build:mobile', {stdio: 'inherit'}); 55 | } 56 | 57 | const runOnTarget = (targetDevice) => { 58 | execSync(`npx cap run ${platform} --target ${targetDevice}`, {stdio: 'inherit'}) 59 | } 60 | 61 | const watchFiles = (targetDevice) => { 62 | buildApp() 63 | runOnTarget(targetDevice); 64 | console.log("Watching for file changes..."); 65 | 66 | chokidar.watch(`${buildDir}/**/*.ts`, { 67 | interval: 100, 68 | binaryInterval: 300, 69 | awaitWriteFinish: { 70 | stabilityThreshold: 2000, 71 | pollInterval: 100 72 | }, 73 | }).on('change', (event, path) => { 74 | console.log("File change detected."); 75 | buildApp() 76 | runOnTarget(targetDevice); 77 | console.log("Watching for file changes..."); 78 | }); 79 | } 80 | (async () => { 81 | 82 | let targetDevice = await selectTargetDevice(getAvailableDevices()); 83 | if (targetDevice) { 84 | watchFiles(targetDevice); 85 | } 86 | 87 | })(); 88 | 89 | export function init() { 90 | 91 | } 92 | -------------------------------------------------------------------------------- /bin/uninstall-nativescript.mjs: -------------------------------------------------------------------------------- 1 | import { join, relative, resolve, sep } from 'path'; 2 | import { spawn } from 'child_process'; 3 | 4 | const projectDir = process.cwd(); 5 | const cmdArgs = ['dist/esm/postinstall', `--action uninstall`]; 6 | // console.log('cmdArgs:', cmdArgs); 7 | const child = spawn(`node`, cmdArgs, { 8 | cwd: resolve(join(projectDir, 'node_modules', '@nativescript', 'capacitor')), 9 | stdio: 'inherit', 10 | shell: true, 11 | }); 12 | child.on('error', (error) => { 13 | console.log('NativeScript uninstall error:', error); 14 | }); 15 | child.on('close', (res) => { 16 | console.log(`NativeScript uninstall complete.`); 17 | console.log(`\n`); 18 | console.log(`You can now 'npm uninstall @nativescript/capacitor'`); 19 | console.log(`\n`); 20 | child.kill(); 21 | }); 22 | 23 | export function init() { 24 | 25 | } -------------------------------------------------------------------------------- /embed/android/app-gradle-helpers/AnalyticsCollector.gradle: -------------------------------------------------------------------------------- 1 | import groovy.json.JsonBuilder 2 | 3 | import java.nio.charset.StandardCharsets 4 | import java.nio.file.Files 5 | import java.nio.file.Paths 6 | import java.nio.file.Path 7 | 8 | class AnalyticsCollector{ 9 | 10 | private final String analyticsFilePath 11 | private boolean hasUseKotlinPropertyInApp = false 12 | private boolean hasKotlinRuntimeClasses = false 13 | 14 | private AnalyticsCollector(String analyticsFilePath){ 15 | this.analyticsFilePath = analyticsFilePath 16 | } 17 | 18 | static AnalyticsCollector withOutputPath(String analyticsFilePath){ 19 | return new AnalyticsCollector(analyticsFilePath) 20 | } 21 | 22 | void markUseKotlinPropertyInApp(boolean useKotlin) { 23 | hasUseKotlinPropertyInApp = useKotlin 24 | } 25 | 26 | void writeAnalyticsFile() { 27 | def jsonBuilder = new JsonBuilder() 28 | def kotlinUsageData = new Object() 29 | kotlinUsageData.metaClass.hasUseKotlinPropertyInApp = hasUseKotlinPropertyInApp 30 | kotlinUsageData.metaClass.hasKotlinRuntimeClasses = hasKotlinRuntimeClasses 31 | jsonBuilder(kotlinUsage: kotlinUsageData) 32 | def prettyJson = jsonBuilder.toPrettyString() 33 | 34 | 35 | 36 | Path statisticsFilePath = Paths.get(analyticsFilePath) 37 | 38 | if (Files.notExists(statisticsFilePath)) { 39 | Files.createDirectories(statisticsFilePath.getParent()) 40 | Files.createFile(statisticsFilePath) 41 | } 42 | 43 | Files.write(statisticsFilePath, prettyJson.getBytes(StandardCharsets.UTF_8)) 44 | 45 | } 46 | } 47 | 48 | ext.AnalyticsCollector = AnalyticsCollector 49 | -------------------------------------------------------------------------------- /embed/android/app-gradle-helpers/BuildToolTask.gradle: -------------------------------------------------------------------------------- 1 | import static org.gradle.internal.logging.text.StyledTextOutput.Style 2 | 3 | class BuildToolTask extends JavaExec { 4 | void setOutputs(def logger) { 5 | def logFile = new File("$workingDir/${name}.log") 6 | if(logFile.exists()) { 7 | logFile.delete() 8 | } 9 | standardOutput new FileOutputStream(logFile) 10 | errorOutput new FailureOutputStream(logger, logFile) 11 | } 12 | } 13 | 14 | class FailureOutputStream extends OutputStream { 15 | private logger 16 | private File logFile 17 | private currentLine = "" 18 | private firstWrite = true 19 | FailureOutputStream(inLogger, inLogFile) { 20 | logger = inLogger 21 | logFile = inLogFile 22 | } 23 | 24 | @Override 25 | void write(int i) throws IOException { 26 | if(firstWrite) { 27 | println "" 28 | firstWrite = false 29 | } 30 | currentLine += String.valueOf((char) i) 31 | } 32 | 33 | @Override 34 | void flush() { 35 | if(currentLine?.trim()) { 36 | logger.withStyle(Style.Failure).println currentLine.trim() 37 | currentLine = "" 38 | } 39 | } 40 | 41 | @Override 42 | void close() { 43 | if(!firstWrite && logFile.exists()) { 44 | logger.withStyle(Style.Info).println "Detailed log here: ${logFile.getAbsolutePath()}\n" 45 | } 46 | super.close() 47 | } 48 | } 49 | 50 | ext.BuildToolTask = BuildToolTask -------------------------------------------------------------------------------- /embed/android/app-gradle-helpers/CustomExecutionLogger.gradle: -------------------------------------------------------------------------------- 1 | import org.gradle.internal.logging.text.StyledTextOutputFactory 2 | 3 | import static org.gradle.internal.logging.text.StyledTextOutput.Style 4 | def outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") 5 | 6 | class CustomExecutionLogger extends BuildAdapter implements TaskExecutionListener { 7 | private logger 8 | private failedTask 9 | 10 | CustomExecutionLogger(passedLogger) { 11 | logger = passedLogger 12 | } 13 | 14 | void buildStarted(Gradle gradle) { 15 | failedTask = null 16 | } 17 | 18 | void beforeExecute(Task task) { 19 | } 20 | 21 | void afterExecute(Task task, TaskState state) { 22 | def failure = state.getFailure() 23 | if(failure) { 24 | failedTask = task 25 | } 26 | } 27 | 28 | void buildFinished(BuildResult result) { 29 | def failure = result.getFailure() 30 | if(failure) { 31 | if(failedTask && (failedTask.getClass().getName().contains("BuildToolTask"))) { 32 | // the error from this task is already logged 33 | return 34 | } 35 | 36 | println "" 37 | logger.withStyle(Style.FailureHeader).println failure.getMessage() 38 | 39 | def causeException = failure.getCause() 40 | while (causeException != null) { 41 | failure = causeException 42 | causeException = failure.getCause() 43 | } 44 | if(failure != causeException) { 45 | logger.withStyle(Style.Failure).println failure.getMessage() 46 | } 47 | println "" 48 | } 49 | } 50 | } 51 | 52 | gradle.useLogger(new CustomExecutionLogger(outLogger)) -------------------------------------------------------------------------------- /embed/android/build-tools/android-metadata-generator.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NativeScript/capacitor/203b85f65af1c7e2a6bb16f3de2d369c9f4bda44/embed/android/build-tools/android-metadata-generator.jar -------------------------------------------------------------------------------- /embed/android/build-tools/dts-generator.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NativeScript/capacitor/203b85f65af1c7e2a6bb16f3de2d369c9f4bda44/embed/android/build-tools/dts-generator.jar -------------------------------------------------------------------------------- /embed/android/build-tools/static-binding-generator.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NativeScript/capacitor/203b85f65af1c7e2a6bb16f3de2d369c9f4bda44/embed/android/build-tools/static-binding-generator.jar -------------------------------------------------------------------------------- /embed/android/debug/java/com/tns/ErrorReport.java: -------------------------------------------------------------------------------- 1 | package com.tns; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | import java.io.OutputStreamWriter; 10 | import java.io.PrintStream; 11 | import java.io.UnsupportedEncodingException; 12 | import java.lang.reflect.Method; 13 | import java.text.SimpleDateFormat; 14 | import java.util.Date; 15 | 16 | import android.Manifest; 17 | import android.app.Activity; 18 | import android.app.PendingIntent; 19 | import android.app.PendingIntent.CanceledException; 20 | import android.content.ClipData; 21 | import android.content.ClipboardManager; 22 | import android.content.Context; 23 | import android.content.Intent; 24 | import android.content.pm.PackageManager; 25 | import android.os.Build; 26 | import android.os.Bundle; 27 | import android.os.Environment; 28 | import com.google.android.material.tabs.TabLayout; 29 | import androidx.core.app.ActivityCompat; 30 | import androidx.fragment.app.Fragment; 31 | import androidx.fragment.app.FragmentManager; 32 | import androidx.fragment.app.FragmentStatePagerAdapter; 33 | import androidx.viewpager.widget.ViewPager; 34 | import androidx.appcompat.app.AppCompatActivity; 35 | import androidx.appcompat.widget.Toolbar; 36 | import android.text.method.ScrollingMovementMethod; 37 | import android.util.Log; 38 | import android.view.LayoutInflater; 39 | import android.view.View; 40 | import android.view.ViewGroup; 41 | import android.widget.Button; 42 | import android.widget.TextView; 43 | import android.widget.Toast; 44 | 45 | 46 | class ErrorReport implements TabLayout.OnTabSelectedListener { 47 | public static final String ERROR_FILE_NAME = "hasError"; 48 | private static AppCompatActivity activity; 49 | 50 | private TabLayout tabLayout; 51 | private ViewPager viewPager; 52 | private Context context; 53 | 54 | private static String exceptionMsg; 55 | private static String logcatMsg; 56 | 57 | private static boolean checkingForPermissions = false; 58 | 59 | private final static String EXTRA_NATIVESCRIPT_ERROR_REPORT = "NativeScriptErrorMessage"; 60 | private final static String EXTRA_ERROR_REPORT_MSG = "msg"; 61 | private final static String EXTRA_PID = "pID"; 62 | private final static int EXTRA_ERROR_REPORT_VALUE = 1; 63 | 64 | private static final int REQUEST_EXTERNAL_STORAGE = 1; 65 | private static String[] PERMISSIONS_STORAGE = { 66 | Manifest.permission.READ_EXTERNAL_STORAGE, 67 | Manifest.permission.WRITE_EXTERNAL_STORAGE 68 | }; 69 | 70 | // Will prevent error activity from killing process if permission request dialog pops up 71 | public static boolean isCheckingForPermissions() { 72 | return checkingForPermissions; 73 | } 74 | 75 | public static void resetCheckingForPermissions() { 76 | checkingForPermissions = false; 77 | } 78 | 79 | // The following will not compile if uncommented with compileSdk lower than 23 80 | public static void verifyStoragePermissions(Activity activity) { 81 | // Check if we have write permission 82 | final int version = Build.VERSION.SDK_INT; 83 | if (version >= 23) { 84 | try { 85 | // Necessary to work around compile errors with compileSdk 22 and lower 86 | Method checkSelfPermissionMethod; 87 | try { 88 | checkSelfPermissionMethod = ActivityCompat.class.getMethod("checkSelfPermission", Context.class, String.class); 89 | } catch (NoSuchMethodException e) { 90 | // method wasn't found, so there is no need to handle permissions explicitly 91 | if (Util.isDebuggableApp(activity)) { 92 | e.printStackTrace(); 93 | } 94 | return; 95 | } 96 | 97 | int permission = (int) checkSelfPermissionMethod.invoke(null, activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); 98 | 99 | if (permission != PackageManager.PERMISSION_GRANTED) { 100 | // We don't have permission so prompt the user 101 | Method requestPermissionsMethod = ActivityCompat.class.getMethod("requestPermissions", Activity.class, PERMISSIONS_STORAGE.getClass(), int.class); 102 | 103 | checkingForPermissions = true; 104 | 105 | requestPermissionsMethod.invoke(null, activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); 106 | } 107 | } catch (Exception e) { 108 | Toast.makeText(activity, "Couldn't resolve permissions", Toast.LENGTH_LONG).show(); 109 | if (Util.isDebuggableApp(activity)) { 110 | e.printStackTrace(); 111 | } 112 | return; 113 | } 114 | } 115 | } 116 | 117 | public ErrorReport(AppCompatActivity activity) { 118 | ErrorReport.activity = activity; 119 | this.context = activity.getApplicationContext(); 120 | } 121 | 122 | static boolean startActivity(final Context context, String errorMessage) { 123 | final Intent intent = getIntent(context); 124 | if (intent == null) { 125 | return false; // (if in release mode) don't do anything 126 | } 127 | 128 | intent.putExtra(EXTRA_ERROR_REPORT_MSG, errorMessage); 129 | 130 | String PID = Integer.toString(android.os.Process.myPid()); 131 | intent.putExtra(EXTRA_PID, PID); 132 | 133 | createErrorFile(context); 134 | 135 | try { 136 | startPendingErrorActivity(context, intent); 137 | } catch (CanceledException e) { 138 | Log.d("ErrorReport", "Couldn't send pending intent! Exception: " + e.getMessage()); 139 | } 140 | 141 | killProcess(context); 142 | 143 | return true; 144 | } 145 | 146 | static void killProcess(Context context) { 147 | // finish current activity and all below it first 148 | if (context instanceof Activity) { 149 | ((Activity) context).finishAffinity(); 150 | } 151 | 152 | // kill process 153 | android.os.Process.killProcess(android.os.Process.myPid()); 154 | } 155 | 156 | static void startPendingErrorActivity(Context context, Intent intent) throws CanceledException { 157 | PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 158 | 159 | pendingIntent.send(context, 0, intent); 160 | } 161 | 162 | static String getErrorMessage(Throwable ex) { 163 | String content; 164 | PrintStream ps = null; 165 | 166 | try { 167 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 168 | ps = new PrintStream(baos); 169 | ex.printStackTrace(ps); 170 | 171 | try { 172 | content = baos.toString("UTF-8"); 173 | } catch (UnsupportedEncodingException e) { 174 | content = e.getMessage(); 175 | } 176 | } finally { 177 | if (ps != null) { 178 | ps.close(); 179 | } 180 | } 181 | 182 | return content; 183 | } 184 | 185 | /* 186 | * Gets the process Id of the running app and filters all 187 | * output that doesn't belong to that process 188 | * */ 189 | public static String getLogcat(String pId) { 190 | String content; 191 | 192 | try { 193 | String logcatCommand = "logcat -d"; 194 | Process process = java.lang.Runtime.getRuntime().exec(logcatCommand); 195 | 196 | BufferedReader bufferedReader = new BufferedReader( 197 | new InputStreamReader(process.getInputStream())); 198 | 199 | StringBuilder log = new StringBuilder(); 200 | String line = ""; 201 | String lineSeparator = System.getProperty("line.separator"); 202 | while ((line = bufferedReader.readLine()) != null) { 203 | if (line.contains(pId)) { 204 | log.append(line); 205 | log.append(lineSeparator); 206 | } 207 | } 208 | 209 | content = log.toString(); 210 | } catch (IOException e) { 211 | content = "Failed to read logcat"; 212 | Log.e("TNS.Android", content); 213 | } 214 | 215 | return content; 216 | } 217 | 218 | static Intent getIntent(Context context) { 219 | Class errorActivityClass; 220 | 221 | if (Util.isDebuggableApp(context)) { 222 | errorActivityClass = ErrorReportActivity.class; 223 | } else { 224 | return null; 225 | } 226 | 227 | Intent intent = new Intent(context, errorActivityClass); 228 | 229 | intent.putExtra(EXTRA_NATIVESCRIPT_ERROR_REPORT, EXTRA_ERROR_REPORT_VALUE); 230 | intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); 231 | 232 | return intent; 233 | } 234 | 235 | static boolean hasIntent(Intent intent) { 236 | int value = intent.getIntExtra(EXTRA_NATIVESCRIPT_ERROR_REPORT, 0); 237 | 238 | return value == EXTRA_ERROR_REPORT_VALUE; 239 | } 240 | 241 | void buildUI() { 242 | Intent intent = activity.getIntent(); 243 | 244 | exceptionMsg = intent.getStringExtra(EXTRA_ERROR_REPORT_MSG); 245 | 246 | String processId = intent.getStringExtra(EXTRA_PID); 247 | logcatMsg = getLogcat(processId); 248 | 249 | int errActivityId = this.context.getResources().getIdentifier("error_activity", "layout", this.context.getPackageName()); 250 | 251 | activity.setContentView(errActivityId); 252 | 253 | int toolBarId = this.context.getResources().getIdentifier("toolbar", "id", this.context.getPackageName()); 254 | 255 | Toolbar toolbar = (Toolbar) activity.findViewById(toolBarId); 256 | activity.setSupportActionBar(toolbar); 257 | 258 | final int tabLayoutId = this.context.getResources().getIdentifier("tabLayout", "id", this.context.getPackageName()); 259 | 260 | tabLayout = (TabLayout) activity.findViewById(tabLayoutId); 261 | tabLayout.addTab(tabLayout.newTab().setText("Exception")); 262 | tabLayout.addTab(tabLayout.newTab().setText("Logcat")); 263 | tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); 264 | int pagerId = this.context.getResources().getIdentifier("pager", "id", this.context.getPackageName()); 265 | 266 | viewPager = (ViewPager) activity.findViewById(pagerId); 267 | 268 | Pager adapter = new Pager(activity.getSupportFragmentManager(), tabLayout.getTabCount()); 269 | 270 | viewPager.setAdapter(adapter); 271 | viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { 272 | @Override 273 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 274 | 275 | } 276 | 277 | @Override 278 | public void onPageSelected(int position) { 279 | tabLayout.getTabAt(position).select(); 280 | viewPager.setCurrentItem(position); 281 | } 282 | 283 | @Override 284 | public void onPageScrollStateChanged(int state) { 285 | 286 | } 287 | }); 288 | 289 | this.addOnTabSelectedListener(tabLayout); 290 | } 291 | 292 | private void addOnTabSelectedListener(TabLayout tabLayout) { 293 | tabLayout.addOnTabSelectedListener(this); 294 | } 295 | private static void createErrorFile(final Context context) { 296 | try { 297 | File errFile = new File(context.getFilesDir(), ERROR_FILE_NAME); 298 | errFile.createNewFile(); 299 | } catch (IOException e) { 300 | Log.d("ErrorReport", e.getMessage()); 301 | } 302 | } 303 | 304 | @Override 305 | public void onTabSelected(TabLayout.Tab tab) { 306 | viewPager.setCurrentItem(tab.getPosition()); 307 | } 308 | 309 | @Override 310 | public void onTabUnselected(TabLayout.Tab tab) { 311 | 312 | } 313 | 314 | @Override 315 | public void onTabReselected(TabLayout.Tab tab) { 316 | viewPager.setCurrentItem(tab.getPosition()); 317 | } 318 | 319 | private class Pager extends FragmentStatePagerAdapter { 320 | 321 | int tabCount; 322 | 323 | @SuppressWarnings("deprecation") 324 | public Pager(FragmentManager fm, int tabCount) { 325 | super(fm); 326 | this.tabCount = tabCount; 327 | } 328 | 329 | @Override 330 | public Fragment getItem(int position) { 331 | switch (position) { 332 | case 0: 333 | return new ExceptionTab(); 334 | case 1: 335 | return new LogcatTab(); 336 | default: 337 | return null; 338 | } 339 | } 340 | 341 | @Override 342 | public int getCount() { 343 | return tabCount; 344 | } 345 | } 346 | 347 | public static class ExceptionTab extends Fragment { 348 | @Override 349 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 350 | int exceptionTabId = container.getContext().getResources().getIdentifier("exception_tab", "layout", container.getContext().getPackageName()); 351 | View view = inflater.inflate(exceptionTabId, container, false); 352 | 353 | int txtViewId = container.getContext().getResources().getIdentifier("txtErrorMsg", "id", container.getContext().getPackageName()); 354 | TextView txtErrorMsg = (TextView) view.findViewById(txtViewId); 355 | txtErrorMsg.setText(exceptionMsg); 356 | txtErrorMsg.setMovementMethod(new ScrollingMovementMethod()); 357 | 358 | int btnCopyExceptionId = container.getContext().getResources().getIdentifier("btnCopyException", "id", container.getContext().getPackageName()); 359 | Button copyToClipboard = (Button) view.findViewById(btnCopyExceptionId); 360 | copyToClipboard.setOnClickListener(new View.OnClickListener() { 361 | @Override 362 | public void onClick(View v) { 363 | ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); 364 | ClipData clip = ClipData.newPlainText("nsError", exceptionMsg); 365 | clipboard.setPrimaryClip(clip); 366 | } 367 | }); 368 | 369 | return view; 370 | } 371 | } 372 | 373 | public static class LogcatTab extends Fragment { 374 | @Override 375 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 376 | int logcatTabId = container.getContext().getResources().getIdentifier("logcat_tab", "layout", container.getContext().getPackageName()); 377 | View view = inflater.inflate(logcatTabId, container, false); 378 | 379 | int textViewId = container.getContext().getResources().getIdentifier("logcatMsg", "id", container.getContext().getPackageName()); 380 | TextView txtlogcatMsg = (TextView) view.findViewById(textViewId); 381 | txtlogcatMsg.setText(logcatMsg); 382 | 383 | txtlogcatMsg.setMovementMethod(new ScrollingMovementMethod()); 384 | 385 | final String logName = "Log-" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()); 386 | 387 | int btnCopyLogcatId = container.getContext().getResources().getIdentifier("btnCopyLogcat", "id", container.getContext().getPackageName()); 388 | Button copyToClipboard = (Button) view.findViewById(btnCopyLogcatId); 389 | copyToClipboard.setOnClickListener(new View.OnClickListener() { 390 | @Override 391 | public void onClick(View v) { 392 | verifyStoragePermissions(activity); 393 | 394 | if (!isCheckingForPermissions()) { 395 | try { 396 | File dir = new File(Environment.getExternalStorageDirectory().getPath() + "/logcat-reports/"); 397 | dir.mkdirs(); 398 | 399 | File logcatReportFile = new File(dir, logName); 400 | FileOutputStream stream = new FileOutputStream(logcatReportFile); 401 | OutputStreamWriter writer = new OutputStreamWriter(stream, "UTF-8"); 402 | writer.write(logcatMsg); 403 | writer.close(); 404 | 405 | String logPath = dir.getPath() + "/" + logName; 406 | 407 | ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); 408 | ClipData clip = ClipData.newPlainText("logPath", logPath); 409 | clipboard.setPrimaryClip(clip); 410 | 411 | Toast.makeText(activity, "Path copied to clipboard: " + logPath, Toast.LENGTH_LONG).show(); 412 | } catch (Exception e) { 413 | String err = "Could not write logcat report to sdcard. Make sure you have allowed access to external storage!"; 414 | Toast.makeText(activity, err, Toast.LENGTH_LONG).show(); 415 | if (Util.isDebuggableApp(container.getContext())) { 416 | e.printStackTrace(); 417 | } 418 | } 419 | } 420 | } 421 | }); 422 | 423 | return view; 424 | } 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /embed/android/debug/java/com/tns/ErrorReportActivity.java: -------------------------------------------------------------------------------- 1 | package com.tns; 2 | 3 | import android.app.Application; 4 | import android.os.Bundle; 5 | import androidx.annotation.NonNull; 6 | import androidx.appcompat.app.AppCompatActivity; 7 | import android.widget.Toast; 8 | 9 | import java.lang.reflect.Method; 10 | 11 | import static com.tns.ErrorReport.isCheckingForPermissions; 12 | import static com.tns.ErrorReport.resetCheckingForPermissions; 13 | 14 | public class ErrorReportActivity extends AppCompatActivity { 15 | public void onCreate(Bundle savedInstanceState) { 16 | setTheme(androidx.appcompat.R.style.Theme_AppCompat_NoActionBar); 17 | 18 | super.onCreate(savedInstanceState); 19 | Application app = this.getApplication(); 20 | Logger logger = new LogcatLogger(app); 21 | 22 | RuntimeHelper.initLiveSync(null, logger, app); 23 | 24 | new ErrorReport(this).buildUI(); 25 | } 26 | 27 | @Override 28 | protected void onUserLeaveHint() { 29 | super.onUserLeaveHint(); 30 | 31 | if (!isCheckingForPermissions()) { 32 | ErrorReport.killProcess(this); 33 | } 34 | } 35 | 36 | // @Override 37 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 38 | try { 39 | Method onRequestPermissionsResultMethod = AppCompatActivity.class.getMethod("onRequestPermissionsResult", int.class, permissions.getClass(), grantResults.getClass()); 40 | onRequestPermissionsResultMethod.invoke(new AppCompatActivity() /* never do this */, requestCode, permissions, grantResults); 41 | 42 | resetCheckingForPermissions(); 43 | } catch (Exception e) { 44 | if (Util.isDebuggableApp(this)) { 45 | e.printStackTrace(); 46 | } 47 | Toast.makeText(this, "Couldn't resolve permissions", Toast.LENGTH_LONG).show(); 48 | resetCheckingForPermissions(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /embed/android/debug/java/com/tns/NativeScriptSyncService.java: -------------------------------------------------------------------------------- 1 | package com.tns; 2 | 3 | import java.io.Closeable; 4 | import java.io.DataInputStream; 5 | import java.io.File; 6 | import java.io.FileFilter; 7 | import java.io.FileInputStream; 8 | import java.io.FileNotFoundException; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | 14 | import android.content.Context; 15 | import android.content.pm.ApplicationInfo; 16 | import android.content.pm.PackageManager.NameNotFoundException; 17 | import android.net.LocalServerSocket; 18 | import android.net.LocalSocket; 19 | import android.util.Log; 20 | 21 | public class NativeScriptSyncService { 22 | private static String SYNC_ROOT_SOURCE_DIR = "/data/local/tmp/"; 23 | private static final String SYNC_SOURCE_DIR = "/sync/"; 24 | private static final String FULL_SYNC_SOURCE_DIR = "/fullsync/"; 25 | private static final String REMOVED_SYNC_SOURCE_DIR = "/removedsync/"; 26 | 27 | private final Runtime runtime; 28 | private static Logger logger; 29 | private final Context context; 30 | 31 | private final String syncPath; 32 | private final String fullSyncPath; 33 | private final String removedSyncPath; 34 | private final File fullSyncDir; 35 | private final File syncDir; 36 | private final File removedSyncDir; 37 | 38 | private LocalServerSocketThread localServerThread; 39 | private Thread localServerJavaThread; 40 | 41 | public NativeScriptSyncService(Runtime runtime, Logger logger, Context context) { 42 | this.runtime = runtime; 43 | NativeScriptSyncService.logger = logger; 44 | this.context = context; 45 | 46 | syncPath = SYNC_ROOT_SOURCE_DIR + context.getPackageName() + SYNC_SOURCE_DIR; 47 | fullSyncPath = SYNC_ROOT_SOURCE_DIR + context.getPackageName() + FULL_SYNC_SOURCE_DIR; 48 | removedSyncPath = SYNC_ROOT_SOURCE_DIR + context.getPackageName() + REMOVED_SYNC_SOURCE_DIR; 49 | fullSyncDir = new File(fullSyncPath); 50 | syncDir = new File(syncPath); 51 | removedSyncDir = new File(removedSyncPath); 52 | } 53 | 54 | public void sync() { 55 | if (logger != null && logger.isEnabled()) { 56 | logger.write("Sync is enabled:"); 57 | logger.write("Sync path : " + syncPath); 58 | logger.write("Full sync path : " + fullSyncPath); 59 | logger.write("Removed files sync path: " + removedSyncPath); 60 | } 61 | 62 | if (fullSyncDir.exists()) { 63 | executeFullSync(context, fullSyncDir); 64 | return; 65 | } 66 | 67 | if (syncDir.exists()) { 68 | executePartialSync(context, syncDir); 69 | } 70 | 71 | if (removedSyncDir.exists()) { 72 | executeRemovedSync(context, removedSyncDir); 73 | } 74 | } 75 | 76 | private class LocalServerSocketThread implements Runnable { 77 | private volatile boolean running; 78 | private final String name; 79 | 80 | private ListenerWorker commThread; 81 | private LocalServerSocket serverSocket; 82 | 83 | public LocalServerSocketThread(String name) { 84 | this.name = name; 85 | this.running = false; 86 | } 87 | 88 | public void stop() { 89 | this.running = false; 90 | try { 91 | serverSocket.close(); 92 | } catch (IOException e) { 93 | if (com.tns.Runtime.isDebuggable()) { 94 | e.printStackTrace(); 95 | } 96 | } 97 | } 98 | 99 | public void run() { 100 | running = true; 101 | try { 102 | serverSocket = new LocalServerSocket(this.name); 103 | while (running) { 104 | LocalSocket socket = serverSocket.accept(); 105 | commThread = new ListenerWorker(socket); 106 | new Thread(commThread).start(); 107 | } 108 | } catch (IOException e) { 109 | if (com.tns.Runtime.isDebuggable()) { 110 | e.printStackTrace(); 111 | } 112 | } 113 | } 114 | } 115 | 116 | private class ListenerWorker implements Runnable { 117 | private final DataInputStream input; 118 | private Closeable socket; 119 | private OutputStream output; 120 | 121 | public ListenerWorker(LocalSocket socket) throws IOException { 122 | this.socket = socket; 123 | input = new DataInputStream(socket.getInputStream()); 124 | output = socket.getOutputStream(); 125 | } 126 | 127 | public void run() { 128 | try { 129 | int length = input.readInt(); 130 | input.readFully(new byte[length]); // ignore the payload 131 | executePartialSync(context, syncDir); 132 | executeRemovedSync(context, removedSyncDir); 133 | 134 | runtime.runScript(new File(NativeScriptSyncService.this.context.getFilesDir(), "internal/livesync.js")); 135 | try { 136 | output.write(1); 137 | } catch (IOException e) { 138 | if (com.tns.Runtime.isDebuggable()) { 139 | e.printStackTrace(); 140 | } 141 | } 142 | socket.close(); 143 | } catch (IOException e) { 144 | if (com.tns.Runtime.isDebuggable()) { 145 | e.printStackTrace(); 146 | } 147 | } 148 | } 149 | } 150 | 151 | public void startServer() { 152 | localServerThread = new LocalServerSocketThread(context.getPackageName() + "-livesync"); 153 | localServerJavaThread = new Thread(localServerThread); 154 | localServerJavaThread.start(); 155 | } 156 | 157 | public static boolean isSyncEnabled(Context context) { 158 | int flags; 159 | boolean shouldExecuteSync = false; 160 | try { 161 | flags = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).applicationInfo.flags; 162 | shouldExecuteSync = ((flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0); 163 | } catch (NameNotFoundException e) { 164 | if (com.tns.Runtime.isDebuggable()) { 165 | e.printStackTrace(); 166 | } 167 | return false; 168 | } 169 | 170 | return shouldExecuteSync; 171 | } 172 | 173 | final FileFilter deletingFilesFilter = new FileFilter() { 174 | @Override 175 | public boolean accept(File pathname) { 176 | if (pathname.isDirectory()) { 177 | return true; 178 | } 179 | 180 | boolean success = pathname.delete(); 181 | if (!success) { 182 | logger.write("Syncing: file not deleted: " + pathname.getAbsolutePath().toString()); 183 | } 184 | return false; 185 | } 186 | }; 187 | 188 | private void deleteDir(File directory) { 189 | File[] subDirectories = directory.listFiles(deletingFilesFilter); 190 | if (subDirectories != null) { 191 | for (int i = 0; i < subDirectories.length; i++) { 192 | File subDir = subDirectories[i]; 193 | deleteDir(subDir); 194 | } 195 | } 196 | 197 | boolean success = directory.delete(); 198 | if (!success && directory.exists()) { 199 | logger.write("Syncing: directory not deleted: " + directory.getAbsolutePath().toString()); 200 | } 201 | } 202 | 203 | private void moveFiles(File sourceDir, String sourceRootAbsolutePath, String targetRootAbsolutePath) { 204 | File[] files = sourceDir.listFiles(); 205 | 206 | if (files != null) { 207 | if (logger.isEnabled()) { 208 | logger.write("Syncing total number of fiiles: " + files.length); 209 | } 210 | 211 | for (int i = 0; i < files.length; i++) { 212 | File file = files[i]; 213 | if (file.isFile()) { 214 | if (logger.isEnabled()) { 215 | logger.write("Syncing: " + file.getAbsolutePath().toString()); 216 | } 217 | 218 | String targetFilePath = file.getAbsolutePath().replace(sourceRootAbsolutePath, targetRootAbsolutePath); 219 | File targetFileDir = new File(targetFilePath); 220 | 221 | File targetParent = targetFileDir.getParentFile(); 222 | if (targetParent != null) { 223 | targetParent.mkdirs(); 224 | } 225 | 226 | boolean success = copyFile(file.getAbsolutePath(), targetFilePath); 227 | if (!success) { 228 | logger.write("Sync failed: " + file.getAbsolutePath().toString()); 229 | } 230 | } else { 231 | moveFiles(file, sourceRootAbsolutePath, targetRootAbsolutePath); 232 | } 233 | } 234 | } else { 235 | if (logger.isEnabled()) { 236 | logger.write("Can't move files. Source is empty."); 237 | } 238 | } 239 | } 240 | 241 | // this removes only the app directory from the device to preserve 242 | // any existing files in /files directory on the device 243 | private void executeFullSync(Context context, final File sourceDir) { 244 | String appPath = context.getFilesDir().getAbsolutePath() + "/app"; 245 | final File appDir = new File(appPath); 246 | 247 | if (appDir.exists()) { 248 | deleteDir(appDir); 249 | moveFiles(sourceDir, sourceDir.getAbsolutePath(), appDir.getAbsolutePath()); 250 | } 251 | } 252 | 253 | private void executePartialSync(Context context, File sourceDir) { 254 | String appPath = context.getFilesDir().getAbsolutePath() + "/app"; 255 | final File appDir = new File(appPath); 256 | 257 | if (!appDir.exists()) { 258 | Log.e("TNS", "Application dir does not exists. Partial Sync failed. appDir: " + appPath); 259 | return; 260 | } 261 | 262 | if (logger.isEnabled()) { 263 | logger.write("Syncing sourceDir " + sourceDir.getAbsolutePath() + " with " + appDir.getAbsolutePath()); 264 | } 265 | 266 | moveFiles(sourceDir, sourceDir.getAbsolutePath(), appDir.getAbsolutePath()); 267 | } 268 | 269 | private void deleteRemovedFiles(File sourceDir, String sourceRootAbsolutePath, String targetRootAbsolutePath) { 270 | if (!sourceDir.exists()) { 271 | if (logger.isEnabled()) { 272 | logger.write("Directory does not exist: " + sourceDir.getAbsolutePath()); 273 | } 274 | } 275 | 276 | File[] files = sourceDir.listFiles(); 277 | 278 | if (files != null) { 279 | for (int i = 0; i < files.length; i++) { 280 | File file = files[i]; 281 | String targetFilePath = file.getAbsolutePath().replace(sourceRootAbsolutePath, targetRootAbsolutePath); 282 | File targetFile = new File(targetFilePath); 283 | if (file.isFile()) { 284 | if (logger.isEnabled()) { 285 | logger.write("Syncing removed file: " + file.getAbsolutePath().toString()); 286 | } 287 | 288 | targetFile.delete(); 289 | } else { 290 | deleteRemovedFiles(file, sourceRootAbsolutePath, targetRootAbsolutePath); 291 | 292 | // this is done so empty folders, if any, are deleted after we're don deleting files. 293 | if (targetFile.listFiles().length == 0) { 294 | targetFile.delete(); 295 | } 296 | } 297 | } 298 | } 299 | } 300 | 301 | private void executeRemovedSync(final Context context, final File sourceDir) { 302 | String appPath = context.getFilesDir().getAbsolutePath() + "/app"; 303 | deleteRemovedFiles(sourceDir, sourceDir.getAbsolutePath(), appPath); 304 | } 305 | 306 | private boolean copyFile(String sourceFile, String destinationFile) { 307 | FileInputStream fis = null; 308 | FileOutputStream fos = null; 309 | 310 | try { 311 | fis = new FileInputStream(sourceFile); 312 | fos = new FileOutputStream(destinationFile, false); 313 | 314 | byte[] buffer = new byte[4096]; 315 | int read = 0; 316 | 317 | while ((read = fis.read(buffer)) != -1) { 318 | fos.write(buffer, 0, read); 319 | } 320 | } catch (FileNotFoundException e) { 321 | logger.write("Error copying file " + sourceFile); 322 | if (com.tns.Runtime.isDebuggable()) { 323 | e.printStackTrace(); 324 | } 325 | return false; 326 | } catch (IOException e) { 327 | logger.write("Error copying file " + sourceFile); 328 | if (com.tns.Runtime.isDebuggable()) { 329 | e.printStackTrace(); 330 | } 331 | return false; 332 | } finally { 333 | try { 334 | if (fis != null) { 335 | fis.close(); 336 | } 337 | if (fos != null) { 338 | fos.close(); 339 | } 340 | } catch (IOException e) { 341 | } 342 | } 343 | 344 | return true; 345 | } 346 | } -------------------------------------------------------------------------------- /embed/android/debug/java/com/tns/NativeScriptSyncServiceSocketImpl.java: -------------------------------------------------------------------------------- 1 | package com.tns; 2 | 3 | import android.content.Context; 4 | import android.net.LocalServerSocket; 5 | import android.net.LocalSocket; 6 | 7 | import java.io.Closeable; 8 | import java.io.File; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.security.DigestInputStream; 12 | import java.security.MessageDigest; 13 | import java.io.OutputStream; 14 | import java.util.Arrays; 15 | 16 | public class NativeScriptSyncServiceSocketImpl { 17 | private static String DEVICE_APP_DIR; 18 | 19 | private final Runtime runtime; 20 | private static Logger logger; 21 | private final Context context; 22 | 23 | private LocalServerSocketThread localServerThread; 24 | private Thread localServerJavaThread; 25 | 26 | public NativeScriptSyncServiceSocketImpl(Runtime runtime, Logger logger, Context context) { 27 | this.runtime = runtime; 28 | NativeScriptSyncServiceSocketImpl.logger = logger; 29 | this.context = context; 30 | DEVICE_APP_DIR = this.context.getFilesDir().getAbsolutePath() + "/app"; 31 | } 32 | 33 | private class LocalServerSocketThread implements Runnable { 34 | 35 | private volatile boolean running; 36 | private final String name; 37 | 38 | private LiveSyncWorker livesyncWorker; 39 | private LocalServerSocket deviceSystemSocket; 40 | 41 | public LocalServerSocketThread(String name) { 42 | this.name = name; 43 | this.running = false; 44 | } 45 | 46 | public void stop() { 47 | this.running = false; 48 | try { 49 | deviceSystemSocket.close(); 50 | } catch (IOException e) { 51 | if (com.tns.Runtime.isDebuggable()) { 52 | e.printStackTrace(); 53 | } 54 | } 55 | } 56 | 57 | public void run() { 58 | running = true; 59 | try { 60 | deviceSystemSocket = new LocalServerSocket(this.name); 61 | while (running) { 62 | LocalSocket systemSocket = deviceSystemSocket.accept(); 63 | livesyncWorker = new LiveSyncWorker(systemSocket); 64 | Thread liveSyncThread = setUpLivesyncThread(); 65 | liveSyncThread.start(); 66 | } 67 | } catch (IOException e) { 68 | if (com.tns.Runtime.isDebuggable()) { 69 | e.printStackTrace(); 70 | } 71 | } 72 | catch (java.security.NoSuchAlgorithmException e) { 73 | if (com.tns.Runtime.isDebuggable()) { 74 | e.printStackTrace(); 75 | } 76 | } 77 | } 78 | 79 | private Thread setUpLivesyncThread() { 80 | Thread livesyncThread = new Thread(livesyncWorker); 81 | livesyncThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { 82 | @Override 83 | public void uncaughtException(Thread t, Throwable e) { 84 | logger.write(String.format("%s(%s): %s", t.getName(), t.getId(), e.toString())); 85 | } 86 | }); 87 | livesyncThread.setName("Livesync Thread"); 88 | return livesyncThread; 89 | } 90 | 91 | @Override 92 | protected void finalize() throws Throwable { 93 | deviceSystemSocket.close(); 94 | } 95 | } 96 | 97 | public void startServer() { 98 | localServerThread = new LocalServerSocketThread(context.getPackageName() + "-livesync"); 99 | localServerJavaThread = new Thread(localServerThread); 100 | localServerJavaThread.setName("Livesync Server Thread"); 101 | localServerJavaThread.start(); 102 | } 103 | 104 | private class LiveSyncWorker implements Runnable { 105 | public static final int OPERATION_BYTE_SIZE = 1; 106 | public static final int HASH_BYTE_SIZE = 16; 107 | public static final int LENGTH_BYTE_SIZE = 1; 108 | public static final int OPERATION_ID_BYTE_SIZE = 32; 109 | public static final int DELETE_FILE_OPERATION = 7; 110 | public static final int CREATE_FILE_OPERATION = 8; 111 | public static final int DO_SYNC_OPERATION = 9; 112 | public static final int ERROR_REPORT_CODE = 1; 113 | public static final int OPERATION_END_NO_REFRESH_REPORT_CODE = 3; 114 | public static final int OPERATION_END_REPORT_CODE = 2; 115 | public static final int REPORT_CODE_SIZE = 1; 116 | public static final int DO_REFRESH_LENGTH = 1; 117 | public static final int DO_REFRESH_VALUE = 1; 118 | public static final String FILE_NAME = "fileName"; 119 | public static final String FILE_NAME_LENGTH = FILE_NAME + "Length"; 120 | public static final String OPERATION = "operation"; 121 | public static final String FILE_CONTENT = "fileContent"; 122 | public static final String FILE_CONTENT_LENGTH = FILE_CONTENT + "Length"; 123 | public static final int DEFAULT_OPERATION = -1; 124 | private static final String PROTOCOL_VERSION = "0.2.0"; 125 | private byte[] handshakeMessage; 126 | private final DigestInputStream input; 127 | private Closeable livesyncSocket; 128 | private OutputStream output; 129 | 130 | public LiveSyncWorker(LocalSocket systemSocket) throws IOException, java.security.NoSuchAlgorithmException { 131 | this.livesyncSocket = systemSocket; 132 | MessageDigest md = MessageDigest.getInstance("MD5"); 133 | input = new DigestInputStream(systemSocket.getInputStream(), md); 134 | output = systemSocket.getOutputStream(); 135 | handshakeMessage = getHandshakeMessage(); 136 | } 137 | 138 | public void run() { 139 | try { 140 | output.write(handshakeMessage); 141 | output.flush(); 142 | 143 | } catch (IOException e) { 144 | logger.write(String.format("Error while LiveSyncing: Client socket might be closed!", e.toString())); 145 | if (com.tns.Runtime.isDebuggable()) { 146 | e.printStackTrace(); 147 | } 148 | } 149 | try { 150 | do { 151 | int operation = getOperation(); 152 | 153 | if (operation == DELETE_FILE_OPERATION) { 154 | 155 | String fileName = getFileName(); 156 | validateData(); 157 | deleteRecursive(new File(DEVICE_APP_DIR, fileName)); 158 | 159 | } else if (operation == CREATE_FILE_OPERATION) { 160 | 161 | String fileName = getFileName(); 162 | int contentLength = getFileContentLength(fileName); 163 | validateData(); 164 | byte[] content = getFileContent(fileName, contentLength); 165 | validateData(); 166 | createOrOverrideFile(fileName, content); 167 | 168 | } else if (operation == DO_SYNC_OPERATION) { 169 | byte[] operationUid = readNextBytes(OPERATION_ID_BYTE_SIZE); 170 | byte doRefresh = readNextBytes(DO_REFRESH_LENGTH)[0]; 171 | int doRefreshInt = (int)doRefresh; 172 | int operationReportCode; 173 | 174 | validateData(); 175 | if(runtime != null && doRefreshInt == DO_REFRESH_VALUE) { 176 | runtime.runScript(new File(NativeScriptSyncServiceSocketImpl.this.context.getFilesDir(), "internal/livesync.js"), false); 177 | operationReportCode = OPERATION_END_REPORT_CODE; 178 | } else { 179 | operationReportCode = OPERATION_END_NO_REFRESH_REPORT_CODE; 180 | } 181 | 182 | output.write(getReportMessageBytes(operationReportCode, operationUid)); 183 | output.flush(); 184 | 185 | } else if (operation == DEFAULT_OPERATION) { 186 | logger.write("LiveSync: input stream is empty!"); 187 | break; 188 | } else { 189 | throw new IllegalArgumentException(String.format("\nLiveSync: Operation not recognised. Received operation is %s.", operation)); 190 | } 191 | 192 | } while (true); 193 | } catch (Exception e) { 194 | String message = String.format("Error while LiveSyncing: %s", e.toString()); 195 | this.closeWithError(message); 196 | } catch (Throwable e) { 197 | String message = String.format("%s(%s): Error while LiveSyncing.\nOriginal Exception: %s", Thread.currentThread().getName(), Thread.currentThread().getId(), e.toString()); 198 | this.closeWithError(message); 199 | } 200 | 201 | } 202 | 203 | private byte[] getErrorMessageBytes(String message) { 204 | return this.getReportMessageBytes(ERROR_REPORT_CODE, message.getBytes()); 205 | } 206 | 207 | private byte[] getReportMessageBytes(int reportType, byte[] messageBytes) { 208 | byte[] reportBytes = new byte[]{(byte)reportType}; 209 | byte[] combined = new byte[messageBytes.length + REPORT_CODE_SIZE]; 210 | 211 | System.arraycopy(reportBytes,0,combined, 0, REPORT_CODE_SIZE); 212 | System.arraycopy(messageBytes,0,combined, REPORT_CODE_SIZE, messageBytes.length); 213 | 214 | return combined; 215 | } 216 | 217 | private byte[] getHandshakeMessage() { 218 | byte[] protocolVersionBytes = PROTOCOL_VERSION.getBytes(); 219 | byte[] versionLength = new byte[]{(byte)protocolVersionBytes.length}; 220 | byte[] packageNameBytes = context.getPackageName().getBytes(); 221 | byte[] combined = new byte[protocolVersionBytes.length + packageNameBytes.length + versionLength.length]; 222 | 223 | System.arraycopy(versionLength,0,combined, 0, versionLength.length); 224 | System.arraycopy(protocolVersionBytes,0,combined, versionLength.length, protocolVersionBytes.length); 225 | System.arraycopy(packageNameBytes,0,combined,protocolVersionBytes.length + versionLength.length,packageNameBytes.length); 226 | 227 | return combined; 228 | } 229 | 230 | private void validateData() throws IOException { 231 | MessageDigest messageDigest = input.getMessageDigest(); 232 | byte[] digest = messageDigest.digest(); 233 | input.on(false); 234 | byte[] inputMD5 = readNextBytes(HASH_BYTE_SIZE); 235 | input.on(true); 236 | 237 | 238 | if(!Arrays.equals(digest, inputMD5)){ 239 | throw new IllegalStateException(String.format("\nLiveSync: Validation of data failed.\nComputed hash: %s\nOriginal hash: %s ", Arrays.toString(digest), Arrays.toString(inputMD5))); 240 | } 241 | } 242 | 243 | /* 244 | * Tries to read operation input stream 245 | * If the stream is empty, method returns -1 246 | * */ 247 | private int getOperation() { 248 | Integer operation; 249 | byte[] operationBuff = null; 250 | try { 251 | operationBuff = readNextBytes(OPERATION_BYTE_SIZE); 252 | if (operationBuff == null) { 253 | return DEFAULT_OPERATION; 254 | } 255 | operation = Integer.parseInt(new String(operationBuff)); 256 | 257 | } catch (Exception e) { 258 | if(operationBuff == null){ 259 | operationBuff = new byte[]{}; 260 | } 261 | throw new IllegalStateException(String.format("\nLiveSync: failed to parse %s. Bytes read: $s %s\nOriginal Exception: %s", OPERATION, Arrays.toString(operationBuff), e.toString())); 262 | } 263 | return operation; 264 | } 265 | 266 | private String getFileName() { 267 | byte[] fileNameBuffer; 268 | int fileNameLength = -1; 269 | 270 | try { 271 | fileNameLength = getLength(); 272 | } catch (Exception e) { 273 | throw new IllegalStateException(String.format("\nLiveSync: failed to parse %s. \nOriginal Exception: %s", FILE_NAME_LENGTH, e.toString())); 274 | } 275 | 276 | if(fileNameLength <= 0) { 277 | throw new IllegalStateException(String.format("\nLiveSync: File name length must be positive number or zero. Provided length: %s.", fileNameLength)); 278 | } 279 | 280 | try { 281 | fileNameBuffer = readNextBytes(fileNameLength); 282 | } catch (Exception e) { 283 | throw new IllegalStateException(String.format("\nLiveSync: failed to parse %s.\nOriginal Exception: %s", FILE_NAME, e.toString())); 284 | } 285 | 286 | if (fileNameBuffer == null) { 287 | throw new IllegalStateException(String.format("\nLiveSync: Missing %s bytes.", FILE_NAME)); 288 | } 289 | 290 | String fileName = new String(fileNameBuffer); 291 | if (fileName.trim().length() < fileNameLength) { 292 | logger.write(String.format("WARNING: %s parsed length is less than %s. We read less information than you specified!", FILE_NAME, FILE_NAME_LENGTH)); 293 | } 294 | 295 | return fileName.trim(); 296 | } 297 | 298 | private int getFileContentLength(String fileName) throws IllegalStateException { 299 | int contentLength; 300 | 301 | try { 302 | contentLength = getLength(); 303 | } catch (Exception e) { 304 | throw new IllegalStateException(String.format("\nLiveSync: failed to read %s for %s.\nOriginal Exception: %s", FILE_CONTENT_LENGTH, fileName, e.toString())); 305 | } 306 | 307 | if(contentLength < 0){ 308 | throw new IllegalStateException(String.format("\nLiveSync: Content length must be positive number or zero. Provided content length: %s.", contentLength)); 309 | } 310 | 311 | return contentLength; 312 | } 313 | 314 | private byte[] getFileContent(String fileName, int contentLength) throws IllegalStateException { 315 | byte[] contentBuff = null; 316 | 317 | try { 318 | if(contentLength > 0) { 319 | contentBuff = readNextBytes(contentLength); 320 | } else if(contentLength == 0){ 321 | contentBuff = new byte[]{}; 322 | } 323 | } catch (Exception e) { 324 | throw new IllegalStateException(String.format("\nLiveSync: failed to parse content of file: %s.\nOriginal Exception: %s", fileName, e.toString())); 325 | } 326 | 327 | if (contentLength != 0 && contentBuff == null) { 328 | throw new IllegalStateException(String.format("\nLiveSync: Missing %s bytes for file: %s. Did you send %s bytes?", FILE_CONTENT, fileName, contentLength)); 329 | } 330 | 331 | return contentBuff; 332 | } 333 | 334 | private int getLength() { 335 | byte[] lengthBuffer; 336 | int lengthInt; 337 | 338 | try { 339 | byte lengthSize = readNextBytes(LENGTH_BYTE_SIZE)[0]; 340 | //Cast signed byte to unsigned int 341 | int lengthSizeInt = ((int) lengthSize) & 0xFF; 342 | lengthBuffer = readNextBytes(lengthSizeInt); 343 | 344 | if (lengthBuffer == null) { 345 | throw new IllegalStateException(String.format("\nLiveSync: Missing size length bytes.")); 346 | } 347 | } catch (Exception e) { 348 | throw new IllegalStateException(String.format("\nLiveSync: Failed to read size length. \nOriginal Exception: %s", e.toString())); 349 | } 350 | 351 | try { 352 | lengthInt = Integer.valueOf(new String(lengthBuffer)); 353 | } catch (Exception e) { 354 | throw new IllegalStateException(String.format("\nLiveSync: Failed to parse size length. \nOriginal Exception: %s", e.toString())); 355 | } 356 | 357 | return lengthInt; 358 | } 359 | 360 | private void createOrOverrideFile(String fileName, byte[] content) throws IOException { 361 | File fileToCreate = prepareFile(fileName); 362 | try { 363 | 364 | fileToCreate.getParentFile().mkdirs(); 365 | FileOutputStream fos = new FileOutputStream(fileToCreate.getCanonicalPath()); 366 | if(runtime != null) { 367 | runtime.lock(); 368 | } 369 | fos.write(content); 370 | fos.close(); 371 | 372 | } catch (Exception e) { 373 | throw new IOException(String.format("\nLiveSync: failed to write file: %s\nOriginal Exception: %s", fileName, e.toString())); 374 | } finally { 375 | if(runtime != null) { 376 | runtime.unlock(); 377 | } 378 | } 379 | } 380 | 381 | void deleteRecursive(File fileOrDirectory) { 382 | if (fileOrDirectory.isDirectory()) 383 | for (File child : fileOrDirectory.listFiles()) { 384 | deleteRecursive(child); 385 | } 386 | 387 | fileOrDirectory.delete(); 388 | } 389 | 390 | private File prepareFile(String fileName) { 391 | File fileToCreate = new File(DEVICE_APP_DIR, fileName); 392 | if (fileToCreate.exists()) { 393 | fileToCreate.delete(); 394 | } 395 | return fileToCreate; 396 | } 397 | 398 | /* 399 | * Reads next bites from input stream. Bytes read depend on passed parameter. 400 | * */ 401 | private byte[] readNextBytes(int size) throws IOException { 402 | byte[] buffer = new byte[size]; 403 | int bytesRead = 0; 404 | int bufferWriteOffset = bytesRead; 405 | try { 406 | do { 407 | 408 | bytesRead = this.input.read(buffer, bufferWriteOffset, size); 409 | if (bytesRead == -1) { 410 | if (bufferWriteOffset == 0) { 411 | return null; 412 | } 413 | break; 414 | } 415 | size -= bytesRead; 416 | bufferWriteOffset += bytesRead; 417 | } while (size > 0); 418 | } catch (IOException e) { 419 | String message = e.getMessage(); 420 | if (message != null && message.equals("Try again")) { 421 | throw new IllegalStateException("Error while LiveSyncing: Read operation timed out."); 422 | } else { 423 | throw e; 424 | } 425 | } 426 | 427 | return buffer; 428 | } 429 | 430 | private void closeWithError(String message) { 431 | try { 432 | output.write(getErrorMessageBytes(message)); 433 | output.flush(); 434 | logger.write(message); 435 | this.livesyncSocket.close(); 436 | } catch (IOException e) { 437 | if (com.tns.Runtime.isDebuggable()) { 438 | e.printStackTrace(); 439 | } 440 | } 441 | } 442 | 443 | @Override 444 | protected void finalize() throws Throwable { 445 | this.livesyncSocket.close(); 446 | } 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /embed/android/debug/res/layout/error_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 22 | 23 | 24 | 25 | 39 | 40 | -------------------------------------------------------------------------------- /embed/android/debug/res/layout/exception_tab.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 |