├── .gitignore ├── .gitmodules ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── build.gradle ├── cvio ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── teskalabs │ │ └── cvio │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── teskalabs │ │ │ └── cvio │ │ │ ├── CVIOInitProvider.java │ │ │ ├── CVIOInternals.java │ │ │ ├── CVIOSeaCatPlugin.java │ │ │ ├── CatVision.java │ │ │ ├── VNCDelegate.java │ │ │ ├── VNCServer.java │ │ │ ├── cviojni.java │ │ │ ├── exceptions │ │ │ └── CatVisionException.java │ │ │ └── inapp │ │ │ ├── InAppInputManager.java │ │ │ └── KeySym.java │ ├── jni │ │ ├── Android.mk │ │ ├── Application.mk │ │ ├── build.sh │ │ ├── com_teskalabs_cvio_cviojni.h │ │ └── cviojni.c │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── teskalabs │ └── cvio │ └── ExampleUnitTest.java ├── demo ├── .gitignore ├── build.gradle ├── google-services.json ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── about │ │ ├── index.html │ │ └── teskalabs_logo.png │ └── help │ │ └── index.html │ ├── ic_launcher-web.png │ ├── ic_launcher_catvision-web.png │ ├── ic_launcher_round-web.png │ ├── java │ └── io │ │ └── catvision │ │ └── appl │ │ ├── AboutActivity.java │ │ ├── ApiKeyObtainerActivity.java │ │ ├── HelpActivity.java │ │ ├── InfoActivity.java │ │ ├── IntroActivity.java │ │ ├── MainActivity.java │ │ ├── QRCodeScannerActivity.java │ │ ├── StartedFragment.java │ │ ├── StoppedFragment.java │ │ ├── TestAreaActivity.java │ │ ├── intro │ │ └── CustomIntroFragment.java │ │ └── tictactoe │ │ ├── GameActivity.java │ │ └── GameView.java │ └── res │ ├── anim │ ├── slide_in_left.xml │ ├── slide_in_right.xml │ ├── slide_out_left.xml │ └── slide_out_right.xml │ ├── drawable-hdpi │ ├── cat1.png │ ├── cat2.png │ ├── cat3.png │ ├── cat4.png │ ├── cat5.png │ └── catvision_logo_shadow.png │ ├── drawable-ldpi │ ├── cat1.png │ ├── cat2.png │ ├── cat3.png │ ├── cat4.png │ ├── cat5.png │ └── catvision_logo_shadow.png │ ├── drawable-mdpi │ ├── cat1.png │ ├── cat2.png │ ├── cat3.png │ ├── cat4.png │ ├── cat5.png │ └── catvision_logo_shadow.png │ ├── drawable-xhdpi │ ├── cat1.png │ ├── cat2.png │ ├── cat3.png │ ├── cat4.png │ ├── cat5.png │ └── catvision_logo_shadow.png │ ├── drawable-xxhdpi │ ├── cat1.png │ ├── cat2.png │ ├── cat3.png │ ├── cat4.png │ ├── cat5.png │ └── catvision_logo_shadow.png │ ├── drawable-xxxhdpi │ ├── cat1.png │ ├── cat2.png │ ├── cat3.png │ ├── cat4.png │ ├── cat5.png │ └── catvision_logo_shadow.png │ ├── drawable │ ├── catvision_blink.gif │ ├── catvision_logo.png │ ├── ic_launcher_background.xml │ ├── lib_bg.9.png │ ├── lib_circle.png │ ├── lib_cross.png │ ├── qr_code.png │ ├── seacat_logo.png │ ├── teskalabs_logo.png │ └── touch_browser.png │ ├── layout-land │ ├── activity_main.xml │ ├── content_api_key_obtainer.xml │ └── lib_game.xml │ ├── layout │ ├── activity_about.xml │ ├── activity_api_key_obtainer.xml │ ├── activity_help.xml │ ├── activity_info.xml │ ├── activity_main.xml │ ├── activity_test_area.xml │ ├── content_about.xml │ ├── content_api_key_obtainer.xml │ ├── content_help.xml │ ├── content_info.xml │ ├── content_test_area.xml │ ├── custom_fragment_intro.xml │ ├── custom_fragment_intro_content.xml │ ├── fragment_started.xml │ ├── fragment_stopped.xml │ └── lib_game.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── values-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── config.xml │ ├── dimens.xml │ ├── events.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml ├── external ├── build.sh ├── jpeg │ └── Android.mk ├── libpng │ └── Android.mk └── libvncserver │ ├── Android.mk │ ├── rfbconfig.h │ ├── rfbserver.c │ └── sockets.c ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── merge_seacat_cvio_builds.sh ├── seacat └── build.gradle └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | tmp/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # Intellij 38 | *.iml 39 | .idea/workspace.xml 40 | .idea/libraries/ 41 | .idea/codeStyleSettings.xml 42 | .idea/caches/ 43 | 44 | # Keystore files 45 | *.jks 46 | 47 | # Module external 48 | external/jpeg/obj 49 | external/libpng/obj 50 | external/libvncserver/obj 51 | external/lib 52 | 53 | external/include 54 | 55 | 56 | # Module cvio 57 | cvio/src/main/obj 58 | cvio/src/main/libs 59 | 60 | seacat/SeaCatClient_Android_*.aar 61 | 62 | .DS_Store 63 | .externalNativeBuild 64 | 65 | .gradle 66 | /local.properties 67 | /.idea/caches 68 | /.idea/libraries 69 | /.idea/modules.xml 70 | /.idea/workspace.xml 71 | /.idea/navEditor.xml 72 | /.idea/assetWizardSettings.xml 73 | /build 74 | /captures 75 | .cxx 76 | .idea 77 | .navigation 78 | output.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/jpeg/jni"] 2 | path = external/jpeg/jni 3 | url = https://android.googlesource.com/platform/external/jpeg 4 | [submodule "external/libpng/jni"] 5 | path = external/libpng/jni 6 | url = https://android.googlesource.com/platform/external/libpng 7 | [submodule "external/libvncserver/jni"] 8 | path = external/libvncserver/jni 9 | url = https://android.googlesource.com/platform/external/libvncserver 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 27 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, TeskaLabs 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CatVision.io SDK for Android 2 | 3 | CatVision.io SDK for Android is an library that enables to securely share screen of the app or device. It also enables to remotely control your app via mouse and keyboard events sent to your application. 4 | 5 | ![CatVision.io](https://teskalabscom.azureedge.net/media/img/solutions/teskalabs-catvisionio-illustration-android.png) 6 | 7 | You can watch a [demonstration video](https://www.youtube.com/watch?v=bKjMwUtapxc) from our early development stage. 8 | 9 | ## Read the docs 10 | 11 | Complete documentation for **CatVision.io** can be found at [https://docs.catvision.io](https://docs.catvision.io) 12 | 13 | In the docs you will also find out [how to integrate Catvision](https://docs.catvision.io) in your application. 14 | 15 | ## Build 16 | 17 | Clone this repo and cd into it 18 | 19 | ``` 20 | $ git clone git@github.com:TeskaLabs/CatVision-Android.git 21 | $ cd CatVision-Android 22 | ``` 23 | 24 | Clone submodules 25 | 26 | ``` 27 | $ git submodule init 28 | $ git submodule update 29 | ``` 30 | 31 | Now you can proceed with the following steps: 32 | 33 | 1. Build the VNC server library 34 | 2. Download SeaCat Client dependency 35 | 3. Build the cvio module's JNI 36 | 4. Build the AAR 37 | 38 | ### Build the VNC server library 39 | 40 | You need [Android NDK](https://developer.android.com/ndk/index.html) toolset to build the binaries for Android devices. They need to be available in your `$PATH` or `$ANDROID_NDK`. 41 | 42 | For example on Mac OSX, if you had installed Android NDK using the Android SDK manager before: 43 | 44 | ``` 45 | $ export ANDROID_NDK="${HOME}/Library/Android/sdk/ndk-bundle" 46 | ``` 47 | 48 | Then build the VNC server 49 | 50 | ``` 51 | $ cd external 52 | $ ./build.sh 53 | ``` 54 | 55 | ### Download SeaCat Client dependency 56 | 57 | The CatVision.io module depends on **SeaCat Client**. SeaCat takes care for identifying your device and making it able to connect securely. 58 | 59 | ``` 60 | $ cp SeaCatClient_Android_v1611-rc-1-release.aar ./seacat 61 | ``` 62 | 63 | ### Build the cvio module's JNI 64 | 65 | CatVision java class uses JNI interface to call the VNC server's functions. 66 | 67 | `$BASEDIR` is the path to the cloned repository. 68 | 69 | ``` 70 | $ cd $BASEDIR/cvio/src/main/jni 71 | $ ./build.sh 72 | ``` 73 | 74 | ### Build the project 75 | 76 | Use Android Studio: `Build->Clean Project`, `Build->Make Project` 77 | 78 | The **CatVision AAR** is now in `$BASEDIR/cvio/build/outputs/aar` 79 | 80 | TODO: build from command line 81 | 82 | ### Sign CatVision.IO AAR 83 | 84 | ``` 85 | $ apksigner sign --ks truststore.ks --out catvision-io-sdk-android-v1801.aar --min-sdk-version 14 catvision-io-sdk-android-v1801-release-aligned.aar 86 | ``` 87 | 88 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | maven { 7 | url 'https://maven.fabric.io/public' 8 | } 9 | maven { 10 | url 'https://maven.google.com/' 11 | } 12 | google() 13 | } 14 | dependencies { 15 | classpath 'com.android.tools.build:gradle:4.1.1' 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | classpath 'com.google.gms:google-services:4.3.4' 20 | classpath 'io.fabric.tools:gradle:1.25.4' 21 | } 22 | } 23 | 24 | allprojects { 25 | repositories { 26 | jcenter() 27 | maven { url 'https://jitpack.io' } 28 | maven { 29 | url 'https://maven.google.com/' 30 | } 31 | } 32 | } 33 | 34 | task clean(type: Delete) { 35 | delete rootProject.buildDir 36 | } 37 | -------------------------------------------------------------------------------- /cvio/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /cvio/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | def getVersionCode = { -> 4 | try { 5 | def stdout = new ByteArrayOutputStream() 6 | exec { 7 | commandLine 'git', 'rev-list', '--first-parent', '--count', 'master' 8 | standardOutput = stdout 9 | } 10 | return Integer.parseInt(stdout.toString().trim()) 11 | } 12 | catch (ignored) { 13 | return -1; 14 | } 15 | } 16 | 17 | def getVersionName = { -> 18 | try { 19 | def stdout = new ByteArrayOutputStream() 20 | exec { 21 | commandLine 'git', 'describe', '--abbrev=7', '--tag', '--dirty', '--always' 22 | standardOutput = stdout 23 | } 24 | return stdout.toString().trim() 25 | } 26 | catch (ignored) { 27 | return null; 28 | } 29 | } 30 | 31 | def getVersionNameDashes = { -> 32 | try { 33 | def vn = getVersionName() 34 | return vn.replace('.', '-') 35 | } 36 | catch (ignored) { 37 | return null; 38 | } 39 | } 40 | 41 | def getArchiveBaseName = { -> 42 | return "catvision-io-sdk-android-"+getVersionNameDashes() 43 | } 44 | 45 | android { 46 | compileSdkVersion 30 47 | buildToolsVersion '28.0.3' 48 | 49 | defaultConfig { 50 | minSdkVersion 14 51 | targetSdkVersion 30 52 | versionCode getVersionCode() 53 | versionName getVersionName() 54 | setProperty("archivesBaseName", getArchiveBaseName()) 55 | 56 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 57 | 58 | } 59 | sourceSets { 60 | main { 61 | jniLibs.srcDir 'src/main/libs' 62 | jni.srcDirs = [] 63 | } 64 | } 65 | buildTypes { 66 | release { 67 | minifyEnabled false 68 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 69 | } 70 | } 71 | } 72 | 73 | task assembleWithSeaCat(dependsOn: assemble) { doLast { 74 | 75 | android.libraryVariants.all { variant -> 76 | variant.outputs.each { output -> 77 | // Find SeaCat Client AAR 78 | def seacatAARFile = new File("${projectDir}/../seacat").list().find{it=~/SeaCat.*\.aar$/} 79 | if (seacatAARFile == null) { 80 | throw new GradleException('SeaCat AAR not found in project files.') 81 | } 82 | def seacatAARPath = "${projectDir}/../seacat/${seacatAARFile}" 83 | 84 | // Run merge script 85 | def process 86 | process = "${projectDir}/../merge_seacat_cvio_builds.sh ${seacatAARPath} ${output.outputFile.path}" 87 | process = process.execute() 88 | process.text.eachLine {println it} 89 | if (process.exitValue() != 0) { 90 | throw new GradleException("Execution of the merge script returned ${process.exitValue()}.") 91 | } 92 | println "$output.name\tassembled with SeaCat: ${seacatAARFile}" 93 | } 94 | } 95 | } } 96 | 97 | task releaseAndAlign(dependsOn: assembleWithSeaCat) { doLast { 98 | android.libraryVariants.all { variant -> 99 | if (variant.buildType.name == "release") { 100 | variant.outputs.each { output -> 101 | 102 | def env = System.getenv(); 103 | def android_home = env.get('ANDROID_HOME'); 104 | if (android_home == null) { 105 | println "Please set environment variable ADROID_HOME (export ANDROID_HOME=~/Library/Android/sdk/)." 106 | } 107 | def ZIPALIGN = new File(android_home, '/build-tools/28.0.3/zipalign').toString(); 108 | def APKSIGNER = new File(android_home, '/build-tools/28.0.3/apksigner').toString(); 109 | 110 | def outputPathNoExt = output.outputFile.path[0..(output.outputFile.path.lastIndexOf('.')-1)] 111 | def outputExt = output.outputFile.path[(output.outputFile.path.lastIndexOf('.')+1)..-1] 112 | 113 | // zipalign 114 | try { 115 | exec { 116 | executable "rm" 117 | args "${outputPathNoExt}-aligned.${outputExt}" 118 | } 119 | } catch (Exception e) {} 120 | 121 | def outputSignedPath = "${outputPathNoExt}-aligned.${outputExt}" 122 | exec { 123 | executable "${ZIPALIGN}" 124 | args '-v', '-p', '4', "${output.outputFile.path}", "${outputSignedPath}" 125 | } 126 | return 127 | // apksigner config 128 | // def pkcsConfigPath = '/tmp/pkcs11_java.cfg' 129 | // def pkcsConfigFile = new File(pkcsConfigPath) 130 | // try { 131 | // pkcsConfigFile.delete() 132 | // } catch(Exception e) {} 133 | // try { 134 | // pkcsConfigFile.createNewFile() 135 | // } catch (Exception e) {return null} 136 | // def newLine = System.getProperty("line.separator") 137 | // pkcsConfigFile << 'name = OpenSC-PKCS11' << newLine 138 | // pkcsConfigFile << 'description = SunPKCS11 via OpenSC' << newLine 139 | // pkcsConfigFile << 'library = /opt/local/lib/opensc-pkcs11.so' << newLine 140 | // pkcsConfigFile << 'slotListIndex = 0' << newLine 141 | // // run apksigner 142 | // exec { 143 | //// executable "${APKSIGNER}" 144 | //// args '--ks-provider-class' 'sun.security.pkcs11.SunPKCS11' '--ks-provider-arg' pkcsCopnfigPath '--ks' 'NONE' '--ks-type' 'PKCS11' "${outputSignedPath}" 145 | // executable "jarsigner" 146 | // args '-providerClass' 'sun.security.pkcs11.SunPKCS11' '-providerArg' '/tmp/pkcs11_java.cfg' '-keystore' 'NONE' '-storetype' 'PKCS11' ${outputSignedPath} //'"Certificate for Digital Signature"' 147 | // } 148 | } 149 | } 150 | } 151 | } } 152 | 153 | task sourcesJar(type: Jar) { 154 | from android.sourceSets.main.java.srcDirs 155 | classifier = 'sources' 156 | } 157 | 158 | dependencies { 159 | implementation fileTree(dir: 'libs', include: ['*.jar']) 160 | implementation 'androidx.appcompat:appcompat:1.2.0' 161 | implementation project(':seacat') 162 | implementation 'androidx.annotation:annotation:1.1.0' 163 | } 164 | -------------------------------------------------------------------------------- /cvio/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/mpavelka/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /cvio/src/androidTest/java/com/teskalabs/cvio/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.teskalabs.cvio; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.teskalabs.cvio.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cvio/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /cvio/src/main/java/com/teskalabs/cvio/CVIOInitProvider.java: -------------------------------------------------------------------------------- 1 | package com.teskalabs.cvio; 2 | 3 | import android.content.ContentProvider; 4 | import android.content.ContentValues; 5 | import android.content.Context; 6 | import android.database.Cursor; 7 | import android.net.Uri; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | 12 | /** 13 | * Created by ateska on 31.12.17. 14 | * 15 | * A lot easier initialization of CatVision.io SDK 16 | * Inspired by https://firebase.googleblog.com/2016/12/how-does-firebase-initialize-on-android.html 17 | * and https://medium.com/@andretietz/auto-initialize-your-android-library-2349daf06920 18 | * 19 | */ 20 | 21 | final public class CVIOInitProvider extends ContentProvider { 22 | 23 | public CVIOInitProvider() 24 | { 25 | } 26 | 27 | @Override 28 | public boolean onCreate() 29 | { 30 | // get the context (Application context) 31 | Context context = getContext(); 32 | 33 | return CatVision.init(context); 34 | } 35 | 36 | @Nullable 37 | @Override 38 | public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 39 | { 40 | return null; 41 | } 42 | 43 | @Nullable 44 | @Override 45 | public String getType(@NonNull Uri uri) 46 | { 47 | return null; 48 | } 49 | 50 | @Nullable 51 | @Override 52 | public Uri insert(@NonNull Uri uri, ContentValues values) 53 | { 54 | return null; 55 | } 56 | 57 | @Override 58 | public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) 59 | { 60 | return 0; 61 | } 62 | 63 | @Override 64 | public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) 65 | { 66 | return 0; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cvio/src/main/java/com/teskalabs/cvio/CVIOInternals.java: -------------------------------------------------------------------------------- 1 | package com.teskalabs.cvio; 2 | 3 | import android.content.Intent; 4 | import com.teskalabs.seacat.android.client.SeaCatClient; 5 | 6 | class CVIOInternals { 7 | 8 | public static Intent createIntent(String action) 9 | { 10 | Intent Intent = new Intent(action); 11 | Intent.addCategory(CatVision.CATEGORY_CVIO); 12 | Intent.addFlags(android.content.Intent.FLAG_FROM_BACKGROUND); 13 | return Intent; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /cvio/src/main/java/com/teskalabs/cvio/CVIOSeaCatPlugin.java: -------------------------------------------------------------------------------- 1 | package com.teskalabs.cvio; 2 | 3 | import com.teskalabs.seacat.android.client.SeaCatPlugin; 4 | 5 | import java.util.Properties; 6 | 7 | final public class CVIOSeaCatPlugin extends SeaCatPlugin { 8 | 9 | final private int port; 10 | 11 | CVIOSeaCatPlugin(int port) 12 | { 13 | this.port = port; 14 | } 15 | 16 | @Override 17 | public Properties getCharacteristics(){ 18 | Properties p = new Properties(); 19 | p.setProperty("RA", "vnc:"+port); 20 | return p; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /cvio/src/main/java/com/teskalabs/cvio/VNCDelegate.java: -------------------------------------------------------------------------------- 1 | package com.teskalabs.cvio; 2 | 3 | interface VNCDelegate 4 | { 5 | int takeImage(); // Return value: 0 - image has been pushed to VNCServer.push(), 1 - VNC server is requested to shutdown immediately 6 | 7 | // rfb* callbacks 8 | 9 | void rfbKbdAddEventProc(boolean down, long keySym, String client); 10 | void rfbKbdReleaseAllKeysProc(String client); 11 | 12 | /* Indicates either pointer movement or a pointer button press or release. The pointer is 13 | now at (x-position, y-position), and the current state of buttons 1 to 8 are represented 14 | by bits 0 to 7 of button-mask respectively, 0 meaning up, 1 meaning down (pressed). 15 | On a conventional mouse, buttons 1, 2 and 3 correspond to the left, middle and right 16 | buttons on the mouse. On a wheel mouse, each step of the wheel upwards is represented 17 | by a press and release of button 4, and each step downwards is represented by 18 | a press and release of button 5. 19 | From: http://www.vislab.usyd.edu.au/blogs/index.php/2009/05/22/an-headerless-indexed-protocol-for-input-1?blog=61 20 | */ 21 | void rfbPtrAddEventProc(int buttonMask, int x, int y, String client); 22 | 23 | void rfbSetXCutTextProc(String text, String client); 24 | 25 | int rfbNewClientHook(String client); 26 | } 27 | -------------------------------------------------------------------------------- /cvio/src/main/java/com/teskalabs/cvio/VNCServer.java: -------------------------------------------------------------------------------- 1 | package com.teskalabs.cvio; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.ContextWrapper; 6 | import android.graphics.PixelFormat; 7 | import android.media.Image; 8 | import android.os.Build; 9 | import android.util.Log; 10 | 11 | import com.teskalabs.seacat.android.client.SeaCatClient; 12 | import com.teskalabs.seacat.android.client.socket.SocketConfig; 13 | 14 | import java.io.IOException; 15 | import java.nio.ByteBuffer; 16 | 17 | import static com.teskalabs.cvio.cviojni.*; 18 | 19 | class VNCServer extends ContextWrapper { 20 | 21 | private static final String TAG = VNCServer.class.getName(); 22 | protected static CVIOSeaCatPlugin cvioSeaCatPlugin = null; 23 | 24 | private Thread mVNCThread = null; 25 | private final String socketFileName; 26 | private static final int port = 5900; 27 | 28 | public VNCServer(Context base, VNCDelegate delegate) { 29 | super(base); 30 | 31 | // Enable SeaCat 32 | if (cvioSeaCatPlugin == null) { 33 | cvioSeaCatPlugin = new CVIOSeaCatPlugin(port); 34 | } 35 | 36 | jni_set_delegate(delegate); 37 | 38 | // There has to be one directory (/s/) - it is used to ensure correct access level 39 | socketFileName = getDir("cvio", Context.MODE_PRIVATE).getAbsolutePath() + "/s/vnc"; 40 | } 41 | 42 | public void configureSeaCat() throws IOException { 43 | SeaCatClient.configureSocket( 44 | port, 45 | SocketConfig.Domain.AF_UNIX, SocketConfig.Type.SOCK_STREAM, 0, 46 | socketFileName, "" 47 | ); 48 | } 49 | 50 | 51 | public boolean run(final int screenWidth, final int screenHeight) 52 | { 53 | if ((screenWidth < 0) || (screenHeight < 0)) 54 | { 55 | Log.e(TAG, "Screen width/height is not specified"); 56 | return false; 57 | } 58 | 59 | synchronized (this) { 60 | // Check if VNC Thread is alive 61 | while (this.mVNCThread != null) { 62 | int rc; 63 | rc = jni_shutdown(); 64 | if (rc != 0) Log.w(TAG, "jni_shutdown returned: " + rc); 65 | 66 | try { 67 | this.mVNCThread.join(5000); 68 | if (this.mVNCThread.isAlive()) { 69 | Log.w(TAG, this.mVNCThread + " is still joining ..."); 70 | continue; 71 | } 72 | this.mVNCThread = null; 73 | } catch (InterruptedException e) { 74 | e.printStackTrace(); 75 | } 76 | } 77 | 78 | // Prepare VNC thread and launch 79 | if (this.mVNCThread == null) { 80 | this.mVNCThread = new Thread(new Runnable() { 81 | public void run() { 82 | int rc; 83 | 84 | Log.d(TAG, "VNC Server started"); 85 | 86 | rc = jni_run(socketFileName, screenWidth, screenHeight); 87 | if (rc != 0) Log.w(TAG, "VNC Server thread exited with rc: " + rc); 88 | 89 | Log.i(TAG, "VNC Server terminated"); 90 | } 91 | }); 92 | this.mVNCThread.setName("cvioVNCThread"); 93 | this.mVNCThread.setDaemon(true); 94 | this.mVNCThread.start(); 95 | } 96 | } 97 | 98 | return true; 99 | } 100 | 101 | 102 | public void shutdown() { 103 | 104 | if (this.mVNCThread == null) return; 105 | 106 | synchronized (this) { 107 | 108 | while (this.mVNCThread != null) { 109 | int rc; 110 | rc = jni_shutdown(); 111 | if (rc != 0) Log.w(TAG, "jni_shutdown returned: " + rc); 112 | 113 | try { 114 | this.mVNCThread.join(5000); 115 | if (this.mVNCThread.isAlive()) { 116 | Log.w(TAG, this.mVNCThread + " is still joining ..."); 117 | continue; 118 | } 119 | this.mVNCThread = null; 120 | } catch (InterruptedException e) { 121 | e.printStackTrace(); 122 | } 123 | } 124 | } 125 | } 126 | 127 | // Signalize that we have an image ready 128 | public void imageReady() { 129 | jni_image_ready(); 130 | } 131 | 132 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 133 | public void push(Image image, int pixelFormat) { 134 | Image.Plane[] planes = image.getPlanes(); 135 | ByteBuffer b = planes[0].getBuffer(); 136 | if (pixelFormat == PixelFormat.RGBA_8888) { 137 | // planes[0].getPixelStride() has to be 4 (32 bit) 138 | jni_push_pixels_rgba_8888(b, planes[0].getRowStride()); 139 | } 140 | else if (pixelFormat == PixelFormat.RGB_565) 141 | { 142 | // planes[0].getPixelStride() has to be 16 (16 bit) 143 | jni_push_pixels_rgba_565(b, planes[0].getRowStride()); 144 | } 145 | else 146 | { 147 | Log.e(TAG, "Image reader acquired unsupported image format " + pixelFormat); 148 | } 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /cvio/src/main/java/com/teskalabs/cvio/cviojni.java: -------------------------------------------------------------------------------- 1 | package com.teskalabs.cvio; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | class cviojni { 6 | 7 | static { 8 | System.loadLibrary("cviojni"); 9 | } 10 | 11 | static native int jni_run(String socketPath, int width, int height); 12 | static native int jni_shutdown(); 13 | 14 | static native void jni_image_ready(); // Send a 'signal' to VNC server that we have a image ready 15 | 16 | static native int jni_push_pixels_rgba_8888(ByteBuffer pixels, int row_stride); 17 | static native int jni_push_pixels_rgba_565(ByteBuffer pixels, int row_stride); 18 | 19 | static native void jni_set_delegate(VNCDelegate ra); 20 | } 21 | -------------------------------------------------------------------------------- /cvio/src/main/java/com/teskalabs/cvio/exceptions/CatVisionException.java: -------------------------------------------------------------------------------- 1 | package com.teskalabs.cvio.exceptions; 2 | 3 | public class CatVisionException extends Exception { 4 | public CatVisionException() {} 5 | public CatVisionException(Exception e) { 6 | super(e); 7 | } 8 | public CatVisionException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cvio/src/main/java/com/teskalabs/cvio/inapp/InAppInputManager.java: -------------------------------------------------------------------------------- 1 | package com.teskalabs.cvio.inapp; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.content.Context; 6 | import android.os.Build; 7 | import android.os.Bundle; 8 | import android.os.SystemClock; 9 | import android.util.Log; 10 | import android.view.InputDevice; 11 | import android.view.KeyEvent; 12 | import android.view.MotionEvent; 13 | import android.view.View; 14 | 15 | import java.lang.ref.WeakReference; 16 | import java.lang.reflect.Field; 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | 21 | public class InAppInputManager implements Application.ActivityLifecycleCallbacks { 22 | 23 | private static final String TAG = InAppInputManager.class.getName(); 24 | 25 | private final Object currentActivityLock = new Object(); // Synchronize access to currentActivity and currentRootView 26 | private WeakReference currentActivity = null; 27 | 28 | private boolean button1Pressed = false; 29 | private int metaState = 0; // State of the meta keys 30 | 31 | public InAppInputManager(Context context) { 32 | Application app = null; 33 | try { 34 | app = (Application) context; 35 | } 36 | catch (ClassCastException e) 37 | { 38 | app = null; 39 | } 40 | 41 | if (app == null) 42 | { 43 | Log.e(TAG, "Provided context is not an application - input events will not work"); 44 | return; 45 | } 46 | 47 | app.registerActivityLifecycleCallbacks(this); 48 | } 49 | 50 | private View obtainTargetView() { 51 | final Activity a = obtainActivity(); 52 | //Log.i(TAG, "obtainTargetView a:"+a); 53 | if (a == null) return null; 54 | 55 | View v = a.findViewById(android.R.id.content).getRootView(); 56 | //Log.i(TAG, "obtainTargetView vr:"+a); 57 | if (v == null) return null; 58 | if (v.hasWindowFocus()) return v; // Quick way 59 | 60 | List lv = getWindowManagerViews(); 61 | for(View vi : lv) 62 | { 63 | //Log.i(TAG, "obtainTargetView vi:"+vi+" f:"+vi.hasWindowFocus()); 64 | if (vi.hasWindowFocus()) return vi; 65 | } 66 | 67 | return v; 68 | } 69 | 70 | private Activity obtainActivity() { 71 | synchronized (currentActivityLock) { 72 | if (currentActivity == null) return null; 73 | return currentActivity.get(); 74 | } 75 | } 76 | 77 | // 78 | 79 | public void onMouseEvent(int buttonMask, int x, int y) { 80 | 81 | if ((!button1Pressed) && ((buttonMask & 1) != 0)) 82 | { 83 | injectTouchEvent(1, MotionEvent.ACTION_DOWN, x, y); 84 | button1Pressed = true; 85 | } 86 | 87 | else if (button1Pressed) 88 | { 89 | if ((buttonMask & 1) == 0) 90 | { 91 | injectTouchEvent(1, MotionEvent.ACTION_UP, x, y); 92 | button1Pressed = false; 93 | } 94 | 95 | else injectTouchEvent(1, MotionEvent.ACTION_MOVE, x, y); 96 | } 97 | } 98 | 99 | private void injectTouchEvent(int buttonId, int event, int x, int y) 100 | { 101 | final View view = obtainTargetView(); 102 | if (view == null) return; 103 | final Activity activity = obtainActivity(); 104 | if (activity == null) return; 105 | 106 | int viewLocation[] = new int[2]; 107 | view.getLocationOnScreen(viewLocation); 108 | 109 | MotionEvent.PointerProperties pp = new MotionEvent.PointerProperties(); 110 | pp.toolType = MotionEvent.TOOL_TYPE_FINGER; 111 | pp.id = 0; 112 | MotionEvent.PointerProperties[] pps = new MotionEvent.PointerProperties[]{pp}; 113 | 114 | MotionEvent.PointerCoords pc = new MotionEvent.PointerCoords(); 115 | pc.size = 1; 116 | pc.pressure = 1; 117 | pc.x = x - viewLocation[0]; 118 | pc.y = y - viewLocation[1]; 119 | MotionEvent.PointerCoords[] pcs = new MotionEvent.PointerCoords[]{pc}; 120 | 121 | long t = SystemClock.uptimeMillis(); 122 | 123 | final MotionEvent e = MotionEvent.obtain( 124 | t, // long downTime 125 | t + 100, // long eventTime 126 | event, // int action 127 | pps.length, // int pointerCount 128 | pps, // MotionEvent.PointerProperties[] pointerProperties 129 | pcs, // MotionEvent.PointerCoords[] pointerCoords 130 | 0, // int metaState 131 | 0, // int buttonState 132 | 1, // float xPrecision 133 | 1, // float yPrecision 134 | 1, // int deviceId 135 | 0, // int edgeFlags 136 | InputDevice.SOURCE_TOUCHSCREEN, //int source 137 | 0 // int flags 138 | ); 139 | 140 | activity.runOnUiThread(new Runnable() { 141 | public void run() { 142 | 143 | view.dispatchTouchEvent(e); 144 | } 145 | }); 146 | } 147 | 148 | /// 149 | 150 | public void onKeyboardEvent(boolean down, KeySym ks) 151 | { 152 | if ((ks == KeySym.XK_Escape) && ((this.metaState & (KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_RIGHT_ON)) != 0)) 153 | { 154 | if (!down) return; 155 | injectBackEvent(); 156 | return; 157 | } 158 | 159 | if (ks.keyeventCode != -1) 160 | { 161 | injectKeyboardEvent(down, ks); 162 | 163 | if (ks.metaState != 0) 164 | { 165 | if (down) this.metaState |= ks.metaState; 166 | else this.metaState &= ~ks.metaState; 167 | } 168 | 169 | return; 170 | } 171 | 172 | Log.i(TAG, "onKeyboardEvent: down:"+down+" keySym:"+ks); 173 | 174 | 175 | } 176 | 177 | private void injectKeyboardEvent(boolean down, KeySym ks) { 178 | final View view = obtainTargetView(); 179 | if (view == null) return; 180 | final Activity activity = obtainActivity(); 181 | if (activity == null) return; 182 | 183 | long t = SystemClock.uptimeMillis(); 184 | 185 | int action = KeyEvent.ACTION_UP; 186 | if (down) action = KeyEvent.ACTION_DOWN; 187 | 188 | final KeyEvent e = new KeyEvent( 189 | t, // downTime 190 | t + 100, // eventTime 191 | action, // action (ACTION_DOWN / ACTION_UP) 192 | ks.keyeventCode, // code 193 | 0, // Repeat 194 | this.metaState, 195 | 0, // Device Id 196 | 0 //ks.code 197 | ); 198 | 199 | // Log.i(TAG, "Injecting:"+e); 200 | 201 | activity.runOnUiThread(new Runnable() { 202 | public void run() { 203 | view.dispatchKeyEvent(e); 204 | } 205 | }); 206 | } 207 | 208 | /// 209 | 210 | private void injectBackEvent() 211 | { 212 | final Activity activity = obtainActivity(); 213 | if (activity == null) return; 214 | 215 | activity.runOnUiThread(new Runnable() { 216 | public void run() { 217 | activity.onBackPressed(); 218 | } 219 | }); 220 | } 221 | 222 | /// 223 | 224 | @Override 225 | public void onActivityCreated(Activity activity, Bundle bundle) { 226 | 227 | } 228 | 229 | @Override 230 | public void onActivityStarted(Activity activity) { 231 | Log.i(TAG, "onActivityStarted:" + activity); 232 | synchronized (currentActivityLock) { 233 | this.currentActivity = new WeakReference<>(activity); 234 | } 235 | } 236 | 237 | @Override 238 | public void onActivityResumed(Activity activity) { 239 | Log.i(TAG, "onActivityResumed:" + activity); 240 | synchronized (currentActivityLock) { 241 | this.currentActivity = new WeakReference<>(activity); 242 | } 243 | } 244 | 245 | @Override 246 | public void onActivityPaused(Activity activity) { 247 | Log.i(TAG, "onActivityPaused:" + activity); 248 | synchronized (currentActivityLock) { 249 | if ((this.currentActivity != null) && (this.currentActivity.get() == activity)) { 250 | this.currentActivity = null; 251 | } 252 | } 253 | } 254 | 255 | @Override 256 | public void onActivityStopped(Activity activity) { 257 | Log.i(TAG, "onActivityStopped:" + activity); 258 | synchronized (currentActivityLock) { 259 | if ((this.currentActivity != null) && (this.currentActivity.get() == activity)) { 260 | this.currentActivity = null; 261 | } 262 | } 263 | } 264 | 265 | @Override 266 | public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { 267 | } 268 | 269 | @Override 270 | public void onActivityDestroyed(Activity activity) { 271 | 272 | } 273 | 274 | /// 275 | 276 | private static List getWindowManagerViews() { 277 | try { 278 | 279 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && 280 | Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { 281 | 282 | // get the list from WindowManagerImpl.mViews 283 | Class wmiClass = Class.forName("android.view.WindowManagerImpl"); 284 | Object wmiInstance = wmiClass.getMethod("getDefault").invoke(null); 285 | 286 | return viewsFromWM(wmiClass, wmiInstance); 287 | 288 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 289 | 290 | // get the list from WindowManagerGlobal.mViews 291 | Class wmgClass = Class.forName("android.view.WindowManagerGlobal"); 292 | Object wmgInstance = wmgClass.getMethod("getInstance").invoke(null); 293 | 294 | return viewsFromWM(wmgClass, wmgInstance); 295 | } 296 | 297 | } catch (Exception e) { 298 | e.printStackTrace(); 299 | } 300 | 301 | return new ArrayList(); 302 | } 303 | 304 | private static List viewsFromWM(Class wmClass, Object wmInstance) throws Exception { 305 | 306 | Field viewsField = wmClass.getDeclaredField("mViews"); 307 | viewsField.setAccessible(true); 308 | Object views = viewsField.get(wmInstance); 309 | 310 | if (views instanceof List) { 311 | return (List) viewsField.get(wmInstance); 312 | } else if (views instanceof View[]) { 313 | return Arrays.asList((View[])viewsField.get(wmInstance)); 314 | } 315 | 316 | return new ArrayList(); 317 | } 318 | 319 | } 320 | -------------------------------------------------------------------------------- /cvio/src/main/jni/Android.mk: -------------------------------------------------------------------------------- 1 | LOCAL_PATH:= $(call my-dir) 2 | PROJECT_PATH:= $(LOCAL_PATH)/../../../.. 3 | 4 | 5 | include $(CLEAR_VARS) 6 | LOCAL_MODULE := libvncserver 7 | LOCAL_SRC_FILES := $(PROJECT_PATH)/external/lib/$(TARGET_ARCH_ABI)/libvncserver.a 8 | include $(PREBUILT_STATIC_LIBRARY) 9 | 10 | 11 | include $(CLEAR_VARS) 12 | LOCAL_MODULE := libjpeg_static 13 | LOCAL_SRC_FILES := $(PROJECT_PATH)/external/lib/$(TARGET_ARCH_ABI)/libjpeg_static.a 14 | include $(PREBUILT_STATIC_LIBRARY) 15 | 16 | 17 | include $(CLEAR_VARS) 18 | LOCAL_MODULE := libpng 19 | LOCAL_SRC_FILES := $(PROJECT_PATH)/external/lib/$(TARGET_ARCH_ABI)/libpng.a 20 | include $(PREBUILT_STATIC_LIBRARY) 21 | 22 | 23 | include $(CLEAR_VARS) 24 | 25 | LOCAL_SRC_FILES:= \ 26 | cviojni.c 27 | 28 | WITH_WEBSOCKETS:=0 29 | 30 | LOCAL_C_INCLUDES := $(PROJECT_PATH)/external/include 31 | 32 | 33 | LOCAL_LDLIBS:=-llog -lz 34 | LOCAL_STATIC_LIBRARIES := libvncserver libjpeg_static libpng 35 | LOCAL_MODULE:= cviojni 36 | 37 | 38 | include $(BUILD_SHARED_LIBRARY) 39 | -------------------------------------------------------------------------------- /cvio/src/main/jni/Application.mk: -------------------------------------------------------------------------------- 1 | APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 2 | APP_PLATFORM := android-14 3 | 4 | -------------------------------------------------------------------------------- /cvio/src/main/jni/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | JAVAC=javac 4 | 5 | # # Clean everything first 6 | rm -f com_teskalabs_cvio_android_cviojni.h 7 | rm -fr ../obj ../libs/armeabi-v7a ../libs/armeabi ../libs/x86 8 | 9 | # Prepare compiled class 10 | mkdir -p ../../../build/jni/classes 11 | ${JAVAC} -d ../../../build/jni/classes -classpath ~/Library/Android/sdk/platforms/android-21/android.jar:../java ../java/com/teskalabs/cvio/cviojni.java 12 | 13 | # Prepare header file 14 | javah -d . -classpath ~/Library/Android/sdk/platforms/android-21/android.jar:../../../build/jni/classes com.teskalabs.cvio.cviojni 15 | 16 | # Compile Android JNI 17 | ${ANDROID_NDK}/ndk-build -B 18 | -------------------------------------------------------------------------------- /cvio/src/main/jni/com_teskalabs_cvio_cviojni.h: -------------------------------------------------------------------------------- 1 | /* DO NOT EDIT THIS FILE - it is machine generated */ 2 | #include 3 | /* Header for class com_teskalabs_cvio_cviojni */ 4 | 5 | #ifndef _Included_com_teskalabs_cvio_cviojni 6 | #define _Included_com_teskalabs_cvio_cviojni 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | /* 11 | * Class: com_teskalabs_cvio_cviojni 12 | * Method: jni_run 13 | * Signature: (Ljava/lang/String;II)I 14 | */ 15 | JNIEXPORT jint JNICALL Java_com_teskalabs_cvio_cviojni_jni_1run 16 | (JNIEnv *, jclass, jstring, jint, jint); 17 | 18 | /* 19 | * Class: com_teskalabs_cvio_cviojni 20 | * Method: jni_shutdown 21 | * Signature: ()I 22 | */ 23 | JNIEXPORT jint JNICALL Java_com_teskalabs_cvio_cviojni_jni_1shutdown 24 | (JNIEnv *, jclass); 25 | 26 | /* 27 | * Class: com_teskalabs_cvio_cviojni 28 | * Method: jni_image_ready 29 | * Signature: ()V 30 | */ 31 | JNIEXPORT void JNICALL Java_com_teskalabs_cvio_cviojni_jni_1image_1ready 32 | (JNIEnv *, jclass); 33 | 34 | /* 35 | * Class: com_teskalabs_cvio_cviojni 36 | * Method: jni_push_pixels_rgba_8888 37 | * Signature: (Ljava/nio/ByteBuffer;I)I 38 | */ 39 | JNIEXPORT jint JNICALL Java_com_teskalabs_cvio_cviojni_jni_1push_1pixels_1rgba_18888 40 | (JNIEnv *, jclass, jobject, jint); 41 | 42 | /* 43 | * Class: com_teskalabs_cvio_cviojni 44 | * Method: jni_push_pixels_rgba_565 45 | * Signature: (Ljava/nio/ByteBuffer;I)I 46 | */ 47 | JNIEXPORT jint JNICALL Java_com_teskalabs_cvio_cviojni_jni_1push_1pixels_1rgba_1565 48 | (JNIEnv *, jclass, jobject, jint); 49 | 50 | /* 51 | * Class: com_teskalabs_cvio_cviojni 52 | * Method: jni_set_delegate 53 | * Signature: (Lcom/teskalabs/cvio/VNCDelegate;)V 54 | */ 55 | JNIEXPORT void JNICALL Java_com_teskalabs_cvio_cviojni_jni_1set_1delegate 56 | (JNIEnv *, jclass, jobject); 57 | 58 | #ifdef __cplusplus 59 | } 60 | #endif 61 | #endif 62 | -------------------------------------------------------------------------------- /cvio/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CatVision.io 3 | 4 | -------------------------------------------------------------------------------- /cvio/src/test/java/com/teskalabs/cvio/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.teskalabs.cvio; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'io.fabric' 3 | 4 | android { 5 | compileSdkVersion 30 6 | buildToolsVersion '28.0.3' 7 | defaultConfig { 8 | applicationId "io.catvision.appl" 9 | minSdkVersion 21 10 | targetSdkVersion 30 11 | versionCode 20120403 12 | versionName 'v201204-3' 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | sourceSets.main { 22 | jniLibs.srcDir 'src/main/libs' 23 | jni.srcDirs = [] //disable automatic ndk-build call 24 | } 25 | productFlavors { 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation fileTree(dir: 'libs', include: ['*.jar']) 31 | implementation project(':seacat') 32 | implementation project(':cvio') 33 | 34 | implementation 'com.github.apl-devs:appintro:v4.2.2' 35 | implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.21' 36 | implementation 'me.dm7.barcodescanner:zxing:1.9.8' 37 | implementation 'androidx.appcompat:appcompat:1.2.0' 38 | implementation 'com.android.support.constraint:constraint-layout:2.0.4' 39 | // Crashlytics 40 | implementation 'com.google.firebase:firebase-core:18.0.0' 41 | implementation 'com.google.firebase:firebase-crashlytics-ktx:17.3.0' 42 | implementation 'com.google.firebase:firebase-analytics-ktx:18.0.0' 43 | 44 | implementation 'com.google.firebase:firebase-core:18.0.0' 45 | // implementation 'com.android.support:support-v4:26.+' 46 | implementation 'androidx.appcompat:appcompat:1.2.0' 47 | implementation 'com.google.android.material:material:1.2.1' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 49 | } 50 | 51 | apply plugin: 'com.google.gms.google-services' 52 | -------------------------------------------------------------------------------- /demo/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "500594913840", 4 | "firebase_url": "https://catvision-io.firebaseio.com", 5 | "project_id": "catvision-io", 6 | "storage_bucket": "catvision-io.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:500594913840:android:e62712fcb81de0fc", 12 | "android_client_info": { 13 | "package_name": "io.catvision.app" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "500594913840-53j3d26oj6545a2jtok11l4pn2eimo4l.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyA30dAiJyft9T7tzhnvTDkC2D21ewn0GZM" 25 | } 26 | ], 27 | "services": { 28 | "analytics_service": { 29 | "status": 1 30 | }, 31 | "appinvite_service": { 32 | "status": 1, 33 | "other_platform_oauth_client": [] 34 | }, 35 | "ads_service": { 36 | "status": 2 37 | } 38 | } 39 | }, 40 | { 41 | "client_info": { 42 | "mobilesdk_app_id": "1:500594913840:android:ed34010ab865b2a4", 43 | "android_client_info": { 44 | "package_name": "io.catvision.appl" 45 | } 46 | }, 47 | "oauth_client": [ 48 | { 49 | "client_id": "500594913840-53j3d26oj6545a2jtok11l4pn2eimo4l.apps.googleusercontent.com", 50 | "client_type": 3 51 | } 52 | ], 53 | "api_key": [ 54 | { 55 | "current_key": "AIzaSyA30dAiJyft9T7tzhnvTDkC2D21ewn0GZM" 56 | } 57 | ], 58 | "services": { 59 | "analytics_service": { 60 | "status": 1 61 | }, 62 | "appinvite_service": { 63 | "status": 1, 64 | "other_platform_oauth_client": [] 65 | }, 66 | "ads_service": { 67 | "status": 2 68 | } 69 | } 70 | } 71 | ], 72 | "configuration_version": "1" 73 | } -------------------------------------------------------------------------------- /demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/mpavelka/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 46 | 47 | 51 | 52 | 56 | 57 | 61 | 62 | 66 | 67 | 71 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /demo/src/main/assets/about/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 |

