├── settings.gradle ├── media ├── flightgear.gif └── thumbstick.gif ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── app ├── src │ └── main │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── yoke.png │ │ ├── mipmap-mdpi │ │ │ └── yoke.png │ │ ├── mipmap-xhdpi │ │ │ └── yoke.png │ │ ├── mipmap-xxhdpi │ │ │ └── yoke.png │ │ ├── mipmap-xxxhdpi │ │ │ └── yoke.png │ │ ├── drawable-hdpi │ │ │ ├── baseline_refresh_white_18.png │ │ │ ├── baseline_refresh_white_24.png │ │ │ ├── baseline_refresh_white_36.png │ │ │ ├── baseline_refresh_white_48.png │ │ │ ├── baseline_more_vert_white_18.png │ │ │ ├── baseline_more_vert_white_24.png │ │ │ ├── baseline_more_vert_white_36.png │ │ │ └── baseline_more_vert_white_48.png │ │ ├── drawable-mdpi │ │ │ ├── baseline_refresh_white_18.png │ │ │ ├── baseline_refresh_white_24.png │ │ │ ├── baseline_refresh_white_36.png │ │ │ ├── baseline_refresh_white_48.png │ │ │ ├── baseline_more_vert_white_18.png │ │ │ ├── baseline_more_vert_white_24.png │ │ │ ├── baseline_more_vert_white_36.png │ │ │ └── baseline_more_vert_white_48.png │ │ ├── drawable-xhdpi │ │ │ ├── baseline_refresh_white_18.png │ │ │ ├── baseline_refresh_white_24.png │ │ │ ├── baseline_refresh_white_36.png │ │ │ ├── baseline_refresh_white_48.png │ │ │ ├── baseline_more_vert_white_18.png │ │ │ ├── baseline_more_vert_white_24.png │ │ │ ├── baseline_more_vert_white_36.png │ │ │ └── baseline_more_vert_white_48.png │ │ ├── drawable-xxhdpi │ │ │ ├── baseline_refresh_white_18.png │ │ │ ├── baseline_refresh_white_24.png │ │ │ ├── baseline_refresh_white_36.png │ │ │ ├── baseline_refresh_white_48.png │ │ │ ├── baseline_more_vert_white_18.png │ │ │ ├── baseline_more_vert_white_24.png │ │ │ ├── baseline_more_vert_white_36.png │ │ │ └── baseline_more_vert_white_48.png │ │ ├── drawable-xxxhdpi │ │ │ ├── baseline_more_vert_white_18.png │ │ │ ├── baseline_more_vert_white_24.png │ │ │ ├── baseline_more_vert_white_36.png │ │ │ ├── baseline_more_vert_white_48.png │ │ │ ├── baseline_refresh_white_18.png │ │ │ ├── baseline_refresh_white_24.png │ │ │ ├── baseline_refresh_white_36.png │ │ │ └── baseline_refresh_white_48.png │ │ ├── menu │ │ │ └── overflow.xml │ │ ├── drawable │ │ │ ├── baseline_more_vert_24.xml │ │ │ └── baseline_refresh_24.xml │ │ ├── values │ │ │ ├── styles.xml │ │ │ └── strings.xml │ │ ├── layout │ │ │ └── main_wv.xml │ │ ├── values-pt-rBR │ │ │ └── strings.xml │ │ └── values-es │ │ │ └── strings.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── simonramstedt │ │ └── yoke │ │ └── YokeActivity.java ├── release │ └── output.json └── build.gradle ├── README.md ├── LICENSE ├── .gitignore ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /media/flightgear.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/media/flightgear.gif -------------------------------------------------------------------------------- /media/thumbstick.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/media/thumbstick.gif -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/yoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/mipmap-hdpi/yoke.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/yoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/mipmap-mdpi/yoke.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/yoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/mipmap-xhdpi/yoke.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/yoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/mipmap-xxhdpi/yoke.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/yoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/mipmap-xxxhdpi/yoke.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_refresh_white_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-hdpi/baseline_refresh_white_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_refresh_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-hdpi/baseline_refresh_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_refresh_white_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-hdpi/baseline_refresh_white_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_refresh_white_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-hdpi/baseline_refresh_white_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_refresh_white_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-mdpi/baseline_refresh_white_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_refresh_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-mdpi/baseline_refresh_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_refresh_white_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-mdpi/baseline_refresh_white_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_refresh_white_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-mdpi/baseline_refresh_white_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_more_vert_white_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-hdpi/baseline_more_vert_white_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_more_vert_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-hdpi/baseline_more_vert_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_more_vert_white_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-hdpi/baseline_more_vert_white_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_more_vert_white_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-hdpi/baseline_more_vert_white_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_more_vert_white_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-mdpi/baseline_more_vert_white_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_more_vert_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-mdpi/baseline_more_vert_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_more_vert_white_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-mdpi/baseline_more_vert_white_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_more_vert_white_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-mdpi/baseline_more_vert_white_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_refresh_white_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xhdpi/baseline_refresh_white_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_refresh_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xhdpi/baseline_refresh_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_refresh_white_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xhdpi/baseline_refresh_white_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_refresh_white_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xhdpi/baseline_refresh_white_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_refresh_white_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxhdpi/baseline_refresh_white_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_refresh_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxhdpi/baseline_refresh_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_refresh_white_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxhdpi/baseline_refresh_white_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_refresh_white_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxhdpi/baseline_refresh_white_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_more_vert_white_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xhdpi/baseline_more_vert_white_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_more_vert_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xhdpi/baseline_more_vert_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_more_vert_white_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xhdpi/baseline_more_vert_white_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_more_vert_white_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xhdpi/baseline_more_vert_white_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_more_vert_white_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxhdpi/baseline_more_vert_white_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_more_vert_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxhdpi/baseline_more_vert_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_more_vert_white_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxhdpi/baseline_more_vert_white_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_more_vert_white_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxhdpi/baseline_more_vert_white_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_more_vert_white_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_more_vert_white_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_more_vert_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_more_vert_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_more_vert_white_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_more_vert_white_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_more_vert_white_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_more_vert_white_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_refresh_white_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_refresh_white_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_refresh_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_refresh_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_refresh_white_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_refresh_white_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_refresh_white_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmst/yoke-android/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_refresh_white_48.png -------------------------------------------------------------------------------- /app/release/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":4,"versionName":"0.1.2","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] -------------------------------------------------------------------------------- /app/src/main/res/menu/overflow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Aug 18 10:47:52 EDT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yoke (Android app) 2 | 3 | [Get it on F-Droid](https://f-droid.org/packages/com.simonramstedt.yoke/) 6 | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.simonramstedt.yoke) 9 | 10 | ![Flightgear](media/flightgear.gif) 11 | 12 | See https://github.com/rmst/yoke for more. 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_more_vert_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_refresh_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | android { 3 | compileSdkVersion 29 4 | defaultConfig { 5 | applicationId 'com.simonramstedt.yoke' 6 | minSdkVersion 21 7 | targetSdkVersion 29 8 | } 9 | buildTypes { 10 | release { 11 | minifyEnabled true 12 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 13 | } 14 | } 15 | compileOptions { 16 | sourceCompatibility JavaVersion.VERSION_1_8 17 | targetCompatibility JavaVersion.VERSION_1_8 18 | } 19 | productFlavors { 20 | } 21 | lintOptions { 22 | disable 'SetJavaScriptEnabled' 23 | } 24 | } 25 | 26 | dependencies { 27 | } 28 | 29 | repositories { 30 | google() 31 | jcenter() 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Simon Ramstedt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | spare-parts 3 | 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the ART/Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | out/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # IntelliJ 39 | *.iml 40 | .idea/workspace.xml 41 | .idea/tasks.xml 42 | .idea/gradle.xml 43 | .idea/assetWizardSettings.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | .idea/caches 47 | 48 | # Keystore files 49 | # Uncomment the following line if you do not want to check your keystore files in. 50 | #*.jks 51 | 52 | # External native build folder generated in Android Studio 2.2 and later 53 | .externalNativeBuild 54 | 55 | # Google Services (e.g. APIs or Firebase) 56 | google-services.json 57 | 58 | # Freeline 59 | freeline.py 60 | freeline/ 61 | freeline_project_description.json 62 | 63 | # fastlane 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | fastlane/readme.md 69 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_wv.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 14 | 15 | 20 | 21 | 31 | 32 | 40 | 41 | 48 | 49 | 59 | 60 | 61 | 62 | 68 | 69 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | Yoke 6 | 7 | 8 | Conectado para 9 | Conectar para 10 | 11 | Reconectar 12 | Mais opções 13 | 14 | 15 | Icrementar gamepad 16 | 17 | 18 | Por favor usar menu de listagem para conectar a máquina. 19 | Voce precisa baixar um gamepad. Aperte o ícone ⋮ então "%1$s", depois ↻ "%2$s". 20 | Endereço inválido. 21 | 22 | Gamepad incrementado com sucesso. 23 | 24 | 25 | Conectar para… 26 | → nada 27 | → nova conexão manual 28 | 29 | 30 | Insira o endereço IP e a porta 31 | ex. 192.168.1.123:11111 32 | OK 33 | Cancelar 34 | 35 | 37 | Falha serviço descoberto com código de erro %1$d. 38 | Falha em abrir socket UDP para %1$s:%2$d. 39 | Diretório não criado %1$s 40 | Não é possível apagar %1$s 41 | Não é possível renomear %1$s 42 | Erro de Leitura/Escrita (IOException): %1$s. 43 | Erro de parsing (JSONException): %1$s. 44 | Conexão recusada. Verifique se o Yoke está rodando no seu PC. 45 | Um Geranciador De Segurança impede o Yoke de ler or escrever arquivos (SecurityException). 46 | Um Gerenciador De Segurança impede o Yoke de se comunicar com seu PC. 47 | Serviço nulo. 48 | OK 49 | 50 | Mensagem do seu gamepad: 51 | OK 52 | 53 | 54 | Adicionando %1$s… 55 | Fechando conexão… 56 | Criando diretório local %1$s 57 | Conectando diretamente para o endereço de IP %1$s… 58 | Baixando arquivo para %1$s 59 | Descoberta de serviço \"%1$s\" iniciada. 60 | Descoberta de serviço \"%1$s\" interrompida. 61 | Carregando de %1$s 62 | Nada selecionado. 63 | Conectado. 64 | Tentando abrir socket UDP para %1$s na porta %2$d… 65 | Serviço encontrado. 66 | Service perdido. 67 | Serviço de descoberta com falha, código de erro %1$d. 68 | Serviço \"%1$s\" resolvido com sucesso. 69 | Resolvendo serviço \"%1$s\"… 70 | Novo serviço alvo: \"%1$s\". 71 | Girador alterado automaticamente para "%1$s" (pos %2$d). 72 | "%1$s" (pos %2$d) selecionado no girador. 73 | Conexão fechada. 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | Yoke 6 | 7 | 8 | Connected to 9 | Connect to 10 | 11 | Reconnect 12 | More options 13 | 14 | 15 | Upgrade gamepad 16 | 17 | 18 | Please use the dropdown menu to connect to a machine. 19 | You need to download a gamepad. Click the ⋮ menu button, then "%1$s", then ↻ "%2$s". 20 | Invalid address. 21 | 22 | Gamepad succesfully upgraded. 23 | 24 | 25 | Connect to… 26 | → nothing 27 | → new manual connection 28 | 29 | 30 | Enter IP address and port 31 | e.g. 192.168.1.123:11111 32 | OK 33 | Cancel 34 | 35 | 37 | Service discovery failed with error code %1$d. 38 | Failed to open UDP socket to %1$s:%2$d. 39 | Failed to connect to %1$s 40 | Could not create folder %1$s 41 | Could not delete %1$s 42 | Could not rename %1$s 43 | Read/write error (IOException): %1$s. 44 | Error parsing manifest (JSONException): %1$s. 45 | Webserver not found. Please check if Yoke is running on your PC. 46 | Connection refused. Please check if Yoke is running on your PC. 47 | The Yoke client for PC has stopped. You can check your PC screen for more information. 48 | No Internet connection. Please make sure this device is connected to your network. 49 | A Security Manager is preventing Yoke from reading or writing files (SecurityException). 50 | A Security Manager is preventing Yoke from communicating with your PC. 51 | Service null. 52 | OK 53 | 54 | Message from your gamepad: 55 | OK 56 | 57 | 58 | Adding %1$s… 59 | Closing connection… 60 | Creating local folder %1$s 61 | Connecting directly to IP address %1$s… 62 | Downloading file to %1$s 63 | Service discovery \"%1$s\" started. 64 | Service discovery \"%1$s\" stopped. 65 | Loading from %1$s 66 | Nothing selected. 67 | Connected. 68 | Trying to open UDP socket to %1$s on port %2$d… 69 | Service found. 70 | Service lost. 71 | Resolve failed with error code %1$d. 72 | Service \"%1$s\" succesfully resolved. 73 | Resolving service \"%1$s\"… 74 | New service target: \"%1$s\". 75 | Spinner set automatically to "%1$s" (pos %2$d). 76 | "%1$s" (pos %2$d) selected in the spinner. 77 | Connection closed. 78 | Sent disconnect signal. 79 | 80 | 81 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /app/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | Yoke 6 | 7 | 8 | Conectado a 9 | Conectar a 10 | 11 | Reconectar 12 | Más opciones 13 | 14 | 15 | Actualizar mando 16 | 17 | 18 | Por favor, usa el menú desplegable para conectarte a una máquina. 19 | Es necesario descargar un mando. Haz clic en el ⋮ botón menú, luego en «%1$s» y luego en ↻ «%2$s». 20 | Dirección inválida. 21 | 22 | Mando actualizado con éxito. 23 | 24 | 25 | Conectar a… 26 | → nada 27 | → nueva conexión manual 28 | 29 | 30 | Introduce dirección IP y puerto 31 | p. ej., 192.168.1.123:11111 32 | Vale 33 | Cancelar 34 | 35 | 37 | El descubrimiento de servicios ha fallado con código de error %1$d. 38 | No se pudo abrir un socket UDP a %1$s:%2$d. 39 | No se pudo crear la carpeta %1$s 40 | No se pudo borrar %1$s 41 | No se pudo renombrar %1$s 42 | Error de lectura/escritura (IOException): %1$s. 43 | Error interpretando manifiesto (JSONException): %1$s. 44 | No se encontró el servidor web. Por favor, comprueba si Yoke fue iniciado en tu ordenador. 45 | Conexión rechazada. Por favor, comprueba si Yoke fue iniciado en tu ordenador. 46 | El cliente Yoke para PC se ha detenido. Puedes ver más información en tu ordenador. 47 | No hay conexión a internet. Comprueba que este dispositivo está conectado a tu red. 48 | Un gestor de seguridad impide a Yoke leer o escribir archivos (SecurityException). 49 | Un gestor de seguridad impide a Yoke comunicarse con tu ordenador. 50 | Servicio nulo. 51 | Vale 52 | 53 | Mensaje de tu mando: 54 | Vale 55 | 56 | 57 | Añadiendo %1$s… 58 | Cerrando conexión… 59 | Creando carpeta local %1$s 60 | Conectando directamente a dirección IP %1$s… 61 | Descargando archivo a %1$s 62 | Comenzó el descubrimiento de servicios «%1$s». 63 | Se detuvo el descubrimiento de servicios «%1$s». 64 | Cargando desde %1$s 65 | No hay nada seleccionado. 66 | Conectado. 67 | Intentando abrir socket UDP a %1$s en el puerto %2$d… 68 | Servicio encontrado. 69 | Servicio perdido. 70 | La resolución del servicio falló con código de error %1$d. 71 | Servicio «%1$s» resuelto con éxito. 72 | Resolviendo servicio «%1$s»… 73 | Nuevo servicio destino: «%1$s». 74 | Spinner cambiado automáticamente a "%1$s" (pos %2$d). 75 | «%1$s» (pos %2$d) seleccionado en el spinner. 76 | Conexión cerrada. 77 | Enviada la señal de desconexión. 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonramstedt/yoke/YokeActivity.java: -------------------------------------------------------------------------------- 1 | package com.simonramstedt.yoke; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.content.SharedPreferences; 8 | import android.content.res.Resources; 9 | import android.net.nsd.NsdManager; 10 | import android.net.nsd.NsdServiceInfo; 11 | import android.os.AsyncTask; 12 | import android.os.Bundle; 13 | import android.os.Handler; 14 | import android.text.InputType; 15 | import android.util.Log; 16 | import android.view.Display; 17 | import android.view.MenuItem; 18 | import android.view.View; 19 | import android.view.WindowManager; 20 | import android.webkit.JavascriptInterface; 21 | import android.webkit.WebView; 22 | import android.widget.AdapterView; 23 | import android.widget.ArrayAdapter; 24 | import android.widget.EditText; 25 | import android.widget.PopupMenu; 26 | import android.widget.ProgressBar; 27 | import android.widget.Spinner; 28 | import android.widget.TextView; 29 | import android.widget.Toast; 30 | 31 | import java.io.BufferedInputStream; 32 | import java.io.BufferedReader; 33 | import java.io.File; 34 | import java.io.FileOutputStream; 35 | import java.io.FileReader; 36 | import java.io.IOException; 37 | import java.io.InputStream; 38 | import java.io.OutputStream; 39 | import java.lang.SecurityException; 40 | import java.lang.StackTraceElement; 41 | import java.net.DatagramPacket; 42 | import java.net.DatagramSocket; 43 | import java.net.InetAddress; 44 | import java.net.ServerSocket; 45 | import java.net.SocketException; 46 | import java.net.URL; 47 | import java.net.URLConnection; 48 | import java.net.UnknownHostException; 49 | import java.nio.charset.Charset; 50 | import java.util.Arrays; 51 | import java.util.ArrayList; 52 | import java.util.HashMap; 53 | import java.util.List; 54 | import java.util.Map; 55 | 56 | import org.json.JSONArray; 57 | import org.json.JSONObject; 58 | import org.json.JSONException; 59 | 60 | 61 | public class YokeActivity extends Activity implements NsdManager.DiscoveryListener { 62 | private final String SERVICE_TYPE = "_yoke._udp."; 63 | private String NOTHING; 64 | private String ENTER_IP; 65 | private NsdManager mNsdManager; 66 | private NsdServiceInfo mService; 67 | private DatagramSocket mSocket; 68 | private byte[] vals_buffer = null; 69 | private byte[] disconnect_pattern = null; 70 | private final int poll_rate = 20; 71 | private Map mServiceMap = new HashMap<>(); 72 | private List mServiceNames = new ArrayList<>(); 73 | private SharedPreferences sharedPref; 74 | private TextView mTextView; 75 | private Spinner mSpinner; 76 | private volatile boolean mSpinnerAutomatic = false; 77 | private String oldtgt; 78 | private ProgressBar mProgressBar; 79 | private ArrayAdapter mAdapter; 80 | private Handler handler; 81 | private WebView wv; 82 | private Resources res; 83 | private File currentJoypadPath; 84 | private File currentMainPath; 85 | private File currentManifestPath; 86 | private File futureJoypadPath; 87 | private File futureMainPath; 88 | private File futureManifestPath; 89 | private String currentHost = null; 90 | private int currentPort = 0; // the value is irrelevant if currentHost is null 91 | 92 | private void log(String m) { 93 | if (BuildConfig.DEBUG) 94 | Log.d("Yoke", m); 95 | } 96 | 97 | private void logError(String m, Exception e) { 98 | Log.e("Yoke", m); 99 | if (e != null) { 100 | StringBuilder sb = new StringBuilder(); 101 | for (StackTraceElement element : e.getStackTrace()) { 102 | sb.append(element.toString()); 103 | sb.append("\n"); 104 | } 105 | Log.e("Yoke", sb.toString()); 106 | } 107 | YokeActivity.this.runOnUiThread(() -> { 108 | AlertDialog.Builder builder = new AlertDialog.Builder(YokeActivity.this); 109 | builder.setMessage(m); 110 | builder.setNegativeButton(R.string.dismiss_error, new DialogInterface.OnClickListener() { 111 | public void onClick(DialogInterface dialog, int id) { 112 | dialog.cancel(); 113 | } 114 | }); 115 | builder.show(); 116 | }); 117 | } 118 | 119 | private void logInfo(String m) { 120 | Log.i("Yoke", m); 121 | YokeActivity.this.runOnUiThread(() -> { 122 | AlertDialog.Builder builder = new AlertDialog.Builder(YokeActivity.this); 123 | builder.setMessage(m); 124 | builder.setNegativeButton(R.string.dismiss_error, new DialogInterface.OnClickListener() { 125 | public void onClick(DialogInterface dialog, int id) { 126 | dialog.cancel(); 127 | } 128 | }); 129 | builder.show(); 130 | }); 131 | } 132 | 133 | private void deleteRecursively(File joypadPath) throws IOException { 134 | // Deleting a folder without emptying it first fails or raises an error, 135 | // and there is no one-liner in this version to empty its contents. So: 136 | ArrayList erasables = new ArrayList<>(); 137 | erasables.add(joypadPath); 138 | for (int i = 0; i < erasables.size(); i++) { 139 | // don't optimize the line above. The ArrayList will be modified at every iteration. 140 | File folder = erasables.get(i); 141 | if (folder.isDirectory()) { 142 | for (String entry : folder.list()) { 143 | erasables.add(new File(folder, entry)); 144 | } 145 | } 146 | } 147 | // Files should already be ordered by depth, so we iterate backwards: 148 | for (int i = erasables.size() - 1; i >= 0; i--) { 149 | File currentFile = erasables.get(i); 150 | if (!currentFile.delete()) { 151 | throw new IOException(String.format(res.getString(R.string.error_could_not_delete), currentFile)); 152 | } 153 | } 154 | } 155 | 156 | class WebAppInterface { 157 | Context mContext; 158 | 159 | // Instantiate the interface and set the context 160 | WebAppInterface(Context c) { 161 | mContext = c; 162 | } 163 | 164 | // The following methods are called from within the WebView. 165 | 166 | /* update_vals(String vals) sends an ISO-8859-1 string containing the 167 | * current gamepad status, which is sent to the PC client as a stream of 168 | * bytes. 169 | * 170 | * ArrayBuffers can be sent by encoding them as an ISO-8859-1 string first. 171 | * There doesn't seem to be a way to send an arbitrary number of 172 | * bytes directly. 173 | */ 174 | @JavascriptInterface 175 | public void update_vals(String vals) { 176 | vals_buffer = vals.getBytes(Charset.forName("ISO-8859-1")); 177 | } 178 | 179 | /* Set the disconnection pattern. 180 | * If set, Yoke will send three times this pattern in the event of deliberate 181 | * disconnection (when the user taps the Reconnect button or selects a different 182 | * option from the spinner). 183 | * 184 | * This pattern is unset automatically every time a new page loads. 185 | */ 186 | @JavascriptInterface 187 | public void set_bye(String m) { 188 | disconnect_pattern = m.getBytes(Charset.forName("ISO-8859-1")); 189 | } 190 | 191 | /* alert() displays a pop-up on the Yoke app. This is intended as a replacement 192 | * of the JavaScript alert() function, as prompts can't be shown on a WebView. 193 | * 194 | * Unlike JS's native function, this function doesn't block the JavaScript thread. 195 | */ 196 | @JavascriptInterface 197 | public void alert(String m) { 198 | AlertDialog.Builder builder = new AlertDialog.Builder(YokeActivity.this, R.style.WebviewPrompt); 199 | builder.setTitle(R.string.alert_from_webview); 200 | builder.setMessage(m); 201 | builder.setNegativeButton(R.string.dismiss_alert, new DialogInterface.OnClickListener() { 202 | public void onClick(DialogInterface dialog, int id) { 203 | dialog.cancel(); 204 | } 205 | }); 206 | builder.show(); 207 | } 208 | } 209 | 210 | // https://stackoverflow.com/questions/15758856/android-how-to-download-file-from-webserver/ 211 | class DownloadFilesFromURL extends AsyncTask { 212 | // This class can be used in two ways: 213 | // with one argument, parses the manifest and downloads the content from the entire webserver. 214 | // with two arguments, downloads a specific file. For the moment we use this to download the manifest 215 | // as a crude ping and a way to ensure the IP is actually running Yoke. 216 | 217 | private final long INDETERMINATE = -1L; 218 | private final long DETERMINATE = -2L; 219 | private final long UPDATE_SUCCESS = -8L; 220 | private final long PING_SUCCESS = -16L; 221 | 222 | private final int MAX_PROGRESS = 1000; 223 | 224 | @Override 225 | protected void onPreExecute() { 226 | super.onPreExecute(); 227 | try { 228 | if (futureJoypadPath.exists()) { 229 | deleteRecursively(futureJoypadPath); 230 | } 231 | log(String.format( 232 | res.getString(R.string.log_creating_folder), futureJoypadPath.getAbsolutePath() 233 | )); 234 | if (!futureJoypadPath.mkdirs()) { 235 | throw new IOException(String.format( 236 | res.getString(R.string.error_could_not_create_folder), futureJoypadPath.getAbsolutePath() 237 | )); 238 | } 239 | } catch (SecurityException e) { 240 | logError(res.getString(R.string.error_security_exception), e); 241 | cancel(true); 242 | } catch (IOException e) { 243 | logError(e.getMessage(), e); 244 | cancel(true); 245 | } 246 | } 247 | 248 | @Override 249 | protected Long doInBackground(String... f_url) { 250 | StringBuilder manifestSB = new StringBuilder(); 251 | byte data[] = new byte[1024]; 252 | try { 253 | if (f_url.length == 1) { 254 | // Download the whole server 255 | URL url = new URL(f_url[0] + "manifest.json"); 256 | InputStream input = new BufferedInputStream(url.openStream(), 8192); 257 | OutputStream output = new FileOutputStream(futureManifestPath); 258 | int count; 259 | publishProgress(0L, INDETERMINATE); 260 | while ((count = input.read(data)) != -1) { 261 | output.write(data, 0, count); 262 | } 263 | BufferedReader inputBR = new BufferedReader( 264 | new FileReader(futureManifestPath) 265 | ); 266 | String line = ""; 267 | while ((line = inputBR.readLine()) != null) { 268 | manifestSB.append(line).append("\n"); 269 | } 270 | JSONObject manifestJSON = new JSONObject(manifestSB.toString()); 271 | 272 | long totalBytes = manifestJSON.optLong("size", INDETERMINATE); 273 | if (totalBytes != INDETERMINATE) 274 | publishProgress(0L, DETERMINATE); 275 | JSONArray entries = manifestJSON.getJSONArray("folders"); 276 | for (int i = 0, length = entries.length(); i < length; i++) { 277 | File newFolder = new File(futureJoypadPath, entries.getString(i)); 278 | log(String.format(res.getString(R.string.log_creating_folder), newFolder.getAbsolutePath())); 279 | if (!newFolder.mkdirs()) { 280 | throw new IOException(String.format( 281 | res.getString(R.string.error_could_not_create_folder), newFolder.getAbsolutePath() 282 | )); 283 | } 284 | } 285 | 286 | entries = manifestJSON.getJSONArray("files"); 287 | long cumulativeBytes = 0; 288 | for (int i = 0, length=entries.length(); i < length; i++) { 289 | File newFile = new File(futureJoypadPath, entries.getString(i)); 290 | log(String.format(res.getString(R.string.log_downloading_file), newFile.getAbsolutePath())); 291 | input = new BufferedInputStream(new URL(f_url[0] + entries.getString(i)).openStream(), 8192); 292 | output = new FileOutputStream(newFile); 293 | 294 | while ((count = input.read(data)) != -1) { 295 | cumulativeBytes += count; 296 | output.write(data, 0, count); 297 | publishProgress(cumulativeBytes, totalBytes); 298 | } 299 | 300 | output.flush(); 301 | output.close(); 302 | input.close(); 303 | } 304 | return UPDATE_SUCCESS; 305 | } else { 306 | // download just one file 307 | URL url = new URL(f_url[0] + "/" + f_url[1]); 308 | InputStream input = new BufferedInputStream(url.openStream(), 8192); 309 | publishProgress(0L, INDETERMINATE); 310 | while (input.read(data) != -1) { } 311 | return PING_SUCCESS; 312 | } 313 | } catch (IOException e) { 314 | if (e.getMessage().contains(" ECONNREFUSED ")) { 315 | logError(res.getString(R.string.error_connection_refused), e); 316 | } else if (e.getMessage().contains(" ENETUNREACH ")) { 317 | logError(res.getString(R.string.error_network_unreachable), e); 318 | } else if (e.getMessage().startsWith("Failed to connect to")) { 319 | logError(res.getString(R.string.error_failed_download), e); 320 | } else { 321 | logError(e.getMessage(), e); 322 | } 323 | cancel(true); 324 | } catch (JSONException e) { 325 | logError(String.format(res.getString(R.string.error_json_exception), e.getLocalizedMessage()), e); 326 | cancel(true); 327 | } catch (SecurityException e) { 328 | logError(res.getString(R.string.error_security_exception), e); 329 | cancel(true); 330 | } 331 | return 0L; 332 | } 333 | 334 | protected void onProgressUpdate(Long... progress) { 335 | if (progress[1] == INDETERMINATE) { 336 | mProgressBar.setIndeterminate(true); 337 | mProgressBar.setVisibility(View.VISIBLE); 338 | } else if (progress[1] == DETERMINATE) { 339 | mTextView.setText(res.getString(R.string.toolbar_connected_to)); 340 | mProgressBar.setIndeterminate(false); 341 | mProgressBar.setProgress(0); 342 | mProgressBar.setMax((int)MAX_PROGRESS); 343 | mProgressBar.setVisibility(View.VISIBLE); 344 | } else { 345 | mProgressBar.setProgress((int)(progress[0]*MAX_PROGRESS/progress[1])); 346 | } 347 | } 348 | 349 | @Override 350 | protected void onPostExecute(Long result) { 351 | if (result == UPDATE_SUCCESS) { 352 | mProgressBar.setVisibility(View.INVISIBLE); 353 | try { 354 | if (currentJoypadPath.exists()) { 355 | deleteRecursively(currentJoypadPath); 356 | } 357 | if (!futureJoypadPath.renameTo(currentJoypadPath)) { 358 | logError(String.format(res.getString(R.string.error_could_not_rename), futureJoypadPath.getAbsolutePath()), null); 359 | cancel(true); 360 | } 361 | Toast.makeText(YokeActivity.this, res.getString(R.string.toast_layout_succesfully_upgraded), Toast.LENGTH_LONG).show(); 362 | } catch (IOException e) { 363 | logError(String.format(res.getString(R.string.error_io_exception), e.getLocalizedMessage()), e); 364 | cancel(true); 365 | } 366 | } else if (result == PING_SUCCESS) { 367 | mProgressBar.setVisibility(View.INVISIBLE); 368 | mTextView.setText(res.getString(R.string.toolbar_connected_to)); 369 | if (currentMainPath.exists()) { 370 | String url = "file://" + currentMainPath.toString(); 371 | log(String.format(res.getString(R.string.log_loading_url), url)); 372 | wv.loadUrl(url); 373 | } else { 374 | logInfo(String.format( 375 | res.getString(R.string.info_download_layout_first), 376 | res.getString(R.string.menu_upgrade_layout), 377 | res.getString(R.string.toolbar_reconnect) 378 | )); 379 | } 380 | } 381 | } 382 | 383 | @Override 384 | protected void onCancelled() { 385 | mProgressBar.setVisibility(View.INVISIBLE); 386 | mTextView.setText(res.getString(R.string.toolbar_connect_to)); 387 | } 388 | } 389 | 390 | @Override 391 | public void onCreate(Bundle savedInstanceState) { 392 | super.onCreate(savedInstanceState); 393 | sharedPref = getPreferences(Context.MODE_PRIVATE); 394 | setContentView(R.layout.main_wv); 395 | 396 | wv = findViewById(R.id.webView); 397 | wv.getSettings().setJavaScriptEnabled(true); 398 | wv.addJavascriptInterface(new WebAppInterface(this), "Yoke"); 399 | 400 | mNsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE); 401 | 402 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 403 | 404 | // Localization. TODO: detect NOTHING and MANUAL CONNECTION buttons by id, not by content. 405 | res = getResources(); 406 | NOTHING = res.getString(R.string.dropdown_nothing); 407 | ENTER_IP = res.getString(R.string.dropdown_enter_ip); 408 | oldtgt = NOTHING; 409 | 410 | // Paths for layout (can't define them until Android context is established) 411 | currentJoypadPath = new File(getFilesDir(), "joypad"); 412 | currentMainPath = new File(currentJoypadPath, "main.html"); 413 | currentManifestPath = new File(currentJoypadPath, "manifest.json"); 414 | futureJoypadPath = new File(getFilesDir(), "future"); 415 | futureMainPath = new File(futureJoypadPath, "main.html"); 416 | futureManifestPath = new File(futureJoypadPath, "manifest.json"); 417 | 418 | // Progress bar for download: 419 | mProgressBar = (ProgressBar) findViewById(R.id.downloadProgress); 420 | 421 | // Filling spinner with addresses to connect to: 422 | mTextView = (TextView) findViewById(R.id.textView); 423 | 424 | mSpinner = (Spinner) findViewById(R.id.spinner); 425 | mAdapter = new ArrayAdapter<>(getApplicationContext(), android.R.layout.simple_spinner_item); 426 | mAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 427 | mAdapter.add(NOTHING); 428 | mAdapter.add(ENTER_IP); 429 | 430 | for (String adr : sharedPref.getString("addresses", "").split(System.lineSeparator())) { 431 | adr = adr.trim(); // workaround for android bug where random whitespace is added to Strings in shared preferences 432 | if (!adr.isEmpty()) { 433 | mAdapter.add(adr); 434 | mServiceNames.add(adr); 435 | log(String.format(res.getString(R.string.log_adding_address), adr)); 436 | } 437 | } 438 | mSpinner.setAdapter(mAdapter); 439 | mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 440 | @Override 441 | public void onItemSelected(AdapterView parent, View view, int pos, long l) { 442 | String tgt = parent.getItemAtPosition(pos).toString(); 443 | 444 | if (mSpinnerAutomatic) { 445 | mSpinnerAutomatic = false; 446 | log(String.format(res.getString(R.string.log_spinner_automatic), tgt, pos)); 447 | return; 448 | } 449 | 450 | log(String.format(res.getString(R.string.log_spinner_selected), tgt, pos)); 451 | 452 | // clean up old target if no longer available 453 | if (!mServiceNames.contains(oldtgt) && !oldtgt.equals(NOTHING) && !oldtgt.equals(ENTER_IP)) { 454 | mAdapter.remove(oldtgt); 455 | if (oldtgt.equals(tgt)) tgt = NOTHING; 456 | } 457 | 458 | if (tgt.equals(NOTHING)) { 459 | requestDisconnect(); 460 | closeSocket(); 461 | } else if (tgt.equals(ENTER_IP)) { 462 | mSpinnerAutomatic = true; 463 | mSpinner.setSelection(mAdapter.getPosition(oldtgt)); 464 | 465 | AlertDialog.Builder builder = new AlertDialog.Builder(YokeActivity.this); 466 | builder.setTitle(res.getString(R.string.enter_ip_title)); 467 | 468 | final EditText input = new EditText(YokeActivity.this); 469 | input.setInputType(InputType.TYPE_TEXT_VARIATION_URI); 470 | input.setHint(res.getString(R.string.enter_ip_hint)); 471 | 472 | builder.setView(input); 473 | 474 | builder.setPositiveButton(res.getString(R.string.enter_ip_ok), (dialog, which) -> { 475 | String name = input.getText().toString(); 476 | 477 | boolean invalid = name.split(":").length != 2; 478 | 479 | if (!invalid) { 480 | try { 481 | Integer.parseInt(name.split(":")[1]); 482 | } catch (NumberFormatException e) { 483 | invalid = true; 484 | } 485 | } 486 | 487 | if (invalid) { 488 | logInfo(res.getString(R.string.info_invalid_address)); 489 | dialog.cancel(); 490 | } else { 491 | mServiceNames.add(name); 492 | mAdapter.add(name); 493 | 494 | SharedPreferences.Editor editor = sharedPref.edit(); 495 | String addresses = sharedPref.getString("addresses", ""); 496 | addresses = addresses + name + System.lineSeparator(); 497 | editor.putString("addresses", addresses); 498 | editor.apply(); 499 | 500 | oldtgt = name; 501 | mSpinnerAutomatic = true; 502 | mSpinner.setSelection(mAdapter.getPosition(name)); 503 | connectToAddress(name); 504 | } 505 | }); 506 | builder.setNegativeButton(res.getString(R.string.enter_ip_cancel), (dialog, which) -> { 507 | dialog.cancel(); 508 | }); 509 | builder.show(); 510 | } else { 511 | log(String.format(res.getString(R.string.log_service_targeting), tgt)); 512 | 513 | if (mServiceMap.containsKey(tgt)) { 514 | connectToService(tgt); 515 | } else { 516 | connectToAddress(tgt); 517 | } 518 | } 519 | if (!tgt.equals(ENTER_IP)) 520 | oldtgt = mSpinner.getSelectedItem().toString(); 521 | } 522 | 523 | @Override 524 | public void onNothingSelected(AdapterView adapterView) { 525 | log(res.getString(R.string.log_nothing_selected)); 526 | } 527 | }); 528 | } 529 | 530 | @Override 531 | protected void onResume() { 532 | super.onResume(); 533 | 534 | mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, this); 535 | 536 | handler = new Handler(); 537 | handler.post(new Runnable() { 538 | 539 | @Override 540 | public void run() { 541 | update(); 542 | 543 | if (handler != null) 544 | handler.postDelayed(this, poll_rate); 545 | } 546 | }); 547 | 548 | } 549 | 550 | @Override 551 | protected void onPause() { 552 | super.onPause(); 553 | 554 | mNsdManager.stopServiceDiscovery(this); 555 | 556 | closeSocket(); 557 | 558 | handler = null; 559 | } 560 | 561 | public void onWindowFocusChanged(boolean hasFocus) { 562 | super.onWindowFocusChanged(hasFocus); 563 | if (hasFocus) { 564 | getWindow().getDecorView().setSystemUiVisibility( 565 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 566 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 567 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 568 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 569 | | View.SYSTEM_UI_FLAG_FULLSCREEN 570 | | View.SYSTEM_UI_FLAG_IMMERSIVE 571 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); 572 | } 573 | } 574 | 575 | private void update() { 576 | if (mSocket != null && vals_buffer != null) { 577 | send(vals_buffer); 578 | } 579 | } 580 | 581 | public void send(byte[] msg) { 582 | try { 583 | mSocket.send(new DatagramPacket(msg, msg.length)); 584 | } catch (SecurityException e) { 585 | logError(res.getString(R.string.error_sending_security_exception), e); 586 | closeSocket(); 587 | } catch (IOException e) { 588 | if (e.getMessage().contains(" ECONNREFUSED ")) { 589 | logError(res.getString(R.string.error_pc_client_closed), e); 590 | } else if (e.getMessage().contains(" ENETUNREACH ")) { 591 | logError(res.getString(R.string.error_network_unreachable), e); 592 | } else { 593 | logError(e.getMessage(), e); 594 | } 595 | closeSocket(); 596 | } 597 | } 598 | 599 | public void connectToService(String tgt) { 600 | NsdServiceInfo service = mServiceMap.get(tgt); 601 | log(String.format(res.getString(R.string.log_service_resolving), service.getServiceType())); 602 | mNsdManager.resolveService(service, new NsdManager.ResolveListener() { 603 | @Override 604 | public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { 605 | logError(String.format(res.getString(R.string.log_service_resolve_error), errorCode), null); 606 | closeSocket(); 607 | } 608 | 609 | @Override 610 | public void onServiceResolved(NsdServiceInfo serviceInfo) { 611 | // check name again (could have changed in the mean time) 612 | if (tgt.equals(serviceInfo.getServiceName())) { 613 | log(String.format(res.getString(R.string.log_service_resolve_success), serviceInfo)); 614 | 615 | mService = serviceInfo; 616 | openSocket(mService.getHost().getHostName(), mService.getPort()); 617 | 618 | } 619 | } 620 | }); 621 | } 622 | 623 | public void connectToAddress(String tgt) { 624 | log(String.format(res.getString(R.string.log_directly_connecting), tgt)); 625 | String[] addr = tgt.split(":"); 626 | (new Thread(()-> openSocket(addr[0], Integer.parseInt(addr[1])))).start(); 627 | } 628 | 629 | public void reconnect(View view) { 630 | String tgt = mSpinner.getSelectedItem().toString(); 631 | if (currentHost == null) { 632 | logInfo(res.getString(R.string.info_connected_to_nowhere)); 633 | } else { 634 | requestDisconnect(); 635 | closeSocket(); 636 | mSpinnerAutomatic = true; 637 | mSpinner.setSelection(mAdapter.getPosition(tgt)); 638 | connectToAddress(tgt); 639 | mSpinnerAutomatic = false; 640 | } 641 | } 642 | 643 | public void showOverflowMenu(View view) { 644 | PopupMenu popup = new PopupMenu(this, view); 645 | popup.inflate(R.menu.overflow); 646 | popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 647 | public boolean onMenuItemClick(MenuItem item) { 648 | if (item.getItemId() == R.id.upgradeLayout) { 649 | if (currentHost != null) { 650 | new DownloadFilesFromURL().execute( 651 | "http://" + currentHost + ":" + Integer.toString(currentPort) + "/" 652 | ); 653 | } else { 654 | logInfo(res.getString(R.string.info_connected_to_nowhere)); 655 | } 656 | return true; 657 | } else { 658 | return false; 659 | } 660 | } 661 | }); 662 | popup.show(); 663 | } 664 | 665 | 666 | public void openSocket(String host, int port) { 667 | currentHost = host; 668 | currentPort = port; 669 | log(String.format(res.getString(R.string.log_opening_udp), host, port)); 670 | 671 | try { 672 | mSocket = new DatagramSocket(0); 673 | mSocket.connect(InetAddress.getByName(host), port); 674 | 675 | log(res.getString(R.string.log_open_udp_success)); 676 | new DownloadFilesFromURL().execute( 677 | "http://" + currentHost + ":" + Integer.toString(currentPort) + "/", "manifest.json" 678 | ); 679 | 680 | } catch (SocketException | UnknownHostException e) { 681 | mSocket = null; currentHost = null; 682 | YokeActivity.this.runOnUiThread(() -> { 683 | mTextView.setText(res.getString(R.string.toolbar_connect_to)); 684 | mSpinnerAutomatic = true; 685 | mSpinner.setSelection(mAdapter.getPosition(NOTHING)); 686 | }); 687 | logError(String.format(res.getString(R.string.error_open_udp_error), host, port), e); 688 | } 689 | } 690 | 691 | public void onDiscoveryStarted(String serviceType) { 692 | log(String.format(res.getString(R.string.log_discovery_started), serviceType)); 693 | } 694 | 695 | @Override 696 | public void onServiceFound(NsdServiceInfo service) { 697 | log(res.getString(R.string.log_service_found) + service); 698 | mServiceMap.put(service.getServiceName(), service); 699 | mServiceNames.add(service.getServiceName()); 700 | this.runOnUiThread(() -> { 701 | if (mSpinner.getSelectedItem().toString().equals(service.getServiceName())) 702 | return; 703 | mAdapter.add(service.getServiceName()); 704 | }); 705 | 706 | } 707 | 708 | 709 | 710 | @Override 711 | public void onServiceLost(NsdServiceInfo service) { 712 | log(res.getString(R.string.log_service_lost) + service); 713 | mServiceMap.remove(service.getServiceName()); 714 | mServiceNames.remove(service.getServiceName()); 715 | this.runOnUiThread(() -> { 716 | if (mSpinner.getSelectedItem().toString().equals(service.getServiceName())) 717 | return; 718 | 719 | mAdapter.remove(service.getServiceName()); 720 | }); 721 | 722 | } 723 | 724 | @Override 725 | public void onDiscoveryStopped(String serviceType) { 726 | log(String.format(res.getString(R.string.log_discovery_stopped), serviceType)); 727 | } 728 | 729 | @Override 730 | public void onStartDiscoveryFailed(String serviceType, int errorCode) { 731 | logError(String.format(res.getString(R.string.error_discovery_failed), errorCode), null); 732 | mNsdManager.stopServiceDiscovery(this); 733 | } 734 | 735 | @Override 736 | public void onStopDiscoveryFailed(String serviceType, int errorCode) { 737 | logError(String.format(res.getString(R.string.error_discovery_failed), errorCode), null); 738 | mNsdManager.stopServiceDiscovery(this); 739 | } 740 | 741 | // requestDisconnect() sends the disconnect_pattern set by set_disconnect_pattern() 742 | // before closing the connection. This signals the PC client to disconnect the device. 743 | // 744 | // This warning is not to be sent on accidental disconnections (like exiting the app) 745 | // or error states (that usually happen when the client is no longer listening). 746 | private void requestDisconnect() { 747 | if (mSocket != null && disconnect_pattern != null) { 748 | // Send three times, just in case: 749 | send(disconnect_pattern); 750 | send(disconnect_pattern); 751 | send(disconnect_pattern); 752 | log(res.getString(R.string.log_warning_shot)); 753 | } 754 | } 755 | 756 | // closeSocket() closes the connection as soon as possible. It disables the periodic 757 | // status transmissions, then closes the socket, then updates the UI to reflect this. 758 | private void closeSocket() { 759 | log(res.getString(R.string.log_closing_connection)); 760 | // Don't update the last status report: 761 | wv.loadUrl("about:blank"); 762 | vals_buffer = null; 763 | disconnect_pattern = null; 764 | mService = null; 765 | currentHost = null; 766 | mTextView.setText(res.getString(R.string.toolbar_connect_to)); 767 | if (!mSpinner.getSelectedItem().toString().equals(NOTHING)) { 768 | mSpinnerAutomatic = true; 769 | mSpinner.setSelection(mAdapter.getPosition(NOTHING)); 770 | } 771 | // Delayed to the very end, to sidestep occasional errors: 772 | if (mSocket != null) { 773 | mSocket.close(); 774 | mSocket = null; 775 | log(res.getString(R.string.log_udp_closed)); 776 | } 777 | } 778 | } 779 | 780 | --------------------------------------------------------------------------------