15 | CatVision.io Try Me! App is an application created for demonstration of what CatVision.io SDK for Android can do. 16 |

17 |

18 | CatVision.io SDK for Android provides an easy and secure remote access to a screen of your mobile application. Purposely built for customer and tech support. IT service desk agents and customer tech support only need a web browser to remotely resolve any issues. It allows you to quickly solve any user issues with mobile apps by seeing what customer sees and guide him interactively and quickly. 19 |

20 |

21 | Contact us at team@catvision.io and get started right away! 22 |

23 |

24 | Developed by: 25 |

26 |

27 | TeskaLabs 28 |

29 | 30 | -------------------------------------------------------------------------------- /demo/src/main/assets/about/teskalabs_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/assets/about/teskalabs_logo.png -------------------------------------------------------------------------------- /demo/src/main/assets/help/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 |
15 |

Pairing

16 |

17 | For screen share of this application and your device, you need to first Pair CatVision.io mobile app with Web-App located on app.catvision.io. You can do this by scanning QR Code placed in Web-App or via Browser on your mobile, just tap on PAIR ME, then via Browser button, sign up or log in and then tap on the proper button. Refresh page and your app is paired with Web-App and you can start screen-sharing by tapping on SHARE ME button. 18 |

19 |
20 |
21 |

Screen Share

22 |

23 | Screen-sharing is beneficial technology; you can see from your browser what is displayed on the screen of your mobile device, and more! You can even control UI elements of the application itself. Understandably, you can't control device itself. It would need to be approved by the device manufacturer, for example, Samsung or Huawei. But you can see everything that's going on! 24 |

25 |
26 |
27 |

Unpair

28 |

29 | Function unpair serves its logical purpose; it will delete your pairing with Web-App, for example, if you wish to pair application with new Product inside Web-App. 30 |

31 |
32 |
33 |

What is "Product"?

34 |

35 | The product is how we call environment from where you can access mobile devices with your application equipped with CatVision.io SDK. Every application has its Product, so it is more clear and well-arranged that way. You can have one Product or ten Products, and it depends only on you. 36 |

37 |
38 | 39 | -------------------------------------------------------------------------------- /demo/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /demo/src/main/ic_launcher_catvision-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/ic_launcher_catvision-web.png -------------------------------------------------------------------------------- /demo/src/main/ic_launcher_round-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/ic_launcher_round-web.png -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/AboutActivity.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | import android.webkit.WebSettings; 7 | import android.webkit.WebView; 8 | import android.webkit.WebViewClient; 9 | 10 | import androidx.appcompat.app.AppCompatActivity; 11 | import androidx.appcompat.widget.Toolbar; 12 | 13 | public class AboutActivity extends AppCompatActivity { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_about); 19 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 20 | setSupportActionBar(toolbar); 21 | 22 | try { 23 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 24 | getSupportActionBar().setDisplayShowHomeEnabled(true); 25 | } catch (NullPointerException e) { 26 | e.printStackTrace(); 27 | } 28 | 29 | // Web view 30 | WebView webView = (WebView)findViewById(R.id.mainWebView); 31 | WebSettings webSettings = webView.getSettings(); 32 | webSettings.setCacheMode(WebSettings.LOAD_DEFAULT); 33 | // other 34 | webSettings.setJavaScriptEnabled(true); 35 | webSettings.setJavaScriptCanOpenWindowsAutomatically(true); 36 | webSettings.setAllowFileAccessFromFileURLs(true); 37 | webSettings.setAllowUniversalAccessFromFileURLs(true); 38 | // important! 39 | webView.setWebViewClient(new MyViewClient()); 40 | webView.loadUrl("file:///android_asset/about/index.html"); 41 | } 42 | 43 | @Override 44 | public boolean onSupportNavigateUp() { 45 | onBackPressed(); 46 | return true; 47 | } 48 | 49 | private class MyViewClient extends WebViewClient { 50 | @Override 51 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 52 | if(url.startsWith("mailto:")){ 53 | Intent i = new Intent(Intent.ACTION_SENDTO, Uri.parse(url)); 54 | startActivity(i); 55 | } 56 | else{ 57 | view.loadUrl(url); 58 | } 59 | return true; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/ApiKeyObtainerActivity.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.DialogInterface; 5 | import android.content.Intent; 6 | import android.content.pm.PackageManager; 7 | import android.net.Uri; 8 | import android.os.Build; 9 | import android.os.Bundle; 10 | import android.text.InputType; 11 | import android.view.Menu; 12 | import android.view.MenuItem; 13 | import android.view.View; 14 | import android.widget.EditText; 15 | 16 | import androidx.annotation.NonNull; 17 | import androidx.appcompat.app.AppCompatActivity; 18 | import androidx.appcompat.widget.Toolbar; 19 | import androidx.core.app.ActivityCompat; 20 | 21 | import com.google.firebase.analytics.FirebaseAnalytics; 22 | 23 | public class ApiKeyObtainerActivity extends AppCompatActivity { 24 | // Permissions 25 | private static int CAMERA_PERMISSION = 201; 26 | // Requests 27 | private static int QR_CODE_REQUEST = 1; 28 | 29 | private FirebaseAnalytics mFirebaseAnalytics; 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_api_key_obtainer); 35 | Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar); 36 | setSupportActionBar(toolbar); 37 | 38 | try { 39 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 40 | getSupportActionBar().setDisplayShowHomeEnabled(true); 41 | } catch (NullPointerException e) { 42 | e.printStackTrace(); 43 | } 44 | 45 | mFirebaseAnalytics = FirebaseAnalytics.getInstance(this); 46 | } 47 | 48 | @Override 49 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 50 | super.onActivityResult(requestCode, resultCode, data); 51 | if (requestCode == QR_CODE_REQUEST) { 52 | if (resultCode == RESULT_OK) { 53 | setResult(resultCode, data); 54 | finish(); 55 | } 56 | } 57 | } 58 | 59 | public void onClickQRCodeScan(View v) { 60 | if (isCameraPermissionGranted()) 61 | startQRScanActivity(); 62 | } 63 | 64 | public void onClickGetDeepLink(View v) { 65 | mFirebaseAnalytics.logEvent(getResources().getString(R.string.event_pair_link), new Bundle()); 66 | openURLinWebBrowser(getResources().getString(R.string.open_app_with_key_url)); 67 | finish(); 68 | } 69 | 70 | // Custom methods ------------------------------------------------------------------------------ 71 | /** 72 | * Starts an activity that retrieves the QR code. 73 | */ 74 | public void startQRScanActivity() { 75 | mFirebaseAnalytics.logEvent(getResources().getString(R.string.event_pair_qr), new Bundle()); 76 | Intent intent = new Intent(getApplicationContext(), QRCodeScannerActivity.class); 77 | startActivityForResult(intent, QR_CODE_REQUEST); 78 | } 79 | 80 | /** 81 | * Calls an intent to open a specified URL in the web browser. 82 | * @param url String 83 | */ 84 | public void openURLinWebBrowser(String url) { 85 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); 86 | } 87 | 88 | public void obtainApiKeyByInput(View w) { 89 | AlertDialog.Builder builder = new AlertDialog.Builder(this); 90 | builder.setTitle(getResources().getString(R.string.manual_new_api_key)); 91 | 92 | // Set up the input 93 | final EditText input = new EditText(this); 94 | 95 | // Specify the type of input expected 96 | input.setInputType(InputType.TYPE_CLASS_TEXT); 97 | builder.setView(input); 98 | 99 | // Set up the buttons 100 | builder.setPositiveButton(getResources().getString(R.string.dialog_button_ok), new DialogInterface.OnClickListener() { 101 | @Override 102 | public void onClick(DialogInterface dialog, int which) { 103 | String ApiKeyId = input.getText().toString(); 104 | // Checking 105 | if (ApiKeyId.equals("")) { 106 | dialog.cancel(); 107 | return; 108 | } 109 | // Sending back 110 | Intent intent = new Intent(); 111 | intent.putExtra("apikey_id", ApiKeyId); 112 | setResult(RESULT_OK, intent); 113 | finish(); 114 | } 115 | }); 116 | builder.setNegativeButton(getResources().getString(R.string.dialog_button_cancel), new DialogInterface.OnClickListener() { 117 | @Override 118 | public void onClick(DialogInterface dialog, int which) { 119 | dialog.cancel(); 120 | } 121 | }); 122 | builder.show(); 123 | } 124 | 125 | // Permissions --------------------------------------------------------------------------------- 126 | /** 127 | * Checks if it is allowed to use the camera. 128 | * @return boolean 129 | */ 130 | public boolean isCameraPermissionGranted() { 131 | if (Build.VERSION.SDK_INT >= 23) { 132 | if (checkSelfPermission(android.Manifest.permission.CAMERA) 133 | == PackageManager.PERMISSION_GRANTED) { 134 | return true; 135 | } else { 136 | ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.CAMERA}, CAMERA_PERMISSION); 137 | return false; 138 | } 139 | } else { 140 | return true; 141 | } 142 | } 143 | 144 | /** 145 | * Continues after the permission is obtained. 146 | * @param requestCode int 147 | * @param permissions @NonNull String[] 148 | * @param grantResults @NonNull int[] 149 | */ 150 | @Override 151 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 152 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 153 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 154 | if (requestCode == CAMERA_PERMISSION) { 155 | startQRScanActivity(); 156 | } 157 | } 158 | } 159 | 160 | // Menu ---------------------------------------------------------------------------------------- 161 | 162 | public boolean onCreateOptionsMenu(Menu menu) { 163 | return true; 164 | } 165 | 166 | public boolean onMenuItemClickShowClientTag(MenuItem v) { 167 | return true; 168 | } 169 | 170 | public boolean onMenuItemClickResetIdentity(MenuItem v) { 171 | return true; 172 | } 173 | 174 | public boolean onMenuItemClickTestArea(MenuItem v) { 175 | return true; 176 | } 177 | 178 | public boolean onMenuItemClickOverrideApiKeyId(MenuItem v) { 179 | return true; 180 | } 181 | 182 | @Override 183 | public boolean onSupportNavigateUp() { 184 | onBackPressed(); 185 | return true; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/HelpActivity.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl; 2 | 3 | import android.os.Bundle; 4 | import android.webkit.WebSettings; 5 | import android.webkit.WebView; 6 | import android.webkit.WebViewClient; 7 | 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import androidx.appcompat.widget.Toolbar; 10 | 11 | public class HelpActivity extends AppCompatActivity { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_help); 17 | Toolbar toolbar = findViewById(R.id.toolbar); 18 | setSupportActionBar(toolbar); 19 | 20 | try { 21 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 22 | getSupportActionBar().setDisplayShowHomeEnabled(true); 23 | } catch (NullPointerException e) { 24 | e.printStackTrace(); 25 | } 26 | 27 | // Web view 28 | WebView webView = (WebView)findViewById(R.id.mainWebView); 29 | WebSettings webSettings = webView.getSettings(); 30 | webSettings.setCacheMode(WebSettings.LOAD_DEFAULT); 31 | // other 32 | webSettings.setJavaScriptEnabled(true); 33 | webSettings.setJavaScriptCanOpenWindowsAutomatically(true); 34 | webSettings.setAllowFileAccessFromFileURLs(true); 35 | webSettings.setAllowUniversalAccessFromFileURLs(true); 36 | // important! 37 | webView.setWebViewClient(new WebViewClient()); 38 | webView.loadUrl("file:///android_asset/help/index.html"); 39 | } 40 | 41 | @Override 42 | public boolean onSupportNavigateUp() { 43 | onBackPressed(); 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/InfoActivity.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.widget.TextView; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | import androidx.appcompat.widget.Toolbar; 9 | 10 | public class InfoActivity extends AppCompatActivity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_info); 16 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 17 | setSupportActionBar(toolbar); 18 | 19 | try { 20 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 21 | getSupportActionBar().setDisplayShowHomeEnabled(true); 22 | } catch (NullPointerException e) { 23 | e.printStackTrace(); 24 | } 25 | 26 | // Getting data from the intent 27 | Intent data = getIntent(); 28 | String clientTag = data.getStringExtra("client_tag"); 29 | 30 | // Setting the client tag to the text view 31 | TextView clientTagView = (TextView)findViewById(R.id.client_tag_text); 32 | clientTagView.setText(clientTag); 33 | } 34 | 35 | @Override 36 | public boolean onSupportNavigateUp() { 37 | onBackPressed(); 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/IntroActivity.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl; 2 | 3 | import android.content.Intent; 4 | import android.content.pm.PackageManager; 5 | import android.os.Build; 6 | import android.os.Bundle; 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | import androidx.core.app.ActivityCompat; 10 | import androidx.fragment.app.Fragment; 11 | import com.github.paolorotolo.appintro.AppIntro; 12 | import com.github.paolorotolo.appintro.AppIntroFragment; 13 | import com.google.firebase.analytics.FirebaseAnalytics; 14 | 15 | import io.catvision.appl.intro.CustomIntroFragment; 16 | 17 | public class IntroActivity extends AppIntro { 18 | public static int TYPE_NONE = 0; 19 | public static int TYPE_QR = 1; 20 | public static int TYPE_DEEP = 2; 21 | 22 | // Permissions 23 | private static int CAMERA_PERMISSION = 21; 24 | // Requests 25 | private static int QR_CODE_REQUEST = 31; 26 | // Variables 27 | private FirebaseAnalytics mFirebaseAnalytics; 28 | private AppIntroFragment firstFragment; 29 | private AppIntroFragment lastFragment; 30 | 31 | /** 32 | * Does an action depending on the specified type from the fragment. 33 | * @param type int 34 | */ 35 | public void click(int type) { 36 | switch (type) { 37 | case 1: 38 | if (isCameraPermissionGranted()) 39 | startQRScanActivity(); 40 | break; 41 | case 2: 42 | Intent intent = getIntent(); 43 | intent.putExtra("type", TYPE_DEEP); 44 | setResult(RESULT_OK, intent); 45 | finish(); 46 | break; 47 | default: 48 | break; 49 | } 50 | } 51 | 52 | @Override 53 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 54 | super.onActivityResult(requestCode, resultCode, data); 55 | if (requestCode == QR_CODE_REQUEST) { 56 | if (resultCode == RESULT_OK) { 57 | data.putExtra("type", TYPE_QR); // specify the event 58 | setResult(resultCode, data); 59 | finish(); 60 | } 61 | } 62 | } 63 | 64 | @Override 65 | protected void onCreate(@Nullable Bundle savedInstanceState) { 66 | super.onCreate(savedInstanceState); 67 | 68 | // Note here that we DO NOT use setContentView(); 69 | // Add your slide fragments here. 70 | // AppIntro will automatically generate the dots indicator and buttons. 71 | // addSlide(firstFragment); 72 | 73 | // Special custom fragments 74 | // Pairing #1 75 | Fragment customFragment1 = new CustomIntroFragment(); 76 | Bundle b1 = new Bundle(); 77 | b1.putCharSequence("title", getResources().getString(R.string.intro_third_title)); 78 | b1.putCharSequence("description", getResources().getString(R.string.intro_third_text)); 79 | b1.putInt("image", R.drawable.cat3); 80 | b1.putInt("color", getResources().getColor(R.color.colorSlide3)); 81 | b1.putInt("type", TYPE_QR); 82 | customFragment1.setArguments(b1); 83 | 84 | // Pairing #2 85 | Fragment customFragment2 = new CustomIntroFragment(); 86 | Bundle b2 = new Bundle(); 87 | b2.putCharSequence("title", getResources().getString(R.string.intro_fourth_title)); 88 | b2.putCharSequence("description", getResources().getString(R.string.intro_fourth_text)); 89 | b2.putInt("image", R.drawable.cat4); 90 | b2.putInt("color", getResources().getColor(R.color.colorSlide4)); 91 | b2.putInt("type", TYPE_DEEP); 92 | customFragment2.setArguments(b2); 93 | 94 | // Instead of fragments, you can also use our default slide 95 | // Just set a title, description, background and image. AppIntro will do the rest. 96 | firstFragment = AppIntroFragment.newInstance(getResources().getString(R.string.intro_first_title), getResources().getString(R.string.intro_first_text), R.drawable.cat1, getResources().getColor(R.color.colorSlide1)); 97 | addSlide(firstFragment); 98 | addSlide(AppIntroFragment.newInstance(getResources().getString(R.string.intro_second_title), getResources().getString(R.string.intro_second_text), R.drawable.cat2, getResources().getColor(R.color.colorSlide2))); 99 | 100 | addSlide(customFragment1); 101 | addSlide(customFragment2); 102 | 103 | lastFragment = AppIntroFragment.newInstance(getResources().getString(R.string.intro_fifth_title), getResources().getString(R.string.intro_fifth_text), R.drawable.cat5, getResources().getColor(R.color.colorSlide5)); 104 | addSlide(lastFragment); 105 | // setting the title 106 | setTitle(getResources().getString(R.string.intro_title_first)); 107 | 108 | // OPTIONAL METHODS 109 | // Override bar/separator color. 110 | setBarColor(getResources().getColor(R.color.colorPrimary)); 111 | // setSeparatorColor(Color.parseColor("#2196F3")); 112 | 113 | // Hide Skip/Done button. 114 | showSkipButton(true); 115 | setProgressButtonEnabled(true); 116 | 117 | // Turn vibration on and set intensity. 118 | // NOTE: you will probably need to ask VIBRATE permission in Manifest. 119 | // setVibrate(true); 120 | // setVibrateIntensity(30); 121 | 122 | mFirebaseAnalytics = FirebaseAnalytics.getInstance(this); 123 | } 124 | 125 | @Override 126 | public void onRestoreInstanceState(Bundle savedInstanceState) { 127 | 128 | } 129 | 130 | @Override 131 | public void onSaveInstanceState(Bundle outState) { 132 | super.onSaveInstanceState(outState); 133 | } 134 | 135 | @Override 136 | public void onSkipPressed(Fragment currentFragment) { 137 | super.onSkipPressed(currentFragment); 138 | // Do something when users tap on Skip button. 139 | finish(); 140 | } 141 | 142 | @Override 143 | public void onDonePressed(Fragment currentFragment) { 144 | super.onDonePressed(currentFragment); 145 | // Do something when users tap on Done button. 146 | finish(); 147 | } 148 | 149 | @Override 150 | public void onSlideChanged(@Nullable Fragment oldFragment, @Nullable Fragment newFragment) { 151 | super.onSlideChanged(oldFragment, newFragment); 152 | // Do something when the slide changes. 153 | try { 154 | if (newFragment != firstFragment && newFragment != lastFragment) { 155 | setTitle(getResources().getString(R.string.intro_title_other)); 156 | } else { 157 | setTitle(getResources().getString(R.string.intro_title_first)); 158 | } 159 | } catch (NullPointerException e) { 160 | e.printStackTrace(); 161 | } 162 | } 163 | 164 | /** 165 | * Starts an activity that retrieves the QR code. 166 | */ 167 | public void startQRScanActivity() { 168 | mFirebaseAnalytics.logEvent(getResources().getString(R.string.event_pair_qr), new Bundle()); 169 | Intent intent = new Intent(getApplicationContext(), QRCodeScannerActivity.class); 170 | startActivityForResult(intent, QR_CODE_REQUEST); 171 | } 172 | 173 | // Permissions --------------------------------------------------------------------------------- 174 | /** 175 | * Checks if it is allowed to use the camera. 176 | * @return boolean 177 | */ 178 | public boolean isCameraPermissionGranted() { 179 | if (Build.VERSION.SDK_INT >= 23) { 180 | if (checkSelfPermission(android.Manifest.permission.CAMERA) 181 | == PackageManager.PERMISSION_GRANTED) { 182 | return true; 183 | } else { 184 | ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.CAMERA}, CAMERA_PERMISSION); 185 | return false; 186 | } 187 | } else { 188 | return true; 189 | } 190 | } 191 | 192 | /** 193 | * Continues after the permission is obtained. 194 | * @param requestCode int 195 | * @param permissions @NonNull String[] 196 | * @param grantResults @NonNull int[] 197 | */ 198 | @Override 199 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 200 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 201 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 202 | if (requestCode == CAMERA_PERMISSION) { 203 | startQRScanActivity(); 204 | } 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/QRCodeScannerActivity.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import com.google.zxing.Result; 8 | 9 | import me.dm7.barcodescanner.zxing.ZXingScannerView; 10 | 11 | // https://github.com/dm77/barcodescanner 12 | public class QRCodeScannerActivity extends Activity implements ZXingScannerView.ResultHandler { 13 | private ZXingScannerView mScannerView; 14 | 15 | @Override 16 | public void onCreate(Bundle state) { 17 | super.onCreate(state); 18 | mScannerView = new ZXingScannerView(this); // Programmatically initialize the scanner view 19 | setContentView(mScannerView); // Set the scanner view as the content view 20 | } 21 | 22 | @Override 23 | public void onResume() { 24 | super.onResume(); 25 | mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results. 26 | mScannerView.startCamera(); // Start camera on resume 27 | } 28 | 29 | @Override 30 | public void onPause() { 31 | super.onPause(); 32 | mScannerView.stopCamera(); // Stop camera on pause 33 | } 34 | 35 | @Override 36 | public void handleResult(Result rawResult) { 37 | String format = rawResult.getBarcodeFormat().toString(); 38 | if (format.equals("QR_CODE")) { 39 | try { 40 | // Reading the API key ID 41 | Uri uri = Uri.parse(rawResult.getText()); 42 | String apikey = uri.getQueryParameter("apikey").replace(" ", "+"); 43 | // Returning result 44 | Intent result = new Intent(); 45 | result.putExtra("apikey_id", apikey); 46 | result.putExtra("format", format); 47 | setResult(Activity.RESULT_OK, result); 48 | finish(); 49 | } catch (Exception e) { 50 | e.printStackTrace(); 51 | mScannerView.resumeCameraPreview(this); 52 | } 53 | } else { 54 | // If you would like to resume scanning, call this method below: 55 | mScannerView.resumeCameraPreview(this); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/StartedFragment.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import androidx.fragment.app.Fragment; 11 | 12 | 13 | /** 14 | * A simple {@link Fragment} subclass. 15 | * Activities that contain this fragment must implement the 16 | * {@link StartedFragment.OnFragmentInteractionListener} interface 17 | * to handle interaction events. 18 | * Use the {@link StartedFragment#newInstance} factory method to 19 | * create an instance of this fragment. 20 | */ 21 | public class StartedFragment extends Fragment { 22 | // TODO: Rename parameter arguments, choose names that match 23 | // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER 24 | private static final String ARG_PARAM1 = "param1"; 25 | private static final String ARG_PARAM2 = "param2"; 26 | 27 | // TODO: Rename and change types of parameters 28 | private String mParam1; 29 | private String mParam2; 30 | 31 | private OnFragmentInteractionListener mListener; 32 | 33 | public StartedFragment() { 34 | // Required empty public constructor 35 | } 36 | 37 | /** 38 | * Use this factory method to create a new instance of 39 | * this fragment using the provided parameters. 40 | * 41 | * @param param1 Parameter 1. 42 | * @param param2 Parameter 2. 43 | * @return A new instance of fragment StartedFragment. 44 | */ 45 | // TODO: Rename and change types and number of parameters 46 | public static StartedFragment newInstance(String param1, String param2) { 47 | StartedFragment fragment = new StartedFragment(); 48 | Bundle args = new Bundle(); 49 | args.putString(ARG_PARAM1, param1); 50 | args.putString(ARG_PARAM2, param2); 51 | fragment.setArguments(args); 52 | return fragment; 53 | } 54 | 55 | @Override 56 | public void onCreate(Bundle savedInstanceState) { 57 | super.onCreate(savedInstanceState); 58 | if (getArguments() != null) { 59 | mParam1 = getArguments().getString(ARG_PARAM1); 60 | mParam2 = getArguments().getString(ARG_PARAM2); 61 | } 62 | } 63 | 64 | @Override 65 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 66 | Bundle savedInstanceState) { 67 | // Inflate the layout for this fragment 68 | return inflater.inflate(R.layout.fragment_started, container, false); 69 | } 70 | 71 | // TODO: Rename method, update argument and hook method into UI event 72 | public void onButtonPressed(Uri uri) { 73 | if (mListener != null) { 74 | mListener.onFragmentInteractionStopRequest(); 75 | } 76 | } 77 | 78 | @Override 79 | public void onAttach(Context context) { 80 | super.onAttach(context); 81 | if (context instanceof OnFragmentInteractionListener) { 82 | mListener = (OnFragmentInteractionListener) context; 83 | } else { 84 | throw new RuntimeException(context.toString() 85 | + " must implement OnFragmentInteractionListener"); 86 | } 87 | } 88 | 89 | @Override 90 | public void onDetach() { 91 | super.onDetach(); 92 | mListener = null; 93 | } 94 | 95 | /** 96 | * This interface must be implemented by activities that contain this 97 | * fragment to allow an interaction in this fragment to be communicated 98 | * to the activity and potentially other fragments contained in that 99 | * activity. 100 | *

101 | * See the Android Training lesson Communicating with Other Fragments for more information. 104 | */ 105 | public interface OnFragmentInteractionListener { 106 | void onFragmentInteractionStopRequest(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/StoppedFragment.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.Button; 9 | 10 | import androidx.fragment.app.Fragment; 11 | 12 | 13 | /** 14 | * A simple {@link Fragment} subclass. 15 | * Activities that contain this fragment must implement the 16 | * {@link StoppedFragment.OnFragmentInteractionListener} interface 17 | * to handle interaction events. 18 | * Use the {@link StoppedFragment#newInstance} factory method to 19 | * create an instance of this fragment. 20 | */ 21 | public class StoppedFragment extends Fragment { 22 | // TODO: Rename parameter arguments, choose names that match 23 | // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER 24 | private static final String ARG_PARAM1 = "param1"; 25 | private static final String ARG_PARAM2 = "param2"; 26 | 27 | // TODO: Rename and change types of parameters 28 | private String mParam1; 29 | private String mParam2; 30 | 31 | private OnFragmentInteractionListener mListener; 32 | 33 | public StoppedFragment() { 34 | // Required empty public constructor 35 | } 36 | 37 | /** 38 | * Use this factory method to create a new instance of 39 | * this fragment using the provided parameters. 40 | * 41 | * @param param1 Parameter 1. 42 | * @param param2 Parameter 2. 43 | * @return A new instance of fragment StoppedFragment. 44 | */ 45 | // TODO: Rename and change types and number of parameters 46 | public static StoppedFragment newInstance(String param1, String param2) { 47 | StoppedFragment fragment = new StoppedFragment(); 48 | Bundle args = new Bundle(); 49 | args.putString(ARG_PARAM1, param1); 50 | args.putString(ARG_PARAM2, param2); 51 | fragment.setArguments(args); 52 | return fragment; 53 | } 54 | 55 | @Override 56 | public void onCreate(Bundle savedInstanceState) { 57 | super.onCreate(savedInstanceState); 58 | if (getArguments() != null) { 59 | mParam1 = getArguments().getString(ARG_PARAM1); 60 | mParam2 = getArguments().getString(ARG_PARAM2); 61 | } 62 | } 63 | 64 | @Override 65 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 66 | Bundle savedInstanceState) { 67 | // Inflate the layout for this fragment 68 | View view = inflater.inflate(R.layout.fragment_stopped, container, false); 69 | // Checking if the API key is present 70 | refreshApiKeyRelatedView(view); 71 | return view; 72 | } 73 | 74 | public void onClick(View v) { 75 | if (mListener != null) { 76 | mListener.onFragmentInteractionStartRequest(); 77 | } 78 | } 79 | 80 | @Override 81 | public void onAttach(Context context) { 82 | super.onAttach(context); 83 | if (context instanceof OnFragmentInteractionListener) { 84 | mListener = (OnFragmentInteractionListener) context; 85 | } else { 86 | throw new RuntimeException(context.toString() 87 | + " must implement OnFragmentInteractionListener"); 88 | } 89 | } 90 | 91 | @Override 92 | public void onDetach() { 93 | super.onDetach(); 94 | mListener = null; 95 | } 96 | 97 | /** 98 | * This interface must be implemented by activities that contain this 99 | * fragment to allow an interaction in this fragment to be communicated 100 | * to the activity and potentially other fragments contained in that 101 | * activity. 102 | *

103 | * See the Android Training lesson Communicating with Other Fragments for more information. 106 | */ 107 | public interface OnFragmentInteractionListener { 108 | void onFragmentInteractionStartRequest(); 109 | } 110 | 111 | // Custom methods ------------------------------------------------------------------------------ 112 | 113 | /** 114 | * Refreshes all elements related to the Api Key ID. 115 | * @param view View 116 | */ 117 | public void refreshApiKeyRelatedView(View view) { 118 | if (view == null) 119 | view = getView(); 120 | try { 121 | MainActivity mainActivity = (MainActivity) getActivity(); 122 | String api_key = mainActivity.getPreferenceString(MainActivity.SAVED_API_KEY_ID); 123 | // Views 124 | Button buttonStart = (Button)view.findViewById(R.id.button2); 125 | if (api_key != null) { 126 | // Change the label of the button 127 | buttonStart.setText(getResources().getString(R.string.start_sharing)); 128 | } else { 129 | // Change the label of the button 130 | buttonStart.setText(getResources().getString(R.string.start_connecting)); 131 | } 132 | } catch (NullPointerException e) { 133 | e.printStackTrace(); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/TestAreaActivity.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl; 2 | 3 | import android.os.Bundle; 4 | import android.util.Log; 5 | import android.view.KeyEvent; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | import android.widget.ImageView; 9 | 10 | import androidx.appcompat.app.AppCompatActivity; 11 | import androidx.appcompat.widget.Toolbar; 12 | 13 | public class TestAreaActivity extends AppCompatActivity { 14 | 15 | private static final String TAG = TestAreaActivity.class.getName(); 16 | 17 | private ImageView clickMark; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_test_area); 23 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 24 | setSupportActionBar(toolbar); 25 | 26 | try { 27 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 28 | getSupportActionBar().setDisplayShowHomeEnabled(true); 29 | } catch (NullPointerException e) { 30 | e.printStackTrace(); 31 | } 32 | 33 | clickMark = (ImageView) findViewById(R.id.clickMark); 34 | } 35 | 36 | @Override 37 | protected void onStart() 38 | { 39 | super.onStart(); 40 | } 41 | 42 | @Override 43 | protected void onStop() 44 | { 45 | super.onStop(); 46 | } 47 | 48 | 49 | private int getRelativeLeft(View myView) { 50 | if (myView.getParent() == myView.getRootView()) 51 | return myView.getLeft(); 52 | else 53 | return myView.getLeft() + getRelativeLeft((View) myView.getParent()); 54 | } 55 | 56 | private int getRelativeTop(View myView) { 57 | if (myView.getParent() == myView.getRootView()) 58 | return myView.getTop(); 59 | else 60 | return myView.getTop() + getRelativeTop((View) myView.getParent()); 61 | } 62 | 63 | @Override 64 | public boolean onTouchEvent(MotionEvent event) { 65 | int x = (int) event.getX() - getRelativeLeft((View)clickMark.getParent()) - (clickMark.getWidth() / 2); 66 | int y = (int) event.getY() - getRelativeTop((View)clickMark.getParent()) - (clickMark.getHeight() / 2); 67 | 68 | switch (event.getAction()) { 69 | case MotionEvent.ACTION_DOWN: 70 | clickMark.setX(x); 71 | clickMark.setY(y); 72 | break; 73 | 74 | case MotionEvent.ACTION_MOVE: 75 | clickMark.setX(x); 76 | clickMark.setY(y); 77 | break; 78 | 79 | case MotionEvent.ACTION_UP: 80 | break; 81 | } 82 | 83 | return true; 84 | } 85 | 86 | @Override 87 | public boolean onKeyDown(int keyCode, KeyEvent event) { 88 | Log.i(TAG, "onKeyDown: keyCode:"+keyCode+" event:"+event); 89 | return super.onKeyDown(keyCode, event); 90 | } 91 | 92 | @Override 93 | public boolean onSupportNavigateUp() { 94 | onBackPressed(); 95 | return true; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/intro/CustomIntroFragment.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl.intro; 2 | 3 | 4 | import android.os.Bundle; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | import android.widget.LinearLayout; 10 | import android.widget.TextView; 11 | 12 | import androidx.fragment.app.Fragment; 13 | 14 | import io.catvision.appl.IntroActivity; 15 | import io.catvision.appl.R; 16 | 17 | public final class CustomIntroFragment extends Fragment implements View.OnClickListener { 18 | private int type; 19 | 20 | public CustomIntroFragment() { 21 | 22 | } 23 | 24 | @Override 25 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 26 | Bundle savedInstanceState) { 27 | // A new view 28 | View view = inflater.inflate(R.layout.custom_fragment_intro, container, false); 29 | 30 | // Getting the elements 31 | LinearLayout main = (LinearLayout)view.findViewById(R.id.main); 32 | TextView title = (TextView)view.findViewById(R.id.title); 33 | TextView description = (TextView)view.findViewById(R.id.description); 34 | ImageView image = (ImageView)view.findViewById(R.id.image); 35 | 36 | // Getting the arguments 37 | Bundle bundle = getArguments(); 38 | try { 39 | type = bundle.getInt("type"); 40 | CharSequence titleValue = bundle.getCharSequence("title"); 41 | if (titleValue != null) { 42 | title.setText(titleValue); 43 | } 44 | CharSequence descriptionValue = bundle.getCharSequence("description"); 45 | if (descriptionValue != null) { 46 | description.setText(descriptionValue); 47 | } 48 | int colorValue = bundle.getInt("color"); 49 | main.setBackgroundColor(colorValue); 50 | int imageValue = bundle.getInt("image"); 51 | image.setImageResource(imageValue); 52 | } catch (NullPointerException e) { 53 | e.printStackTrace(); 54 | } 55 | 56 | // OnClick 57 | image.setOnClickListener(this); 58 | 59 | return view; 60 | } 61 | 62 | @Override 63 | public void onClick(View v) { 64 | switch (v.getId()) { 65 | case R.id.image: 66 | IntroActivity introActivity = (IntroActivity)getActivity(); 67 | introActivity.click(type); 68 | break; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/tictactoe/GameActivity.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl.tictactoe; 2 | 3 | import java.util.Random; 4 | 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | import android.os.Message; 8 | import android.os.Handler.Callback; 9 | import android.view.View; 10 | import android.view.View.OnClickListener; 11 | import android.widget.Button; 12 | import android.widget.TextView; 13 | 14 | import androidx.appcompat.app.AppCompatActivity; 15 | import androidx.appcompat.widget.Toolbar; 16 | 17 | import io.catvision.appl.tictactoe.GameView.ICellListener; 18 | import io.catvision.appl.tictactoe.GameView.State; 19 | 20 | import io.catvision.appl.R; 21 | 22 | 23 | public class GameActivity extends AppCompatActivity { 24 | 25 | /** Start player. Must be 1 or 2. Default is 1. */ 26 | public static final String EXTRA_START_PLAYER = 27 | "io.catvision.app.tictactoe.GameActivity.EXTRA_START_PLAYER"; 28 | 29 | private static final int MSG_COMPUTER_TURN = 1; 30 | private static final long COMPUTER_DELAY_MS = 500; 31 | 32 | private Handler mHandler = new Handler(new MyHandlerCallback()); 33 | private Random mRnd = new Random(); 34 | private GameView mGameView; 35 | private TextView mInfoView; 36 | private Button mButtonNext; 37 | 38 | /** Called when the activity is first created. */ 39 | @Override 40 | public void onCreate(Bundle bundle) { 41 | super.onCreate(bundle); 42 | 43 | /* 44 | * IMPORTANT: all resource IDs from this library will eventually be merged 45 | * with the resources from the main project that will use the library. 46 | * 47 | * If the main project and the libraries define the same resource IDs, 48 | * the application project will always have priority and override library resources 49 | * and IDs defined in multiple libraries are resolved based on the libraries priority 50 | * defined in the main project. 51 | * 52 | * An intentional consequence is that the main project can override some resources 53 | * from the library. 54 | * (TODO insert example). 55 | * 56 | * To avoid potential conflicts, it is suggested to add a prefix to the 57 | * library resource names. 58 | */ 59 | setContentView(R.layout.lib_game); 60 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 61 | setSupportActionBar(toolbar); 62 | 63 | try { 64 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 65 | getSupportActionBar().setDisplayShowHomeEnabled(true); 66 | } catch (NullPointerException e) { 67 | e.printStackTrace(); 68 | } 69 | 70 | mGameView = (GameView) findViewById(R.id.game_view); 71 | mInfoView = (TextView) findViewById(R.id.info_turn); 72 | mButtonNext = (Button) findViewById(R.id.next_turn); 73 | 74 | mGameView.setFocusable(true); 75 | mGameView.setFocusableInTouchMode(true); 76 | mGameView.setCellListener(new MyCellListener()); 77 | 78 | mButtonNext.setOnClickListener(new MyButtonListener()); 79 | } 80 | 81 | @Override 82 | protected void onResume() { 83 | super.onResume(); 84 | 85 | State player = mGameView.getCurrentPlayer(); 86 | if (player == State.UNKNOWN) { 87 | player = State.fromInt(getIntent().getIntExtra(EXTRA_START_PLAYER, 1)); 88 | if (!checkGameFinished(player)) { 89 | selectTurn(player); 90 | } 91 | } 92 | // if (player == State.PLAYER2) { 93 | // mHandler.sendEmptyMessageDelayed(MSG_COMPUTER_TURN, COMPUTER_DELAY_MS); 94 | // } 95 | if (player == State.WIN) { 96 | setWinState(mGameView.getWinner()); 97 | } 98 | } 99 | 100 | 101 | private State selectTurn(State player) { 102 | mGameView.setCurrentPlayer(player); 103 | mButtonNext.setEnabled(false); 104 | 105 | if (player == State.PLAYER1) { 106 | mInfoView.setText(R.string.player1_turn); 107 | mGameView.setEnabled(true); 108 | 109 | } else if (player == State.PLAYER2) { 110 | mInfoView.setText(R.string.player2_turn); 111 | mGameView.setEnabled(true); // earlier: false 112 | } 113 | 114 | return player; 115 | } 116 | 117 | private class MyCellListener implements ICellListener { 118 | public void onCellSelected() { 119 | if (true/* mGameView.getCurrentPlayer() == State.PLAYER1 */) { 120 | int cell = mGameView.getSelection(); 121 | mButtonNext.setEnabled(cell >= 0); 122 | } 123 | } 124 | } 125 | 126 | private class MyButtonListener implements OnClickListener { 127 | 128 | public void onClick(View v) { 129 | State player = mGameView.getCurrentPlayer(); 130 | 131 | if (player == State.WIN) { 132 | GameActivity.this.finish(); 133 | 134 | } else if (player == State.PLAYER1 || player == State.PLAYER2) { // earlier: only PLAYER1 135 | int cell = mGameView.getSelection(); 136 | if (cell >= 0) { 137 | mGameView.stopBlink(); 138 | mGameView.setCell(cell, player); 139 | finishTurn(); 140 | } 141 | } 142 | } 143 | } 144 | 145 | private class MyHandlerCallback implements Callback { 146 | public boolean handleMessage(Message msg) { 147 | if (msg.what == MSG_COMPUTER_TURN) { 148 | 149 | // Pick a non-used cell at random. That's about all the AI you need for this game. 150 | State[] data = mGameView.getData(); 151 | int used = 0; 152 | while (used != 0x1F) { 153 | int index = mRnd.nextInt(9); 154 | if (((used >> index) & 1) == 0) { 155 | used |= 1 << index; 156 | if (data[index] == State.EMPTY) { 157 | mGameView.setCell(index, mGameView.getCurrentPlayer()); 158 | break; 159 | } 160 | } 161 | } 162 | 163 | finishTurn(); 164 | return true; 165 | } 166 | return false; 167 | } 168 | } 169 | 170 | private State getOtherPlayer(State player) { 171 | return player == State.PLAYER1 ? State.PLAYER2 : State.PLAYER1; 172 | } 173 | 174 | private void finishTurn() { 175 | State player = mGameView.getCurrentPlayer(); 176 | if (!checkGameFinished(player)) { 177 | player = selectTurn(getOtherPlayer(player)); 178 | // if (player == State.PLAYER2) { 179 | // mHandler.sendEmptyMessageDelayed(MSG_COMPUTER_TURN, COMPUTER_DELAY_MS); 180 | // } 181 | } 182 | } 183 | 184 | public boolean checkGameFinished(State player) { 185 | State[] data = mGameView.getData(); 186 | boolean full = true; 187 | 188 | int col = -1; 189 | int row = -1; 190 | int diag = -1; 191 | 192 | // check rows 193 | for (int j = 0, k = 0; j < 3; j++, k += 3) { 194 | if (data[k] != State.EMPTY && data[k] == data[k+1] && data[k] == data[k+2]) { 195 | row = j; 196 | } 197 | if (full && (data[k] == State.EMPTY || 198 | data[k+1] == State.EMPTY || 199 | data[k+2] == State.EMPTY)) { 200 | full = false; 201 | } 202 | } 203 | 204 | // check columns 205 | for (int i = 0; i < 3; i++) { 206 | if (data[i] != State.EMPTY && data[i] == data[i+3] && data[i] == data[i+6]) { 207 | col = i; 208 | } 209 | } 210 | 211 | // check diagonals 212 | if (data[0] != State.EMPTY && data[0] == data[1+3] && data[0] == data[2+6]) { 213 | diag = 0; 214 | } else if (data[2] != State.EMPTY && data[2] == data[1+3] && data[2] == data[0+6]) { 215 | diag = 1; 216 | } 217 | 218 | if (col != -1 || row != -1 || diag != -1) { 219 | setFinished(player, col, row, diag); 220 | return true; 221 | } 222 | 223 | // if we get here, there's no winner but the board is full. 224 | if (full) { 225 | setFinished(State.EMPTY, -1, -1, -1); 226 | return true; 227 | } 228 | return false; 229 | } 230 | 231 | private void setFinished(State player, int col, int row, int diagonal) { 232 | 233 | mGameView.setCurrentPlayer(State.WIN); 234 | mGameView.setWinner(player); 235 | mGameView.setEnabled(false); 236 | mGameView.setFinished(col, row, diagonal); 237 | 238 | setWinState(player); 239 | } 240 | 241 | private void setWinState(State player) { 242 | mButtonNext.setEnabled(true); 243 | mButtonNext.setText("Back"); 244 | 245 | String text; 246 | 247 | if (player == State.EMPTY) { 248 | text = getString(R.string.tie); 249 | } else if (player == State.PLAYER1) { 250 | text = getString(R.string.player1_win); 251 | } else { 252 | text = getString(R.string.player2_win); 253 | } 254 | mInfoView.setText(text); 255 | } 256 | 257 | @Override 258 | public boolean onSupportNavigateUp() { 259 | onBackPressed(); 260 | return true; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /demo/src/main/java/io/catvision/appl/tictactoe/GameView.java: -------------------------------------------------------------------------------- 1 | package io.catvision.appl.tictactoe; 2 | 3 | import java.util.Random; 4 | 5 | import android.content.Context; 6 | import android.content.res.Resources; 7 | import android.graphics.Bitmap; 8 | import android.graphics.BitmapFactory; 9 | import android.graphics.Canvas; 10 | import android.graphics.Paint; 11 | import android.graphics.Rect; 12 | import android.graphics.Bitmap.Config; 13 | import android.graphics.BitmapFactory.Options; 14 | import android.graphics.Paint.Style; 15 | import android.graphics.drawable.Drawable; 16 | import android.os.Bundle; 17 | import android.os.Handler; 18 | import android.os.Message; 19 | import android.os.Parcelable; 20 | import android.os.Handler.Callback; 21 | import android.util.AttributeSet; 22 | import android.view.MotionEvent; 23 | import android.view.View; 24 | 25 | import io.catvision.appl.R; 26 | 27 | //----------------------------------------------- 28 | 29 | public class GameView extends View { 30 | 31 | public static final long FPS_MS = 1000/2; 32 | 33 | public enum State { 34 | UNKNOWN(-3), 35 | WIN(-2), 36 | EMPTY(0), 37 | PLAYER1(1), 38 | PLAYER2(2); 39 | 40 | private int mValue; 41 | 42 | private State(int value) { 43 | mValue = value; 44 | } 45 | 46 | public int getValue() { 47 | return mValue; 48 | } 49 | 50 | public static State fromInt(int i) { 51 | for (State s : values()) { 52 | if (s.getValue() == i) { 53 | return s; 54 | } 55 | } 56 | return EMPTY; 57 | } 58 | } 59 | 60 | private static final int MARGIN = 4; 61 | private static final int MSG_BLINK = 1; 62 | 63 | private final Handler mHandler = new Handler(new MyHandler()); 64 | 65 | private final Rect mSrcRect = new Rect(); 66 | private final Rect mDstRect = new Rect(); 67 | 68 | private int mSxy; 69 | private int mOffetX; 70 | private int mOffetY; 71 | private Paint mWinPaint; 72 | private Paint mLinePaint; 73 | private Paint mBmpPaint; 74 | private Bitmap mBmpPlayer1; 75 | private Bitmap mBmpPlayer2; 76 | private Drawable mDrawableBg; 77 | 78 | private ICellListener mCellListener; 79 | 80 | /** Contains one of {@link State#EMPTY}, {@link State#PLAYER1} or {@link State#PLAYER2}. */ 81 | private final State[] mData = new State[9]; 82 | 83 | private int mSelectedCell = -1; 84 | private State mSelectedValue = State.EMPTY; 85 | private State mCurrentPlayer = State.UNKNOWN; 86 | private State mWinner = State.EMPTY; 87 | 88 | private int mWinCol = -1; 89 | private int mWinRow = -1; 90 | private int mWinDiag = -1; 91 | 92 | private boolean mBlinkDisplayOff; 93 | private final Rect mBlinkRect = new Rect(); 94 | 95 | 96 | 97 | public interface ICellListener { 98 | abstract void onCellSelected(); 99 | } 100 | 101 | public GameView(Context context, AttributeSet attrs) { 102 | super(context, attrs); 103 | requestFocus(); 104 | 105 | mDrawableBg = getResources().getDrawable(R.drawable.lib_bg); 106 | setBackgroundDrawable(mDrawableBg); 107 | 108 | mBmpPlayer1 = getResBitmap(R.drawable.lib_cross); 109 | mBmpPlayer2 = getResBitmap(R.drawable.lib_circle); 110 | 111 | if (mBmpPlayer1 != null) { 112 | mSrcRect.set(0, 0, mBmpPlayer1.getWidth() -1, mBmpPlayer1.getHeight() - 1); 113 | } 114 | 115 | mBmpPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 116 | 117 | mLinePaint = new Paint(); 118 | mLinePaint.setColor(0xFFFFFFFF); 119 | mLinePaint.setStrokeWidth(5); 120 | mLinePaint.setStyle(Style.STROKE); 121 | 122 | mWinPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 123 | mWinPaint.setColor(0xFFFF0000); 124 | mWinPaint.setStrokeWidth(10); 125 | mWinPaint.setStyle(Style.STROKE); 126 | 127 | for (int i = 0; i < mData.length; i++) { 128 | mData[i] = State.EMPTY; 129 | } 130 | 131 | if (isInEditMode()) { 132 | // In edit mode (e.g. in the Eclipse ADT graphical layout editor) 133 | // we'll use some random data to display the state. 134 | Random rnd = new Random(); 135 | for (int i = 0; i < mData.length; i++) { 136 | mData[i] = State.fromInt(rnd.nextInt(3)); 137 | } 138 | } 139 | } 140 | 141 | public State[] getData() { 142 | return mData; 143 | } 144 | 145 | public void setCell(int cellIndex, State value) { 146 | mData[cellIndex] = value; 147 | invalidate(); 148 | } 149 | 150 | public void setCellListener(ICellListener cellListener) { 151 | mCellListener = cellListener; 152 | } 153 | 154 | public int getSelection() { 155 | if (mSelectedValue == mCurrentPlayer) { 156 | return mSelectedCell; 157 | } 158 | 159 | return -1; 160 | } 161 | 162 | public State getCurrentPlayer() { 163 | return mCurrentPlayer; 164 | } 165 | 166 | public void setCurrentPlayer(State player) { 167 | mCurrentPlayer = player; 168 | mSelectedCell = -1; 169 | } 170 | 171 | public State getWinner() { 172 | return mWinner; 173 | } 174 | 175 | public void setWinner(State winner) { 176 | mWinner = winner; 177 | } 178 | 179 | /** Sets winning mark on specified column or row (0..2) or diagonal (0..1). */ 180 | public void setFinished(int col, int row, int diagonal) { 181 | mWinCol = col; 182 | mWinRow = row; 183 | mWinDiag = diagonal; 184 | } 185 | 186 | //----------------------------------------- 187 | 188 | 189 | @Override 190 | protected void onDraw(Canvas canvas) { 191 | super.onDraw(canvas); 192 | 193 | int sxy = mSxy; 194 | int s3 = sxy * 3; 195 | int x7 = mOffetX; 196 | int y7 = mOffetY; 197 | 198 | for (int i = 0, k = sxy; i < 2; i++, k += sxy) { 199 | canvas.drawLine(x7 , y7 + k, x7 + s3 - 1, y7 + k , mLinePaint); 200 | canvas.drawLine(x7 + k, y7 , x7 + k , y7 + s3 - 1, mLinePaint); 201 | } 202 | 203 | for (int j = 0, k = 0, y = y7; j < 3; j++, y += sxy) { 204 | for (int i = 0, x = x7; i < 3; i++, k++, x += sxy) { 205 | mDstRect.offsetTo(MARGIN+x, MARGIN+y); 206 | 207 | State v; 208 | if (mSelectedCell == k) { 209 | if (mBlinkDisplayOff) { 210 | continue; 211 | } 212 | v = mSelectedValue; 213 | } else { 214 | v = mData[k]; 215 | } 216 | 217 | switch(v) { 218 | case PLAYER1: 219 | if (mBmpPlayer1 != null) { 220 | canvas.drawBitmap(mBmpPlayer1, mSrcRect, mDstRect, mBmpPaint); 221 | } 222 | break; 223 | case PLAYER2: 224 | if (mBmpPlayer2 != null) { 225 | canvas.drawBitmap(mBmpPlayer2, mSrcRect, mDstRect, mBmpPaint); 226 | } 227 | break; 228 | } 229 | } 230 | } 231 | 232 | if (mWinRow >= 0) { 233 | int y = y7 + mWinRow * sxy + sxy / 2; 234 | canvas.drawLine(x7 + MARGIN, y, x7 + s3 - 1 - MARGIN, y, mWinPaint); 235 | 236 | } else if (mWinCol >= 0) { 237 | int x = x7 + mWinCol * sxy + sxy / 2; 238 | canvas.drawLine(x, y7 + MARGIN, x, y7 + s3 - 1 - MARGIN, mWinPaint); 239 | 240 | } else if (mWinDiag == 0) { 241 | // diagonal 0 is from (0,0) to (2,2) 242 | 243 | canvas.drawLine(x7 + MARGIN, y7 + MARGIN, 244 | x7 + s3 - 1 - MARGIN, y7 + s3 - 1 - MARGIN, mWinPaint); 245 | 246 | } else if (mWinDiag == 1) { 247 | // diagonal 1 is from (0,2) to (2,0) 248 | 249 | canvas.drawLine(x7 + MARGIN, y7 + s3 - 1 - MARGIN, 250 | x7 + s3 - 1 - MARGIN, y7 + MARGIN, mWinPaint); 251 | } 252 | } 253 | 254 | @Override 255 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 256 | // Keep the view squared 257 | int w = MeasureSpec.getSize(widthMeasureSpec); 258 | int h = MeasureSpec.getSize(heightMeasureSpec); 259 | int d = w == 0 ? h : h == 0 ? w : w < h ? w : h; 260 | setMeasuredDimension(d, d); 261 | } 262 | 263 | @Override 264 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 265 | super.onSizeChanged(w, h, oldw, oldh); 266 | 267 | int sx = (w - 2 * MARGIN) / 3; 268 | int sy = (h - 2 * MARGIN) / 3; 269 | 270 | int size = sx < sy ? sx : sy; 271 | 272 | mSxy = size; 273 | mOffetX = (w - 3 * size) / 2; 274 | mOffetY = (h - 3 * size) / 2; 275 | 276 | mDstRect.set(MARGIN, MARGIN, size - MARGIN, size - MARGIN); 277 | } 278 | 279 | @Override 280 | public boolean onTouchEvent(MotionEvent event) { 281 | int action = event.getAction(); 282 | 283 | if (action == MotionEvent.ACTION_DOWN) { 284 | return true; 285 | 286 | } else if (action == MotionEvent.ACTION_UP) { 287 | int x = (int) event.getX(); 288 | int y = (int) event.getY(); 289 | 290 | int sxy = mSxy; 291 | x = (x - MARGIN) / sxy; 292 | y = (y - MARGIN) / sxy; 293 | 294 | if (isEnabled() && x >= 0 && x < 3 && y >= 0 & y < 3) { 295 | int cell = x + 3 * y; 296 | 297 | State state = cell == mSelectedCell ? mSelectedValue : mData[cell]; 298 | state = state == State.EMPTY ? mCurrentPlayer : State.EMPTY; 299 | 300 | stopBlink(); 301 | 302 | mSelectedCell = cell; 303 | mSelectedValue = state; 304 | mBlinkDisplayOff = false; 305 | mBlinkRect.set(MARGIN + x * sxy, MARGIN + y * sxy, 306 | MARGIN + (x + 1) * sxy, MARGIN + (y + 1) * sxy); 307 | 308 | if (state != State.EMPTY) { 309 | // Start the blinker 310 | mHandler.sendEmptyMessageDelayed(MSG_BLINK, FPS_MS); 311 | } 312 | 313 | if (mCellListener != null) { 314 | mCellListener.onCellSelected(); 315 | } 316 | } 317 | 318 | return true; 319 | } 320 | 321 | return false; 322 | } 323 | 324 | public void stopBlink() { 325 | boolean hadSelection = mSelectedCell != -1 && mSelectedValue != State.EMPTY; 326 | mSelectedCell = -1; 327 | mSelectedValue = State.EMPTY; 328 | if (!mBlinkRect.isEmpty()) { 329 | invalidate(mBlinkRect); 330 | } 331 | mBlinkDisplayOff = false; 332 | mBlinkRect.setEmpty(); 333 | mHandler.removeMessages(MSG_BLINK); 334 | if (hadSelection && mCellListener != null) { 335 | mCellListener.onCellSelected(); 336 | } 337 | } 338 | 339 | @Override 340 | protected Parcelable onSaveInstanceState() { 341 | Bundle b = new Bundle(); 342 | 343 | Parcelable s = super.onSaveInstanceState(); 344 | b.putParcelable("gv_super_state", s); 345 | 346 | b.putBoolean("gv_en", isEnabled()); 347 | 348 | int[] data = new int[mData.length]; 349 | for (int i = 0; i < data.length; i++) { 350 | data[i] = mData[i].getValue(); 351 | } 352 | b.putIntArray("gv_data", data); 353 | 354 | b.putInt("gv_sel_cell", mSelectedCell); 355 | b.putInt("gv_sel_val", mSelectedValue.getValue()); 356 | b.putInt("gv_curr_play", mCurrentPlayer.getValue()); 357 | b.putInt("gv_winner", mWinner.getValue()); 358 | 359 | b.putInt("gv_win_col", mWinCol); 360 | b.putInt("gv_win_row", mWinRow); 361 | b.putInt("gv_win_diag", mWinDiag); 362 | 363 | b.putBoolean("gv_blink_off", mBlinkDisplayOff); 364 | b.putParcelable("gv_blink_rect", mBlinkRect); 365 | 366 | return b; 367 | } 368 | 369 | @Override 370 | protected void onRestoreInstanceState(Parcelable state) { 371 | 372 | if (!(state instanceof Bundle)) { 373 | // Not supposed to happen. 374 | super.onRestoreInstanceState(state); 375 | return; 376 | } 377 | 378 | Bundle b = (Bundle) state; 379 | Parcelable superState = b.getParcelable("gv_super_state"); 380 | 381 | setEnabled(b.getBoolean("gv_en", true)); 382 | 383 | int[] data = b.getIntArray("gv_data"); 384 | if (data != null && data.length == mData.length) { 385 | for (int i = 0; i < data.length; i++) { 386 | mData[i] = State.fromInt(data[i]); 387 | } 388 | } 389 | 390 | mSelectedCell = b.getInt("gv_sel_cell", -1); 391 | mSelectedValue = State.fromInt(b.getInt("gv_sel_val", State.EMPTY.getValue())); 392 | mCurrentPlayer = State.fromInt(b.getInt("gv_curr_play", State.EMPTY.getValue())); 393 | mWinner = State.fromInt(b.getInt("gv_winner", State.EMPTY.getValue())); 394 | 395 | mWinCol = b.getInt("gv_win_col", -1); 396 | mWinRow = b.getInt("gv_win_row", -1); 397 | mWinDiag = b.getInt("gv_win_diag", -1); 398 | 399 | mBlinkDisplayOff = b.getBoolean("gv_blink_off", false); 400 | Rect r = b.getParcelable("gv_blink_rect"); 401 | if (r != null) { 402 | mBlinkRect.set(r); 403 | } 404 | 405 | // let the blink handler decide if it should blink or not 406 | mHandler.sendEmptyMessage(MSG_BLINK); 407 | 408 | super.onRestoreInstanceState(superState); 409 | } 410 | 411 | //----- 412 | 413 | private class MyHandler implements Callback { 414 | public boolean handleMessage(Message msg) { 415 | if (msg.what == MSG_BLINK) { 416 | if (mSelectedCell >= 0 && mSelectedValue != State.EMPTY && mBlinkRect.top != 0) { 417 | mBlinkDisplayOff = !mBlinkDisplayOff; 418 | invalidate(mBlinkRect); 419 | 420 | if (!mHandler.hasMessages(MSG_BLINK)) { 421 | mHandler.sendEmptyMessageDelayed(MSG_BLINK, FPS_MS); 422 | } 423 | } 424 | return true; 425 | } 426 | return false; 427 | } 428 | } 429 | 430 | private Bitmap getResBitmap(int bmpResId) { 431 | Options opts = new Options(); 432 | opts.inDither = false; 433 | 434 | Resources res = getResources(); 435 | Bitmap bmp = BitmapFactory.decodeResource(res, bmpResId, opts); 436 | 437 | if (bmp == null && isInEditMode()) { 438 | // BitmapFactory.decodeResource doesn't work from the rendering 439 | // library in Eclipse's Graphical Layout Editor. Use this workaround instead. 440 | 441 | Drawable d = res.getDrawable(bmpResId); 442 | int w = d.getIntrinsicWidth(); 443 | int h = d.getIntrinsicHeight(); 444 | bmp = Bitmap.createBitmap(w, h, Config.ARGB_8888); 445 | Canvas c = new Canvas(bmp); 446 | d.setBounds(0, 0, w - 1, h - 1); 447 | d.draw(c); 448 | } 449 | 450 | return bmp; 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /demo/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /demo/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /demo/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /demo/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable-hdpi/cat1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-hdpi/cat1.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-hdpi/cat2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-hdpi/cat2.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-hdpi/cat3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-hdpi/cat3.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-hdpi/cat4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-hdpi/cat4.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-hdpi/cat5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-hdpi/cat5.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-hdpi/catvision_logo_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-hdpi/catvision_logo_shadow.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-ldpi/cat1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-ldpi/cat1.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-ldpi/cat2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-ldpi/cat2.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-ldpi/cat3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-ldpi/cat3.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-ldpi/cat4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-ldpi/cat4.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-ldpi/cat5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-ldpi/cat5.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-ldpi/catvision_logo_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-ldpi/catvision_logo_shadow.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-mdpi/cat1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-mdpi/cat1.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-mdpi/cat2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-mdpi/cat2.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-mdpi/cat3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-mdpi/cat3.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-mdpi/cat4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-mdpi/cat4.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-mdpi/cat5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-mdpi/cat5.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-mdpi/catvision_logo_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-mdpi/catvision_logo_shadow.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/cat1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xhdpi/cat1.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/cat2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xhdpi/cat2.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/cat3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xhdpi/cat3.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/cat4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xhdpi/cat4.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/cat5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xhdpi/cat5.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/catvision_logo_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xhdpi/catvision_logo_shadow.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxhdpi/cat1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxhdpi/cat1.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxhdpi/cat2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxhdpi/cat2.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxhdpi/cat3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxhdpi/cat3.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxhdpi/cat4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxhdpi/cat4.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxhdpi/cat5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxhdpi/cat5.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxhdpi/catvision_logo_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxhdpi/catvision_logo_shadow.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxxhdpi/cat1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxxhdpi/cat1.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxxhdpi/cat2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxxhdpi/cat2.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxxhdpi/cat3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxxhdpi/cat3.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxxhdpi/cat4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxxhdpi/cat4.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxxhdpi/cat5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxxhdpi/cat5.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxxhdpi/catvision_logo_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable-xxxhdpi/catvision_logo_shadow.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable/catvision_blink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable/catvision_blink.gif -------------------------------------------------------------------------------- /demo/src/main/res/drawable/catvision_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable/catvision_logo.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 31 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 73 | 74 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/lib_bg.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable/lib_bg.9.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable/lib_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable/lib_circle.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable/lib_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable/lib_cross.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable/qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable/qr_code.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable/seacat_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable/seacat_logo.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable/teskalabs_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable/teskalabs_logo.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable/touch_browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeskaLabs/CatVision-io-SDK-Android/158b6cdabb2544a2ca830e360cd737b2f0dfc2ff/demo/src/main/res/drawable/touch_browser.png -------------------------------------------------------------------------------- /demo/src/main/res/layout-land/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 23 | 24 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 57 | 58 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /demo/src/main/res/layout-land/content_api_key_obtainer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 17 | 18 | 24 | 25 | 36 | 37 |