├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── bbogush │ │ └── web_screen │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── css │ │ │ └── main.css │ │ ├── html │ │ │ └── index.html │ │ ├── img │ │ │ ├── back.svg │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-192x192.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon-96x96.png │ │ │ ├── home.svg │ │ │ ├── lock.svg │ │ │ ├── power.svg │ │ │ └── recent.svg │ │ ├── js │ │ │ ├── adapter-latest.js │ │ │ ├── main.js │ │ │ └── mouse.js │ │ └── private │ │ │ └── keystore.bks │ ├── ic_launcher-playstore.png │ ├── java │ │ ├── com │ │ │ └── bbogush │ │ │ │ └── web_screen │ │ │ │ ├── AdminActivity.java │ │ │ │ ├── AdminReceiver.java │ │ │ │ ├── AppService.java │ │ │ │ ├── CustomPeerConnectionObserver.java │ │ │ │ ├── CustomSdpObserver.java │ │ │ │ ├── HttpServer.java │ │ │ │ ├── IceServer.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── MouseAccessibilityService.java │ │ │ │ ├── NetworkHelper.java │ │ │ │ ├── PermissionHelper.java │ │ │ │ ├── SettingsActivity.java │ │ │ │ ├── SettingsFragment.java │ │ │ │ ├── SettingsHelper.java │ │ │ │ ├── TurnServer.java │ │ │ │ ├── TurnServerPojo.java │ │ │ │ ├── Utils.java │ │ │ │ └── WebRtcManager.java │ │ └── fi │ │ │ └── iki │ │ │ └── elonen │ │ │ ├── NanoHTTPD.java │ │ │ └── NanoWSD.java │ └── res │ │ ├── drawable-hdpi │ │ └── ic_stat_name.png │ │ ├── drawable-mdpi │ │ └── ic_stat_name.png │ │ ├── drawable-xhdpi │ │ └── ic_stat_name.png │ │ ├── drawable-xxhdpi │ │ └── ic_stat_name.png │ │ ├── drawable-xxxhdpi │ │ └── ic_stat_name.png │ │ ├── drawable │ │ ├── bg_button_off.xml │ │ ├── bg_button_on.xml │ │ ├── bg_item.xml │ │ └── wifi_icon.png │ │ ├── layout │ │ ├── activity_admin.xml │ │ ├── activity_main.xml │ │ └── activity_settings.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-ru-rRU │ │ └── strings.xml │ │ ├── values-uk-rUA │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── menu_main.xml │ │ ├── mouse_accessibility_service_config.xml │ │ ├── policies.xml │ │ └── root_preferences.xml │ └── test │ └── java │ └── com │ └── bbogush │ └── web_screen │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img ├── main_icon.svg └── web_hi_res_512.png ├── scripts ├── chrome_unsec_webrtc.sh ├── gen_https_key.sh └── http_forward.sh └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.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/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 bbogush 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web_screen 2 | The Android Java application for screen sharing and remote control from browser based WebRTC. 3 | 4 | ### Use cases 5 | - Presentation of device screen. 6 | - Screen mirroring/sharing. 7 | - Remote control of devices. 8 | - Remote assistance. 9 | 10 | ### Key Features 11 | - Screen sharing via web browser. 12 | - Remote control of device screen by mouse via web browser. 13 | 14 | ### Google Play 15 | https://play.google.com/store/apps/details?id=com.bbogush.web_screen 16 | 17 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion "29.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.bbogush.web_screen" 9 | minSdkVersion 26 10 | targetSdkVersion 29 11 | versionCode 7 12 | versionName "1.1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation fileTree(dir: "libs", include: ["*.jar"]) 32 | implementation 'androidx.appcompat:appcompat:1.1.0' 33 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 34 | implementation 'com.google.android.material:material:1.1.0' 35 | implementation 'com.google.android.gms:play-services-ads:19.1.0' 36 | testImplementation 'junit:junit:4.12' 37 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 39 | implementation 'androidx.preference:preference:1.1.0' 40 | implementation 'org.webrtc:google-webrtc:1.0.32006' 41 | implementation 'com.squareup.retrofit2:retrofit:2.3.0' 42 | implementation 'com.squareup.retrofit2:converter-gson:2.3.0' 43 | implementation 'com.google.code.gson:gson:2.8.1' 44 | implementation('io.socket:socket.io-client:1.0.0') { 45 | // excluding org.json which is provided by Android 46 | exclude group: 'org.json', module: 'json' 47 | } 48 | implementation 'com.neovisionaries:nv-websocket-client:2.10' 49 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/bbogush/web_screen/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.bbogush.web_screen", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 24 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 47 | 48 | 49 | 50 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app/src/main/assets/css/main.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0px; 5 | border: 0px; 6 | overflow: hidden; 7 | font-family: sans-serif; 8 | display: block; 9 | text-align: center; 10 | } 11 | 12 | body { 13 | background: #112233; 14 | } 15 | 16 | .image_screen { 17 | max-width: 100%; 18 | max-height: 95%; 19 | display: block; 20 | padding: 0px; 21 | border: 0px; 22 | margin-left: auto; 23 | margin-right: auto; 24 | } 25 | 26 | .image_div { 27 | height: 100%; 28 | display: block; 29 | text-align: center; 30 | padding: 0px; 31 | margin: 0px; 32 | border: 0px; 33 | } 34 | 35 | table { 36 | display: inline-block; 37 | table-layout: fixed; 38 | width: 100%; 39 | height: 5%; 40 | overflow: hidden; 41 | white-space: nowrap; 42 | overflow-wrap: break-word; 43 | padding: 0px; 44 | margin: 0px; 45 | border: 0px; 46 | } 47 | 48 | tbody { 49 | display: inline-block; 50 | width: 100%; 51 | height: 100%; 52 | } 53 | 54 | tr { 55 | display: inline-block; 56 | width: 100%; 57 | height: 100%; 58 | background-color: #000000; 59 | } 60 | 61 | td { 62 | display: inline-block; 63 | width: 20%; 64 | height: 100%; 65 | overflow: hidden; 66 | white-space: nowrap; 67 | overflow-wrap: break-word; 68 | } 69 | 70 | button { 71 | display: block; 72 | width: 100%; 73 | height: 100%; 74 | border: 0px; 75 | margin: 0px; 76 | padding: 0px; 77 | color: #DDDDDD; 78 | background-color: #000000; 79 | outline: none; 80 | } 81 | 82 | .button_image { 83 | display: block; 84 | width: 100%; 85 | height: 100%; 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/assets/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 27 | 32 | 37 | 42 | 47 | 48 | 49 |
23 | 26 | 28 | 31 | 33 | 36 | 38 | 41 | 43 | 46 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/assets/img/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/assets/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/assets/img/favicon-16x16.png -------------------------------------------------------------------------------- /app/src/main/assets/img/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/assets/img/favicon-192x192.png -------------------------------------------------------------------------------- /app/src/main/assets/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/assets/img/favicon-32x32.png -------------------------------------------------------------------------------- /app/src/main/assets/img/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/assets/img/favicon-96x96.png -------------------------------------------------------------------------------- /app/src/main/assets/img/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/assets/img/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/assets/img/power.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/assets/img/recent.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/assets/js/main.js: -------------------------------------------------------------------------------- 1 | 2 | let pc; 3 | let remoteStream; 4 | let turnReady; 5 | let dataWebSocket; 6 | let remoteVideo; 7 | 8 | let pcConfig = { 9 | 'iceServers': [{ 10 | 'urls': 'stun:stun.l.google.com:19302' 11 | }] 12 | }; 13 | 14 | window.onload = init; 15 | window.onbeforeunload = uninit; 16 | 17 | function init() { 18 | console.log('Main: init.'); 19 | 20 | varInit(); 21 | webSocketInit(); 22 | turnInit(); 23 | } 24 | 25 | function uninit() { 26 | console.log('Main: uninit'); 27 | 28 | webSocketUninit(); 29 | varUninit(); 30 | } 31 | 32 | function varInit() { 33 | remoteVideo = document.querySelector('#screen'); 34 | } 35 | 36 | function varUninit() { 37 | remoteStream = null; 38 | } 39 | 40 | function webSocketInit() { 41 | console.log('WebSocket: init.'); 42 | 43 | dataWebSocket = new WebSocket('wss://' + window.location.host); 44 | dataWebSocket.onopen = onWsOpen; 45 | dataWebSocket.onclose = onWsClose; 46 | dataWebSocket.onerror = onWsError; 47 | dataWebSocket.onmessage = onWsMessage; 48 | } 49 | 50 | function webSocketUninit() { 51 | console.log('WebSocket: uninit.'); 52 | 53 | sendMessage('{type:bye}'); 54 | dataWebSocket.close(); 55 | dataWebSocket = null; 56 | } 57 | 58 | function onWsOpen(event) { 59 | console.log("WebSocket: opened"); 60 | 61 | sendMessage('{type:join}'); 62 | 63 | mouseInit(dataWebSocket); 64 | } 65 | 66 | function onWsClose(event) { 67 | console.log('WebSocket: closed.'); 68 | 69 | mouseUninit(); 70 | destroyPeerConnection(); 71 | } 72 | 73 | function onWsError(error) { 74 | console.log("WebSocket: error: " + error.message); 75 | } 76 | 77 | function onWsMessage(event) { 78 | console.log('WebSocket: received message:', event); 79 | 80 | let message = JSON.parse(event.data); 81 | if (message.type === 'sdp') 82 | handleSdpMessage(message); 83 | else if (message.type === 'ice') 84 | handleIceMessage(message); 85 | else if (message.type === 'bye') 86 | handleRemoteHangup(); 87 | } 88 | 89 | function handleSdpMessage(message) { 90 | if (message.sdp.type === 'offer') { 91 | createPeerConnection(); 92 | pc.setRemoteDescription(new RTCSessionDescription(message.sdp)); 93 | doAnswer(); 94 | } 95 | } 96 | 97 | function createPeerConnection() { 98 | console.log('WebRTC: create RTCPeerConnnection.'); 99 | 100 | try { 101 | pc = new RTCPeerConnection(null); 102 | pc.onicecandidate = handleIceCandidate; 103 | pc.onaddstream = handleRemoteStreamAdded; 104 | pc.onremovestream = handleRemoteStreamRemoved; 105 | } catch (e) { 106 | console.log('WebRTC: Failed to create PeerConnection, exception: ' + e.message); 107 | return; 108 | } 109 | } 110 | 111 | function destroyPeerConnection() { 112 | console.log('WebRTC: destroy RTCPeerConnnection.'); 113 | 114 | if (pc == null) 115 | return; 116 | pc.close(); 117 | pc = null; 118 | } 119 | 120 | function doAnswer() { 121 | console.log('WebRTC: create answer.'); 122 | 123 | pc.createAnswer().then( 124 | setLocalAndSendMessage, 125 | onCreateSessionDescriptionError 126 | ); 127 | } 128 | 129 | function setLocalAndSendMessage(sessionDescription) { 130 | pc.setLocalDescription(sessionDescription); 131 | sendSdpMessage(sessionDescription); 132 | } 133 | 134 | function sendSdpMessage(message) { 135 | console.log('WebSocket: client sending message: ', message); 136 | 137 | sendMessage('{type=sdp,sdp=' + JSON.stringify(message) + '}'); 138 | } 139 | 140 | function onCreateSessionDescriptionError(error) { 141 | console.log('WebRTC: failed to create session description: ' + error.toString()); 142 | } 143 | 144 | function handleIceMessage(message) { 145 | if (message.ice.type === 'candidate') { 146 | let candidate = new RTCIceCandidate({ 147 | sdpMLineIndex: message.ice.label, 148 | candidate: message.ice.candidate 149 | }); 150 | pc.addIceCandidate(candidate).then(onAddIceCandidateSuccess, onAddIceCandidateError); 151 | } 152 | } 153 | 154 | function onAddIceCandidateSuccess() { 155 | console.log('WebRTC: Ice candidate successfully added.'); 156 | } 157 | 158 | function onAddIceCandidateError(error) { 159 | console.log('WebRTC: failed to add ice candidate: ' + error.toString()); 160 | } 161 | 162 | function sendIceMessage(message) { 163 | console.log('WebSocket: client sending message: ', message); 164 | 165 | sendMessage('{type=ice,ice=' + JSON.stringify(message) + '}') 166 | } 167 | 168 | function handleIceCandidate(event) { 169 | console.log('WebRTC: icecandidate event: ', event); 170 | 171 | if (event.candidate) { 172 | sendIceMessage({ 173 | type: 'candidate', 174 | label: event.candidate.sdpMLineIndex, 175 | id: event.candidate.sdpMid, 176 | candidate: event.candidate.candidate 177 | }); 178 | } else { 179 | console.log('WebRTC: end of candidates.'); 180 | } 181 | } 182 | 183 | function turnInit() { 184 | //TODO 185 | // if (location.hostname !== 'localhost') { 186 | // requestTurn( 187 | // 'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913' 188 | // ); 189 | // } 190 | } 191 | 192 | function requestTurn(turnURL) { 193 | let turnExists = false; 194 | for (let i in pcConfig.iceServers) { 195 | if (pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') { 196 | turnExists = true; 197 | turnReady = true; 198 | break; 199 | } 200 | } 201 | if (!turnExists) { 202 | console.log('WebRTC: getting TURN server from ', turnURL); 203 | // No TURN server. Get one from computeengineondemand.appspot.com: 204 | let xhr = new XMLHttpRequest(); 205 | xhr.onreadystatechange = function() { 206 | if (xhr.readyState === 4 && xhr.status === 200) { 207 | let turnServer = JSON.parse(xhr.responseText); 208 | console.log('Got TURN server: ', turnServer); 209 | pcConfig.iceServers.push({ 210 | 'urls': 'turn:' + turnServer.username + '@' + turnServer.turn, 211 | 'credential': turnServer.password 212 | }); 213 | turnReady = true; 214 | } 215 | }; 216 | xhr.open('GET', turnURL, true); 217 | xhr.send(); 218 | } 219 | } 220 | 221 | function handleRemoteStreamAdded(event) { 222 | console.log('WebRTC: remote stream added.'); 223 | remoteStream = event.stream; 224 | remoteVideo.srcObject = remoteStream; 225 | } 226 | 227 | function handleRemoteStreamRemoved(event) { 228 | console.log('WebRTC: Remote stream removed. Event: ', event); 229 | } 230 | 231 | function handleRemoteHangup() { 232 | console.log('WebSocket: session terminated by remote party.'); 233 | destroyPeerConnection(); 234 | } 235 | 236 | function sendMessage(message) { 237 | if (dataWebSocket == null) 238 | return; 239 | 240 | console.log('WebSocket: client sending message: ', message); 241 | dataWebSocket.send(message); 242 | } 243 | -------------------------------------------------------------------------------- /app/src/main/assets/js/mouse.js: -------------------------------------------------------------------------------- 1 | 2 | let mouseDown = false; 3 | let mouseWebSocket = null; 4 | let remoteVideoRect; 5 | 6 | function mouseInit(ws) { 7 | mouseWebSocket = ws; 8 | 9 | remoteVideoRect = document.getElementById('screen'); 10 | remoteVideoRect.addEventListener('mousedown', mouseDownHandler); 11 | remoteVideoRect.addEventListener('mousemove', mouseMoveHandler); 12 | remoteVideoRect.addEventListener('mouseup', mouseUpHandler); 13 | remoteVideoRect.addEventListener('wheel', mouseWheelHandler); 14 | remoteVideoRect.addEventListener('ondragstart', onDragStart); 15 | } 16 | 17 | function mouseUninit() { 18 | remoteVideoRect.removeEventListener('ondragstart', onDragStart); 19 | remoteVideoRect.removeEventListener('wheel', mouseWheelHandler); 20 | remoteVideoRect.removeEventListener('mouseup', mouseUpHandler); 21 | remoteVideoRect.removeEventListener('mousemove', mouseMoveHandler); 22 | remoteVideoRect.removeEventListener('mousedown', mouseDownHandler); 23 | remoteVideoRect = null; 24 | } 25 | 26 | function onDragStart() { 27 | return false; 28 | } 29 | 30 | function mouseDownHandler(e) { 31 | if (!isMouseLeftButtonPressed(e)) 32 | return; 33 | mouseDown = true; 34 | mouseHandler(e, 'down'); 35 | } 36 | 37 | function mouseMoveHandler(e) { 38 | if (!mouseDown) 39 | return; 40 | if (!isMouseLeftButtonPressed(e)) { 41 | mouseDown = false; 42 | mouseHandler(e, 'up'); 43 | return; 44 | } 45 | mouseHandler(e, 'move'); 46 | } 47 | 48 | function mouseUpHandler(e) { 49 | if (!mouseDown) 50 | return; 51 | if (isMouseLeftButtonPressed(e)) 52 | return; 53 | mouseDown = false; 54 | mouseHandler(e, 'up'); 55 | } 56 | 57 | function mouseWheelHandler(e) { 58 | if (!e.ctrlKey) 59 | return; 60 | if (e.deltaY > 0) 61 | mouseHandler(e, 'zoom_out'); 62 | else if (e.deltaY < 0) 63 | mouseHandler(e, 'zoom_in'); 64 | e.preventDefault(); 65 | } 66 | 67 | function isMouseLeftButtonPressed(e) { 68 | let MOUSE_LEFT_BUTTON_NUMBER = 1; 69 | 70 | return e.buttons === undefined ? e.which === MOUSE_LEFT_BUTTON_NUMBER : 71 | e.buttons === MOUSE_LEFT_BUTTON_NUMBER; 72 | } 73 | 74 | function mouseHandler(e, action) { 75 | let position = getPosition(e); 76 | let params = '{type=mouse_' + action + ',x=' + position.x + ',y=' + position.y + '}'; 77 | sendMouseMessage(params); 78 | } 79 | 80 | function getPosition(e) { 81 | let rect = e.target.getBoundingClientRect(); 82 | let x = e.clientX - rect.left; 83 | let y = e.clientY - rect.top; 84 | 85 | x = Math.round(x * e.target.videoWidth * 1.0 / e.target.clientWidth); 86 | y = Math.round(y * e.target.videoHeight * 1.0 / e.target.clientHeight); 87 | 88 | return {x, y}; 89 | } 90 | 91 | function backButtonHandler() { 92 | buttonHandler('back'); 93 | } 94 | 95 | function homeButtonHandler() { 96 | buttonHandler('home'); 97 | } 98 | 99 | function recentButtonHandler() { 100 | buttonHandler('recent'); 101 | } 102 | 103 | function powerButtonHandler() { 104 | buttonHandler('power'); 105 | } 106 | 107 | function lockButtonHandler() { 108 | buttonHandler('lock'); 109 | } 110 | 111 | function buttonHandler(button) { 112 | sendMouseMessage('{type=button_' + button + '}'); 113 | } 114 | 115 | function sendMouseMessage(message) { 116 | if (mouseWebSocket == null) 117 | return; 118 | 119 | mouseWebSocket.send(message); 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /app/src/main/assets/private/keystore.bks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/assets/private/keystore.bks -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/AdminActivity.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.app.Activity; 6 | import android.app.admin.DevicePolicyManager; 7 | import android.content.ComponentName; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.os.Bundle; 11 | import android.util.Log; 12 | 13 | public class AdminActivity extends AppCompatActivity { 14 | private static final String TAG = AdminActivity.class.getSimpleName(); 15 | static final int RESULT_ENABLE = 1; 16 | 17 | DevicePolicyManager deviceManger; 18 | ComponentName compName; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.activity_admin); 24 | 25 | deviceManger = (DevicePolicyManager)getSystemService(Context.DEVICE_POLICY_SERVICE); 26 | compName = new ComponentName(this, AdminReceiver.class); 27 | if (deviceManger.isAdminActive(compName)) { 28 | deviceManger.lockNow(); 29 | finish(); 30 | } else { 31 | askAdminPermission(); 32 | } 33 | } 34 | 35 | private void askAdminPermission() { 36 | Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN); 37 | intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, compName); 38 | intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, 39 | getResources().getString(R.string.ask_admin_permission)); 40 | startActivityForResult(intent, RESULT_ENABLE); 41 | } 42 | 43 | @Override 44 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 45 | switch (requestCode) { 46 | case RESULT_ENABLE: 47 | if (resultCode == Activity.RESULT_OK) { 48 | Log.d(TAG, "Admin permission granted"); 49 | deviceManger.lockNow(); 50 | finish(); 51 | } else { 52 | Log.i(TAG, "Admin permission denied"); 53 | finish(); 54 | } 55 | return; 56 | } 57 | super.onActivityResult(requestCode, resultCode, data); 58 | } 59 | 60 | public static void lockScreen(Context context) { 61 | DevicePolicyManager deviceManger = 62 | (DevicePolicyManager)context.getSystemService(Context.DEVICE_POLICY_SERVICE); 63 | ComponentName compName = new ComponentName(context, AdminReceiver.class); 64 | // Start activity only if need permission 65 | if (deviceManger.isAdminActive(compName)) { 66 | deviceManger.lockNow(); 67 | } else { 68 | Intent intent = new Intent(context, AdminActivity.class); 69 | context.startActivity(intent); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/AdminReceiver.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.app.admin.DeviceAdminReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | public class AdminReceiver extends DeviceAdminReceiver { 8 | @Override 9 | public void onDisabled(Context context, Intent intent) { 10 | super.onDisabled(context, intent); 11 | } 12 | @Override 13 | public void onEnabled(Context context, Intent intent) { 14 | super.onEnabled(context, intent); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/AppService.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationChannel; 5 | import android.app.NotificationManager; 6 | import android.app.PendingIntent; 7 | import android.app.Service; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.os.Binder; 11 | import android.os.Build; 12 | import android.os.IBinder; 13 | import android.util.Log; 14 | import android.widget.Toast; 15 | 16 | import androidx.annotation.Nullable; 17 | import androidx.annotation.RequiresApi; 18 | import androidx.core.app.NotificationCompat; 19 | 20 | import org.json.JSONException; 21 | import org.json.JSONObject; 22 | 23 | import java.io.IOException; 24 | import java.util.Locale; 25 | 26 | public class AppService extends Service { 27 | private static final String TAG = AppService.class.getSimpleName(); 28 | 29 | private static final int SERVICE_ID = 101; 30 | 31 | private static final String NOTIFICATION_CHANNEL_ID = "WebScreenServiceChannel"; 32 | private static final String NOTIFICATION_CHANNEL_NAME = "WebScreen notification channel"; 33 | 34 | private static final String NOTIFICATION_TITLE = "WebScreen is running"; 35 | private static final String NOTIFICATION_CONTENT = "Tap to stop"; 36 | 37 | private static final String MOUSE_PARAM_X = "x"; 38 | private static final String MOUSE_PARAM_Y = "y"; 39 | 40 | private static boolean isRunning = false; 41 | 42 | private final IBinder iBinder = new AppServiceBinder(); 43 | 44 | private WebRtcManager webRtcManager = null; 45 | 46 | private HttpServer httpServer = null; 47 | private boolean isWebServerRunning = false; 48 | 49 | private MouseAccessibilityService mouseAccessibilityService = null; 50 | 51 | @Override 52 | public void onCreate() { 53 | isRunning = true; 54 | Log.d(TAG, "Service created"); 55 | } 56 | 57 | @Override 58 | public void onDestroy() { 59 | serverStop(); 60 | isRunning = false; 61 | Log.d(TAG, "Service destroyed"); 62 | } 63 | 64 | public static boolean isServiceRunning() { 65 | return isRunning; 66 | } 67 | 68 | @Override 69 | public int onStartCommand(Intent intent, int flags, int startId) { 70 | Intent notificationIntent = new Intent(this, MainActivity.class); 71 | notificationIntent.setAction(Intent.ACTION_MAIN); 72 | notificationIntent.addCategory(Intent.CATEGORY_LAUNCHER); 73 | 74 | PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); 75 | 76 | String channelId = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? 77 | createNotificationChannel() : ""; 78 | NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, 79 | channelId); 80 | Notification notification = notificationBuilder.setOngoing(true) 81 | .setContentTitle(NOTIFICATION_TITLE) 82 | .setContentText(NOTIFICATION_CONTENT) 83 | .setSmallIcon(R.drawable.ic_stat_name) 84 | .setCategory(NotificationCompat.CATEGORY_SERVICE) 85 | .setContentIntent(pendingIntent) 86 | .build(); 87 | 88 | startForeground(SERVICE_ID, notification); 89 | 90 | Log.d(TAG, "Service started"); 91 | return START_STICKY; 92 | } 93 | 94 | @RequiresApi(Build.VERSION_CODES.O) 95 | private String createNotificationChannel(){ 96 | NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, 97 | NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT); 98 | 99 | NotificationManager notificationManager = (NotificationManager) 100 | getSystemService(Context.NOTIFICATION_SERVICE); 101 | notificationManager.createNotificationChannel(channel); 102 | return NOTIFICATION_CHANNEL_ID; 103 | } 104 | 105 | public class AppServiceBinder extends Binder { 106 | AppService getService() { 107 | return AppService.this; 108 | } 109 | } 110 | 111 | @Nullable 112 | @Override 113 | public IBinder onBind(Intent intent) { 114 | return iBinder; 115 | } 116 | 117 | public boolean serverStart(Intent intent, int port, 118 | boolean isAccessibilityServiceEnabled, Context context) { 119 | if (!(isWebServerRunning = startHttpServer(port))) 120 | return false; 121 | 122 | webRtcManager = new WebRtcManager(intent, context, httpServer); 123 | 124 | accessibilityServiceSet(context, isAccessibilityServiceEnabled); 125 | 126 | return isWebServerRunning; 127 | } 128 | 129 | public void serverStop() { 130 | if (!isWebServerRunning) 131 | return; 132 | isWebServerRunning = false; 133 | 134 | accessibilityServiceSet(null, false); 135 | 136 | stopHttpServer(); 137 | webRtcManager.close(); 138 | webRtcManager = null; 139 | } 140 | 141 | public boolean isServerRunning() { 142 | return isWebServerRunning; 143 | } 144 | 145 | public boolean serverRestart(int port) { 146 | stopHttpServer(); 147 | isWebServerRunning = startHttpServer(port); 148 | 149 | return isWebServerRunning; 150 | } 151 | 152 | public boolean startHttpServer(int httpServerPort) { 153 | httpServer = new HttpServer(httpServerPort, getApplicationContext(), httpServerInterface); 154 | try { 155 | httpServer.start(); 156 | } catch (IOException e) { 157 | String fmt = getResources().getString(R.string.port_in_use); 158 | String errorMessage = String.format(Locale.getDefault(), fmt, httpServerPort); 159 | Toast.makeText(getApplicationContext(),errorMessage, Toast.LENGTH_SHORT).show(); 160 | return false; 161 | } 162 | 163 | return true; 164 | } 165 | 166 | public void stopHttpServer() { 167 | if (httpServer == null) 168 | return; 169 | 170 | Thread thread = new Thread(new Runnable() { 171 | @Override 172 | public void run() { 173 | try { 174 | // Run stop in thread to avoid NetworkOnMainThreadException 175 | httpServer.stop(); 176 | } catch (Exception e) { 177 | e.printStackTrace(); 178 | } 179 | } 180 | }); 181 | thread.start(); 182 | try { 183 | thread.join(); 184 | } catch (InterruptedException e) { 185 | e.printStackTrace(); 186 | } 187 | 188 | httpServer = null; 189 | } 190 | 191 | private HttpServer.HttpServerInterface httpServerInterface = new 192 | HttpServer.HttpServerInterface() { 193 | @Override 194 | public void onMouseDown(JSONObject message) { 195 | int[] coordinates = getCoordinates(message); 196 | if (coordinates != null && mouseAccessibilityService != null) 197 | mouseAccessibilityService.mouseDown(coordinates[0], coordinates[1]); 198 | } 199 | 200 | @Override 201 | public void onMouseMove(JSONObject message) { 202 | int[] coordinates = getCoordinates(message); 203 | if (coordinates != null && mouseAccessibilityService != null) 204 | mouseAccessibilityService.mouseMove(coordinates[0], coordinates[1]); 205 | } 206 | 207 | @Override 208 | public void onMouseUp(JSONObject message) { 209 | int[] coordinates = getCoordinates(message); 210 | if (coordinates != null && mouseAccessibilityService != null) 211 | mouseAccessibilityService.mouseUp(coordinates[0], coordinates[1]); 212 | } 213 | 214 | @Override 215 | public void onMouseZoomIn(JSONObject message) { 216 | int[] coordinates = getCoordinates(message); 217 | if (coordinates != null && mouseAccessibilityService != null) 218 | mouseAccessibilityService.mouseWheelZoomIn(coordinates[0], coordinates[1]); 219 | } 220 | 221 | @Override 222 | public void onMouseZoomOut(JSONObject message) { 223 | int[] coordinates = getCoordinates(message); 224 | if (coordinates != null && mouseAccessibilityService != null) 225 | mouseAccessibilityService.mouseWheelZoomOut(coordinates[0], coordinates[1]); 226 | } 227 | 228 | @Override 229 | public void onButtonBack() { 230 | if (mouseAccessibilityService != null) 231 | mouseAccessibilityService.backButtonClick(); 232 | } 233 | 234 | @Override 235 | public void onButtonHome() { 236 | if (mouseAccessibilityService != null) 237 | mouseAccessibilityService.homeButtonClick(); 238 | } 239 | 240 | @Override 241 | public void onButtonRecent() { 242 | if (mouseAccessibilityService != null) 243 | mouseAccessibilityService.recentButtonClick(); 244 | } 245 | 246 | @Override 247 | public void onButtonPower() { 248 | if (mouseAccessibilityService != null) 249 | mouseAccessibilityService.powerButtonClick(); 250 | } 251 | 252 | @Override 253 | public void onButtonLock() { 254 | if (mouseAccessibilityService != null) 255 | mouseAccessibilityService.lockButtonClick(); 256 | } 257 | 258 | @Override 259 | public void onJoin(HttpServer server) { 260 | if (webRtcManager == null) 261 | return; 262 | webRtcManager.start(server); 263 | } 264 | 265 | @Override 266 | public void onSdp(JSONObject message) { 267 | if (webRtcManager == null) 268 | return; 269 | webRtcManager.onAnswerReceived(message); 270 | } 271 | 272 | @Override 273 | public void onIceCandidate(JSONObject message) { 274 | if (webRtcManager == null) 275 | return; 276 | webRtcManager.onIceCandidateReceived(message); 277 | } 278 | 279 | @Override 280 | public void onBye() { 281 | if (webRtcManager == null) 282 | return; 283 | webRtcManager.stop(); 284 | } 285 | 286 | @Override 287 | public void onWebSocketClose() { 288 | if (webRtcManager == null) 289 | return; 290 | webRtcManager.stop(); 291 | } 292 | }; 293 | 294 | private int[] getCoordinates(JSONObject json) { 295 | int[] coordinates = new int[2]; 296 | 297 | try { 298 | coordinates[0] = json.getInt(MOUSE_PARAM_X); 299 | coordinates[1] = json.getInt(MOUSE_PARAM_Y); 300 | } catch (JSONException e) { 301 | e.printStackTrace(); 302 | return null; 303 | } 304 | 305 | return coordinates; 306 | } 307 | 308 | public void accessibilityServiceSet(Context context, boolean isEnabled) { 309 | if (isEnabled) { 310 | if (mouseAccessibilityService != null) 311 | return; 312 | mouseAccessibilityService = new MouseAccessibilityService(); 313 | mouseAccessibilityService.setContext(context); 314 | } else { 315 | mouseAccessibilityService = null; 316 | } 317 | } 318 | 319 | public boolean isMouseAccessibilityServiceAvailable() { 320 | return mouseAccessibilityService != null; 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/CustomPeerConnectionObserver.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.util.Log; 4 | 5 | import org.webrtc.DataChannel; 6 | import org.webrtc.IceCandidate; 7 | import org.webrtc.MediaStream; 8 | import org.webrtc.PeerConnection; 9 | import org.webrtc.RtpReceiver; 10 | 11 | class CustomPeerConnectionObserver implements PeerConnection.Observer { 12 | private String logTag; 13 | 14 | public CustomPeerConnectionObserver(String logTag) { 15 | this.logTag = this.getClass().getCanonicalName(); 16 | this.logTag = this.logTag + " "+logTag; 17 | } 18 | 19 | @Override 20 | public void onSignalingChange(PeerConnection.SignalingState signalingState) { 21 | Log.d(logTag, "onSignalingChange() called with: signalingState = [" + signalingState + "]"); 22 | } 23 | 24 | @Override 25 | public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { 26 | Log.d(logTag, "onIceConnectionChange() called with: iceConnectionState = [" + 27 | iceConnectionState + "]"); 28 | } 29 | 30 | @Override 31 | public void onIceConnectionReceivingChange(boolean b) { 32 | Log.d(logTag, "onIceConnectionReceivingChange() called with: b = [" + b + "]"); 33 | } 34 | 35 | @Override 36 | public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { 37 | Log.d(logTag, "onIceGatheringChange() called with: iceGatheringState = [" + 38 | iceGatheringState + "]"); 39 | } 40 | 41 | @Override 42 | public void onIceCandidate(IceCandidate iceCandidate) { 43 | Log.d(logTag, "onIceCandidate() called with: iceCandidate = [" + iceCandidate + "]"); 44 | } 45 | 46 | @Override 47 | public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { 48 | Log.d(logTag, "onIceCandidatesRemoved() called with: iceCandidates = [" + iceCandidates + 49 | "]"); 50 | } 51 | 52 | @Override 53 | public void onAddStream(MediaStream mediaStream) { 54 | Log.d(logTag, "onAddStream() called with: mediaStream = [" + mediaStream + "]"); 55 | } 56 | 57 | @Override 58 | public void onRemoveStream(MediaStream mediaStream) { 59 | Log.d(logTag, "onRemoveStream() called with: mediaStream = [" + mediaStream + "]"); 60 | } 61 | 62 | @Override 63 | public void onDataChannel(DataChannel dataChannel) { 64 | Log.d(logTag, "onDataChannel() called with: dataChannel = [" + dataChannel + "]"); 65 | } 66 | 67 | @Override 68 | public void onRenegotiationNeeded() { 69 | Log.d(logTag, "onRenegotiationNeeded() called"); 70 | } 71 | 72 | @Override 73 | public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { 74 | Log.d(logTag, "onAddTrack() called with: rtpReceiver = [" + rtpReceiver + 75 | "], mediaStreams = [" + mediaStreams + "]"); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/CustomSdpObserver.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.util.Log; 4 | 5 | import org.webrtc.SdpObserver; 6 | import org.webrtc.SessionDescription; 7 | 8 | class CustomSdpObserver implements SdpObserver { 9 | private String tag; 10 | 11 | CustomSdpObserver(String logTag) { 12 | tag = this.getClass().getCanonicalName(); 13 | this.tag = this.tag + " " + logTag; 14 | } 15 | 16 | @Override 17 | public void onCreateSuccess(SessionDescription sessionDescription) { 18 | Log.d(tag, "onCreateSuccess() called with: sessionDescription = [" + sessionDescription + 19 | "]"); 20 | } 21 | 22 | @Override 23 | public void onSetSuccess() { 24 | Log.d(tag, "onSetSuccess() called"); 25 | } 26 | 27 | @Override 28 | public void onCreateFailure(String s) { 29 | Log.d(tag, "onCreateFailure() called with: s = [" + s + "]"); 30 | } 31 | 32 | @Override 33 | public void onSetFailure(String s) { 34 | Log.d(tag, "onSetFailure() called with: s = [" + s + "]"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/HttpServer.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.util.Log; 7 | import android.widget.Toast; 8 | 9 | import org.json.JSONException; 10 | import org.json.JSONObject; 11 | 12 | import java.io.BufferedReader; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.InputStreamReader; 16 | import java.security.KeyStore; 17 | import java.security.KeyStoreException; 18 | import java.util.Timer; 19 | import java.util.TimerTask; 20 | 21 | import javax.net.ssl.KeyManagerFactory; 22 | 23 | import fi.iki.elonen.NanoWSD; 24 | 25 | public class HttpServer extends NanoWSD { 26 | private static final String TAG = HttpServer.class.getSimpleName(); 27 | 28 | private static final String HTML_DIR = "html/"; 29 | private static final String INDEX_HTML = "index.html"; 30 | private static final String MIME_IMAGE_SVG = "image/svg+xml"; 31 | private static final String MIME_JS = "text/javascript"; 32 | private static final String MIME_TEXT_PLAIN_JS = "text/plain"; 33 | private static final String MIME_TEXT_CSS = "text/css"; 34 | private static final String TYPE_PARAM = "type"; 35 | private static final String TYPE_VALUE_MOUSE_UP = "mouse_up"; 36 | private static final String TYPE_VALUE_MOUSE_MOVE = "mouse_move"; 37 | private static final String TYPE_VALUE_MOUSE_DOWN = "mouse_down"; 38 | private static final String TYPE_VALUE_MOUSE_ZOOM_IN = "mouse_zoom_in"; 39 | private static final String TYPE_VALUE_MOUSE_ZOOM_OUT = "mouse_zoom_out"; 40 | private static final String TYPE_VALUE_BUTTON_BACK = "button_back"; 41 | private static final String TYPE_VALUE_BUTTON_HOME = "button_home"; 42 | private static final String TYPE_VALUE_BUTTON_RECENT = "button_recent"; 43 | private static final String TYPE_VALUE_BUTTON_POWER = "button_power"; 44 | private static final String TYPE_VALUE_BUTTON_LOCK = "button_lock"; 45 | private static final String TYPE_VALUE_JOIN = "join"; 46 | private static final String TYPE_VALUE_SDP = "sdp"; 47 | private static final String TYPE_VALUE_ICE = "ice"; 48 | private static final String TYPE_VALUE_BYE = "bye"; 49 | 50 | private Context context; 51 | Ws webSocket = null; 52 | 53 | private HttpServer.HttpServerInterface httpServerInterface; 54 | 55 | public HttpServer(int port, Context context, 56 | HttpServer.HttpServerInterface httpServerInterface) { 57 | super(port); 58 | this.context = context; 59 | this.httpServerInterface = httpServerInterface; 60 | configSecurity(); 61 | } 62 | 63 | private void configSecurity() { 64 | final String keyPassword = "presscott"; 65 | final String certPassword = "presscott"; 66 | 67 | try { 68 | InputStream keyStoreStream = context.getAssets().open("private/keystore.bks"); 69 | KeyStore keyStore = KeyStore.getInstance("BKS"); 70 | keyStore.load(keyStoreStream, keyPassword.toCharArray()); 71 | KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory 72 | .getDefaultAlgorithm()); 73 | keyManagerFactory.init(keyStore, certPassword.toCharArray()); 74 | makeSecure(makeSSLSocketFactory(keyStore, keyManagerFactory), null); 75 | } catch (Exception e) { 76 | e.printStackTrace(); 77 | return; 78 | } 79 | } 80 | 81 | class Ws extends WebSocket { 82 | private static final int PING_INTERVAL = 20000; 83 | private Timer pingTimer = new Timer(); 84 | 85 | public Ws(IHTTPSession handshakeRequest) { 86 | super(handshakeRequest); 87 | } 88 | 89 | @Override 90 | protected void onOpen() { 91 | Log.d(TAG, "WebSocket open"); 92 | TimerTask timerTask = new TimerTask() { 93 | @Override 94 | public void run() { 95 | try { 96 | Ws.this.ping(new byte[0]); 97 | } catch (IOException e) { 98 | e.printStackTrace(); 99 | } 100 | } 101 | }; 102 | 103 | pingTimer.scheduleAtFixedRate(timerTask, PING_INTERVAL, PING_INTERVAL); 104 | } 105 | 106 | @Override 107 | protected void onClose(WebSocketFrame.CloseCode code, String reason, 108 | boolean initiatedByRemote) { 109 | Log.d(TAG, "WebSocket close"); 110 | pingTimer.cancel(); 111 | httpServerInterface.onWebSocketClose(); 112 | } 113 | 114 | @Override 115 | protected void onMessage(WebSocketFrame message) { 116 | JSONObject json; 117 | 118 | try { 119 | json = new JSONObject(message.getTextPayload()); 120 | } catch (JSONException e) { 121 | e.printStackTrace(); 122 | return; 123 | } 124 | 125 | handleRequest(json); 126 | } 127 | 128 | @Override 129 | protected void onPong(WebSocketFrame pong) { 130 | } 131 | 132 | @Override 133 | protected void onException(IOException exception) { 134 | Log.d(TAG, "WebSocket exception"); 135 | } 136 | } 137 | 138 | @Override 139 | protected WebSocket openWebSocket(IHTTPSession handshake) { 140 | webSocket = new Ws(handshake); 141 | return webSocket; 142 | } 143 | 144 | @Override 145 | protected Response serveHttp(IHTTPSession session) { 146 | Method method = session.getMethod(); 147 | String uri = session.getUri(); 148 | 149 | return serveRequest(session, uri, method); 150 | } 151 | 152 | public interface HttpServerInterface { 153 | void onMouseDown(JSONObject message); 154 | void onMouseMove(JSONObject message); 155 | void onMouseUp(JSONObject message); 156 | void onMouseZoomIn(JSONObject message); 157 | void onMouseZoomOut(JSONObject message); 158 | void onButtonBack(); 159 | void onButtonHome(); 160 | void onButtonRecent(); 161 | void onButtonPower(); 162 | void onButtonLock(); 163 | void onJoin(HttpServer server); 164 | void onSdp(JSONObject message); 165 | void onIceCandidate(JSONObject message); 166 | void onBye(); 167 | void onWebSocketClose(); 168 | } 169 | 170 | public void send(String message) throws IOException { 171 | if (webSocket != null) 172 | webSocket.send(message); 173 | } 174 | 175 | private Response serveRequest(IHTTPSession session, String uri, Method method) { 176 | if(Method.GET.equals(method)) 177 | return handleGet(session, uri); 178 | 179 | return notFoundResponse(); 180 | } 181 | 182 | private Response notFoundResponse() { 183 | return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found"); 184 | } 185 | 186 | private Response internalErrorResponse() { 187 | return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, 188 | "Internal error"); 189 | } 190 | 191 | private Response handleGet(IHTTPSession session, String uri) { 192 | if (uri.contentEquals("/")) { 193 | return handleRootRequest(session); 194 | } else if (uri.contains("private")) { 195 | return notFoundResponse(); 196 | } 197 | 198 | return handleFileRequest(session, uri); 199 | } 200 | 201 | private Response handleRootRequest(IHTTPSession session) { 202 | String indexHtml = readFile(HTML_DIR + INDEX_HTML); 203 | 204 | return newFixedLengthResponse(Response.Status.OK, MIME_HTML, indexHtml); 205 | } 206 | 207 | private void handleRequest(JSONObject json) { 208 | String type; 209 | try { 210 | type = json.getString(TYPE_PARAM); 211 | } catch (JSONException e) { 212 | e.printStackTrace(); 213 | return; 214 | } 215 | 216 | switch (type) { 217 | case TYPE_VALUE_MOUSE_DOWN: 218 | httpServerInterface.onMouseDown(json); 219 | break; 220 | case TYPE_VALUE_MOUSE_MOVE: 221 | httpServerInterface.onMouseMove(json); 222 | break; 223 | case TYPE_VALUE_MOUSE_UP: 224 | httpServerInterface.onMouseUp(json); 225 | break; 226 | case TYPE_VALUE_MOUSE_ZOOM_IN: 227 | httpServerInterface.onMouseZoomIn(json); 228 | break; 229 | case TYPE_VALUE_MOUSE_ZOOM_OUT: 230 | httpServerInterface.onMouseZoomOut(json); 231 | break; 232 | case TYPE_VALUE_BUTTON_BACK: 233 | httpServerInterface.onButtonBack(); 234 | break; 235 | case TYPE_VALUE_BUTTON_HOME: 236 | httpServerInterface.onButtonHome(); 237 | break; 238 | case TYPE_VALUE_BUTTON_RECENT: 239 | httpServerInterface.onButtonRecent(); 240 | break; 241 | case TYPE_VALUE_BUTTON_POWER: 242 | httpServerInterface.onButtonPower(); 243 | break; 244 | case TYPE_VALUE_BUTTON_LOCK: 245 | httpServerInterface.onButtonLock(); 246 | break; 247 | case TYPE_VALUE_JOIN: 248 | httpServerInterface.onJoin(this); 249 | break; 250 | case TYPE_VALUE_SDP: 251 | httpServerInterface.onSdp(json); 252 | break; 253 | case TYPE_VALUE_ICE: 254 | httpServerInterface.onIceCandidate(json); 255 | break; 256 | case TYPE_VALUE_BYE: 257 | httpServerInterface.onBye(); 258 | break; 259 | } 260 | } 261 | 262 | private String readFile(String fileName) { 263 | InputStream fileStream; 264 | String string = ""; 265 | 266 | try { 267 | fileStream = context.getAssets().open(fileName); 268 | BufferedReader reader = new BufferedReader(new InputStreamReader(fileStream, 269 | "UTF-8")); 270 | 271 | String line; 272 | while ((line = reader.readLine()) != null) 273 | string += line; 274 | } catch (IOException e) { 275 | e.printStackTrace(); 276 | } 277 | 278 | return string; 279 | } 280 | 281 | private Response handleFileRequest(IHTTPSession session, String uri) { 282 | String relativePath = uri.startsWith("/") ? uri.substring(1) : uri; 283 | 284 | InputStream fileStream; 285 | try { 286 | fileStream = context.getAssets().open(relativePath); 287 | } catch (IOException e) { 288 | e.printStackTrace(); 289 | return notFoundResponse(); 290 | } 291 | 292 | String mime; 293 | if (uri.contains(".js")) 294 | mime = MIME_JS; 295 | else if (uri.contains(".svg")) 296 | mime = MIME_IMAGE_SVG; 297 | else if (uri.contains(".css")) 298 | mime = MIME_TEXT_CSS; 299 | else 300 | mime = MIME_TEXT_PLAIN_JS; 301 | 302 | return newChunkedResponse(Response.Status.OK, mime, fileStream); 303 | } 304 | 305 | private void notifyAboutNewConnection(IHTTPSession session) { 306 | // The message is used to trigger screen redraw on new connection 307 | final String remoteAddress = session.getRemoteIpAddress(); 308 | new Handler(Looper.getMainLooper()).post(new Runnable() { 309 | @Override 310 | public void run() { 311 | Toast.makeText(context, "WebScreen\nNew connection from " + remoteAddress, 312 | Toast.LENGTH_SHORT).show(); 313 | } 314 | }); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/IceServer.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import com.google.gson.annotations.Expose; 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | public class IceServer { 7 | 8 | @SerializedName("url") 9 | @Expose 10 | public String url; 11 | @SerializedName("username") 12 | @Expose 13 | public String username; 14 | @SerializedName("credential") 15 | @Expose 16 | public String credential; 17 | 18 | @Override 19 | public String toString() { 20 | return "IceServer{" + 21 | "url='" + url + '\'' + 22 | ", username='" + username + '\'' + 23 | ", credential='" + credential + '\'' + 24 | '}'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | import androidx.appcompat.widget.Toolbar; 5 | import androidx.core.content.ContextCompat; 6 | 7 | import android.content.ComponentName; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.ServiceConnection; 11 | import android.media.projection.MediaProjectionManager; 12 | import android.net.LinkAddress; 13 | import android.os.Bundle; 14 | import android.os.Handler; 15 | import android.os.IBinder; 16 | import android.os.Looper; 17 | import android.os.Message; 18 | import android.provider.Settings; 19 | import android.util.DisplayMetrics; 20 | import android.util.Log; 21 | import android.view.Display; 22 | import android.view.Menu; 23 | import android.view.MenuItem; 24 | import android.view.View; 25 | import android.widget.CompoundButton; 26 | import android.widget.LinearLayout; 27 | import android.widget.RelativeLayout; 28 | import android.widget.Switch; 29 | import android.widget.TextView; 30 | import android.widget.ToggleButton; 31 | 32 | import com.google.android.gms.ads.AdRequest; 33 | import com.google.android.gms.ads.AdSize; 34 | import com.google.android.gms.ads.AdView; 35 | import com.google.android.gms.ads.MobileAds; 36 | import com.google.android.gms.ads.initialization.InitializationStatus; 37 | import com.google.android.gms.ads.initialization.OnInitializationCompleteListener; 38 | 39 | import java.net.Inet6Address; 40 | import java.util.List; 41 | 42 | public class MainActivity extends AppCompatActivity { 43 | private static final String TAG = MainActivity.class.getSimpleName(); 44 | 45 | private static final int PERM_ACTION_ACCESSIBILITY_SERVICE = 100; 46 | private static final int PERM_MEDIA_PROJECTION_SERVICE = 101; 47 | 48 | private static final int HANDLER_MESSAGE_UPDATE_NETWORK = 0; 49 | 50 | private int httpServerPort; 51 | 52 | private AppService appService = null; 53 | private AppServiceConnection serviceConnection = null; 54 | 55 | private NetworkHelper networkHelper = null; 56 | private SettingsHelper settingsHelper = null; 57 | private PermissionHelper permissionHelper; 58 | 59 | private AdView adView; 60 | 61 | @Override 62 | protected void onCreate(Bundle savedInstanceState) { 63 | Log.d(TAG, "Activity create"); 64 | 65 | super.onCreate(savedInstanceState); 66 | setContentView(R.layout.activity_main); 67 | Toolbar toolbar = findViewById(R.id.toolbar); 68 | setSupportActionBar(toolbar); 69 | 70 | initAds(); 71 | 72 | initSettings(); 73 | 74 | ToggleButton startButton = findViewById(R.id.startButton); 75 | startButton.setOnCheckedChangeListener(new ToggleButton.OnCheckedChangeListener() { 76 | @Override 77 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 78 | if (isChecked) { 79 | buttonView.setBackground(getDrawable(R.drawable.bg_button_on)); 80 | start(); 81 | } 82 | else { 83 | buttonView.setBackground(getDrawable(R.drawable.bg_button_off)); 84 | stop(); 85 | } 86 | } 87 | }); 88 | 89 | ToggleButton remoteControl = findViewById(R.id.remoteControlEnableSwitch); 90 | remoteControl.setOnCheckedChangeListener(new Switch.OnCheckedChangeListener() { 91 | @Override 92 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 93 | buttonView.setBackground(getDrawable(isChecked ? R.drawable.bg_button_on : 94 | R.drawable.bg_button_off)); 95 | remoteControlEnable(isChecked); 96 | } 97 | }); 98 | if (settingsHelper.isRemoteControlEnabled()) 99 | remoteControl.setChecked(true); 100 | 101 | if (AppService.isServiceRunning()) 102 | setStartButton(); 103 | 104 | initPermission(); 105 | 106 | initUrl(); 107 | } 108 | 109 | @Override 110 | public void onDestroy() { 111 | Log.d(TAG, "Activity destroy"); 112 | 113 | if (networkHelper != null) 114 | networkHelper.close(); 115 | unbindService(); 116 | uninitSettings(); 117 | super.onDestroy(); 118 | } 119 | 120 | private void start() { 121 | Log.d(TAG, "Stream start"); 122 | if (AppService.isServiceRunning()) { 123 | bindService(); 124 | return; 125 | } 126 | 127 | permissionHelper.requestInternetPermission(); 128 | } 129 | 130 | private void stop() { 131 | Log.d(TAG, "Stream stop"); 132 | if (!AppService.isServiceRunning()) 133 | return; 134 | 135 | stopService(); 136 | } 137 | 138 | private void initPermission() { 139 | permissionHelper = new PermissionHelper(this, new OnPermissionGrantedListener()); 140 | } 141 | 142 | private class OnPermissionGrantedListener implements 143 | PermissionHelper.OnPermissionGrantedListener { 144 | @Override 145 | public void onAccessNetworkStatePermissionGranted(boolean isGranted) { 146 | if (!isGranted) 147 | return; 148 | networkHelper = new NetworkHelper(getApplicationContext(), 149 | new OnNetworkChangeListener()); 150 | urlUpdate(); 151 | } 152 | 153 | @Override 154 | public void onInternetPermissionGranted(boolean isGranted) { 155 | if (isGranted) 156 | permissionHelper.requestReadExternalStoragePermission(); 157 | else 158 | resetStartButton(); 159 | } 160 | 161 | @Override 162 | public void onReadExternalStoragePermissionGranted(boolean isGranted) { 163 | if (isGranted) 164 | permissionHelper.requestWakeLockPermission(); 165 | else 166 | resetStartButton(); 167 | } 168 | 169 | @Override 170 | public void onWakeLockPermissionGranted(boolean isGranted) { 171 | if (isGranted) 172 | permissionHelper.requestForegroundServicePermission(); 173 | else 174 | resetStartButton(); 175 | } 176 | 177 | @Override 178 | public void onForegroundServicePermissionGranted(boolean isGranted) { 179 | if (isGranted) { 180 | //TODO permissionHelper.requestRecordAudioPermission(); 181 | startService(); 182 | } 183 | else 184 | resetStartButton(); 185 | } 186 | 187 | @Override 188 | public void onRecordAudioPermissionGranted(boolean isGranted) { 189 | if (isGranted) 190 | startService(); 191 | else 192 | resetStartButton(); 193 | } 194 | 195 | @Override 196 | public void onCameraPermissionGranted(boolean isGranted) { 197 | if (isGranted) 198 | startService(); 199 | else 200 | resetStartButton(); 201 | } 202 | } 203 | 204 | @Override 205 | public void onRequestPermissionsResult(int requestCode, String[] permissions, 206 | int[] grantResults) { 207 | permissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults); 208 | } 209 | 210 | private void startService() { 211 | Intent serviceIntent = new Intent(this, AppService.class); 212 | ContextCompat.startForegroundService(this, serviceIntent); 213 | serviceConnection = new AppServiceConnection(); 214 | bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); 215 | } 216 | 217 | private void stopService() { 218 | unbindService(); 219 | Intent serviceIntent = new Intent(this, AppService.class); 220 | stopService(serviceIntent); 221 | } 222 | 223 | private void bindService() { 224 | Intent serviceIntent = new Intent(this, AppService.class); 225 | serviceConnection = new AppServiceConnection(); 226 | bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); 227 | } 228 | 229 | private void unbindService() { 230 | if (serviceConnection == null) 231 | return; 232 | 233 | unbindService(serviceConnection); 234 | serviceConnection = null; 235 | } 236 | 237 | private class AppServiceConnection implements ServiceConnection { 238 | @Override 239 | public void onServiceConnected(ComponentName name, IBinder service) { 240 | AppService.AppServiceBinder binder = (AppService.AppServiceBinder)service; 241 | appService = binder.getService(); 242 | 243 | if (!appService.isServerRunning()) 244 | askMediaProjectionPermission(); 245 | else if (appService.isMouseAccessibilityServiceAvailable()) 246 | setRemoteControlSwitch(); 247 | } 248 | 249 | @Override 250 | public void onServiceDisconnected(ComponentName name) { 251 | appService = null; 252 | resetStartButton(); 253 | Log.e(TAG, "Service unexpectedly exited"); 254 | } 255 | } 256 | 257 | private void askMediaProjectionPermission() { 258 | MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) 259 | getSystemService(Context.MEDIA_PROJECTION_SERVICE); 260 | startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), 261 | PERM_MEDIA_PROJECTION_SERVICE); 262 | } 263 | 264 | @Override 265 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 266 | switch (requestCode) { 267 | case PERM_MEDIA_PROJECTION_SERVICE: 268 | if (resultCode == RESULT_OK) { 269 | if (!appService.serverStart(data, httpServerPort, 270 | isAccessibilityServiceEnabled(), getApplicationContext())) { 271 | resetStartButton(); 272 | return; 273 | } 274 | } 275 | else 276 | resetStartButton(); 277 | break; 278 | case PERM_ACTION_ACCESSIBILITY_SERVICE: 279 | if (isAccessibilityServiceEnabled()) 280 | enableAccessibilityService(true); 281 | else 282 | resetRemoteControlSwitch(); 283 | break; 284 | } 285 | super.onActivityResult(requestCode, resultCode, data); 286 | } 287 | 288 | private void setStartButton() { 289 | ToggleButton startButton = findViewById(R.id.startButton); 290 | startButton.setChecked(true); 291 | } 292 | 293 | private void resetStartButton() { 294 | ToggleButton startButton = findViewById(R.id.startButton); 295 | startButton.setChecked(false); 296 | } 297 | 298 | private void enableAccessibilityService(boolean isEnabled) { 299 | settingsHelper.setRemoteControlEnabled(isEnabled); 300 | 301 | if (appService != null) 302 | appService.accessibilityServiceSet(getApplicationContext(), isEnabled); 303 | } 304 | 305 | private boolean isAccessibilityServiceEnabled() { 306 | Context context = getApplicationContext(); 307 | ComponentName compName = new ComponentName(context, MouseAccessibilityService.class); 308 | String flatName = compName.flattenToString(); 309 | String enabledList = Settings.Secure.getString(context.getContentResolver(), 310 | Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); 311 | return enabledList != null && enabledList.contains(flatName); 312 | } 313 | 314 | private void setRemoteControlSwitch() { 315 | ToggleButton remoteControl = findViewById(R.id.remoteControlEnableSwitch); 316 | remoteControl.setChecked(true); 317 | } 318 | 319 | private void resetRemoteControlSwitch() { 320 | ToggleButton remoteControl = findViewById(R.id.remoteControlEnableSwitch); 321 | remoteControl.setChecked(false); 322 | } 323 | 324 | private void remoteControlEnable(boolean isEnabled) { 325 | if (isEnabled) { 326 | if (!isAccessibilityServiceEnabled()) { 327 | startActivityForResult(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS), 328 | PERM_ACTION_ACCESSIBILITY_SERVICE); 329 | } else { 330 | enableAccessibilityService(true); 331 | } 332 | } else { 333 | enableAccessibilityService(false); 334 | } 335 | 336 | } 337 | 338 | public void initUrl() { 339 | LinearLayout urlLayout = findViewById(R.id.urlLinerLayout); 340 | urlLayout.setVisibility(View.INVISIBLE); 341 | permissionHelper.requestAccessNetworkStatePermission(); 342 | } 343 | 344 | private class OnNetworkChangeListener implements NetworkHelper.OnNetworkChangeListener { 345 | @Override 346 | public void onChange() { 347 | // Interfaces need some time to update 348 | handler.sendEmptyMessageDelayed(HANDLER_MESSAGE_UPDATE_NETWORK, 1000); 349 | } 350 | } 351 | 352 | private Handler handler = new Handler(Looper.getMainLooper()) { 353 | @Override 354 | public void handleMessage(Message msg) { 355 | switch (msg.what) { 356 | case HANDLER_MESSAGE_UPDATE_NETWORK: 357 | urlUpdate(); 358 | break; 359 | default: 360 | super.handleMessage(msg); 361 | break; 362 | } 363 | } 364 | }; 365 | 366 | private void urlUpdate() { 367 | TextView urlsHeader = findViewById(R.id.urls_header); 368 | urlsHeader.setText(getResources().getString(R.string.no_active_connections)); 369 | LinearLayout urlLayout = findViewById(R.id.urlLinerLayout); 370 | urlLayout.setVisibility(View.INVISIBLE); 371 | 372 | List ipInfoList = networkHelper.getIpInfo(); 373 | for (NetworkHelper.IpInfo ipInfo : ipInfoList) { 374 | if (!ipInfo.interfaceType.equals("Wi-Fi")) 375 | continue; 376 | 377 | String type = ipInfo.interfaceType + " (" + ipInfo.interfaceName + ")"; 378 | TextView connectionType = findViewById(R.id.connectionTypeHeader); 379 | connectionType.setText(type); 380 | 381 | List addresses = ipInfo.addresses; 382 | for (LinkAddress address : addresses) { 383 | if (address.getAddress() instanceof Inet6Address) 384 | continue; 385 | 386 | String url = "https://" + address.getAddress().getHostAddress() + ":" + 387 | httpServerPort; 388 | TextView connectionURL = findViewById(R.id.connectionURL); 389 | connectionURL.setText(url); 390 | urlsHeader.setText(getResources().getString(R.string.urls_header)); 391 | urlLayout.setVisibility(View.VISIBLE); 392 | break; 393 | } 394 | } 395 | } 396 | 397 | @Override 398 | public boolean onCreateOptionsMenu(Menu menu) { 399 | getMenuInflater().inflate(R.xml.menu_main, menu); 400 | return true; 401 | } 402 | 403 | @Override 404 | public boolean onOptionsItemSelected(MenuItem item) { 405 | int id = item.getItemId(); 406 | 407 | if (id == R.id.action_settings) { 408 | Intent intent = new Intent(MainActivity.this, SettingsActivity.class); 409 | startActivity(intent); 410 | return true; 411 | } 412 | 413 | return super.onOptionsItemSelected(item); 414 | } 415 | 416 | private void initAds() { 417 | MobileAds.initialize(this, new OnInitializationCompleteListener() { 418 | @Override 419 | public void onInitializationComplete(InitializationStatus initializationStatus) { 420 | } 421 | }); 422 | 423 | RelativeLayout adLayout = findViewById(R.id.adLayout); 424 | adView = new AdView(this); 425 | RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout. 426 | LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); 427 | layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); 428 | 429 | String adUnitId = getString(BuildConfig.DEBUG ? R.string.adaptive_banner_ad_unit_id_test : 430 | R.string.adaptive_banner_ad_unit_id); 431 | adView.setAdUnitId(adUnitId); 432 | adLayout.addView(adView, layoutParams);; 433 | 434 | AdSize adSize = getAdSize(); 435 | adView.setAdSize(adSize); 436 | 437 | AdRequest adRequest = new AdRequest.Builder().build(); 438 | adView.loadAd(adRequest); 439 | } 440 | 441 | private AdSize getAdSize() { 442 | Display display = getWindowManager().getDefaultDisplay(); 443 | DisplayMetrics outMetrics = new DisplayMetrics(); 444 | display.getMetrics(outMetrics); 445 | 446 | float widthPixels = outMetrics.widthPixels; 447 | float density = outMetrics.density; 448 | 449 | int adWidth = (int) (widthPixels / density); 450 | 451 | return AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(this, adWidth); 452 | } 453 | 454 | public void initSettings() { 455 | settingsHelper = new SettingsHelper(getApplicationContext(), 456 | new OnSettingsChangeListener()); 457 | httpServerPort = settingsHelper.getPort(); 458 | } 459 | 460 | public void uninitSettings() { 461 | settingsHelper.close(); 462 | settingsHelper = null; 463 | } 464 | 465 | private class OnSettingsChangeListener implements SettingsHelper.OnSettingsChangeListener { 466 | @Override 467 | public void onPortChange(int port) { 468 | httpServerPort = port; 469 | urlUpdate(); 470 | if (AppService.isServiceRunning()) { 471 | if (!appService.serverRestart(httpServerPort)) 472 | resetStartButton(); 473 | } 474 | } 475 | } 476 | } 477 | 478 | 479 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/MouseAccessibilityService.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.accessibilityservice.AccessibilityService; 4 | import android.accessibilityservice.GestureDescription; 5 | import android.content.Context; 6 | import android.graphics.Path; 7 | import android.os.Build; 8 | import android.os.PowerManager; 9 | import android.util.DisplayMetrics; 10 | import android.util.Log; 11 | import android.view.Display; 12 | import android.view.WindowManager; 13 | import android.view.accessibility.AccessibilityEvent; 14 | 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | 19 | public class MouseAccessibilityService extends AccessibilityService { 20 | private static final String TAG = MouseAccessibilityService.class.getSimpleName(); 21 | 22 | private static final int PINCH_DURATION_MS = 400; 23 | private static final int PINCH_DISTANCE_CLOSE = 200; 24 | private static final int PINCH_DISTANCE_FAR = 800; 25 | 26 | private static MouseAccessibilityService instance; 27 | 28 | private AtomicBoolean lock = new AtomicBoolean(false); 29 | private boolean isMouseDown = false; 30 | private GestureDescription.StrokeDescription currentStroke = null; 31 | private int prevX = 0, prevY = 0; 32 | private List gestureList = new LinkedList<>(); 33 | private Display display = null; 34 | 35 | @Override 36 | public void onCreate() { 37 | super.onCreate(); 38 | instance = this; 39 | } 40 | 41 | @Override 42 | public void onAccessibilityEvent(AccessibilityEvent event) { 43 | } 44 | 45 | @Override 46 | public void onInterrupt() { 47 | } 48 | 49 | public void setContext(Context context) { 50 | WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); 51 | display = wm.getDefaultDisplay(); 52 | } 53 | 54 | public void mouseDown(int x, int y) { 55 | Log.d(TAG, "Mouse left button down at x=" + x + " y=" + y); 56 | synchronized (lock) { 57 | GestureDescription gesture = buildGesture(x, y, x, y, 0, 1, false, true); 58 | gestureList.add(gesture); 59 | if (gestureList.size() == 1) 60 | dispatchGestureHandler(); 61 | 62 | prevX = x; 63 | prevY = y; 64 | isMouseDown = true; 65 | } 66 | } 67 | 68 | public void mouseMove(int x, int y) { 69 | synchronized (lock) { 70 | if (!isMouseDown) 71 | return; 72 | if (prevX == x && prevY == y) 73 | return; 74 | 75 | GestureDescription gesture = buildGesture(prevX, prevY, x, y, 0, 1, true, true); 76 | gestureList.add(gesture); 77 | if (gestureList.size() == 1) 78 | dispatchGestureHandler(); 79 | 80 | prevX = x; 81 | prevY = y; 82 | } 83 | } 84 | 85 | public void mouseUp(int x, int y) { 86 | Log.d(TAG, "Mouse left button up at x=" + x + " y=" + y); 87 | synchronized (lock) { 88 | GestureDescription gesture = buildGesture(prevX, prevY, x, y, 0, 1, true, false); 89 | gestureList.add(gesture); 90 | if (gestureList.size() == 1) 91 | dispatchGestureHandler(); 92 | 93 | isMouseDown = false; 94 | } 95 | } 96 | 97 | private GestureDescription buildGesture(int x1, int y1, int x2, int y2, long startTime, 98 | long duration, boolean isContinuedGesture, 99 | boolean willContinue) { 100 | Path path = new Path(); 101 | path.moveTo(x1, y1); 102 | if (x1 != x2 || y1 != y2) 103 | path.lineTo(x2, y2); 104 | 105 | GestureDescription.StrokeDescription stroke; 106 | if (!isContinuedGesture) { 107 | stroke = new GestureDescription.StrokeDescription(path, startTime, duration, 108 | willContinue); 109 | } 110 | else { 111 | stroke = currentStroke.continueStroke(path, startTime, duration, willContinue); 112 | } 113 | 114 | GestureDescription.Builder builder = new GestureDescription.Builder(); 115 | builder.addStroke(stroke); 116 | GestureDescription gestureDescription = builder.build(); 117 | 118 | currentStroke = stroke; 119 | 120 | return gestureDescription; 121 | } 122 | 123 | public void mouseWheelZoomIn(int x, int y) { 124 | Log.d(TAG, "Zoom in at x=" + x + " y=" + y); 125 | synchronized (lock) { 126 | pinchGesture(x, y, PINCH_DISTANCE_CLOSE, PINCH_DISTANCE_FAR); 127 | } 128 | } 129 | 130 | public void mouseWheelZoomOut(int x, int y) { 131 | Log.d(TAG, "Zoom out at x=" + x + " y=" + y); 132 | synchronized (lock) { 133 | pinchGesture(x, y, PINCH_DISTANCE_FAR, PINCH_DISTANCE_CLOSE); 134 | } 135 | } 136 | 137 | private void pinchGesture(int x, int y, int startSpacing, int endSpacing) { 138 | int x1 = x - startSpacing / 2; 139 | int y1 = y - startSpacing / 2; 140 | int x2 = x - endSpacing / 2; 141 | int y2 = y - endSpacing / 2; 142 | 143 | if (x1 < 0) 144 | x1 = 0; 145 | if (y1 < 0) 146 | y1 = 0; 147 | if (x2 < 0) 148 | x2 = 0; 149 | if (y2 < 0) 150 | y2 = 0; 151 | 152 | Path path1 = new Path(); 153 | path1.moveTo(x1, y1); 154 | path1.lineTo(x2, y2); 155 | GestureDescription.StrokeDescription stroke1 = new 156 | GestureDescription.StrokeDescription(path1, 0, PINCH_DURATION_MS, false); 157 | 158 | x1 = x + startSpacing / 2; 159 | y1 = y + startSpacing / 2; 160 | x2 = x + endSpacing / 2; 161 | y2 = y + endSpacing / 2; 162 | 163 | DisplayMetrics metrics = new DisplayMetrics(); 164 | display.getRealMetrics(metrics); 165 | if (x1 > metrics.widthPixels) 166 | x1 = metrics.widthPixels; 167 | if (y1 > metrics.heightPixels) 168 | y1 = metrics.heightPixels; 169 | if (x2 > metrics.widthPixels) 170 | x2 = metrics.widthPixels; 171 | if (y2 > metrics.heightPixels) 172 | y2 = metrics.heightPixels; 173 | 174 | Path path2 = new Path(); 175 | path2.moveTo(x1, y1); 176 | path2.lineTo(x2, y2); 177 | GestureDescription.StrokeDescription stroke2 = new 178 | GestureDescription.StrokeDescription(path2, 0, PINCH_DURATION_MS, false); 179 | 180 | GestureDescription.Builder builder = new GestureDescription.Builder(); 181 | builder.addStroke(stroke1); 182 | builder.addStroke(stroke2); 183 | GestureDescription gesture = builder.build(); 184 | 185 | gestureList.add(gesture); 186 | if (gestureList.size() == 1) 187 | dispatchGestureHandler(); 188 | } 189 | 190 | private void dispatchGestureHandler() { 191 | GestureDescription gesture = gestureList.get(0); 192 | 193 | if (!instance.dispatchGesture(gesture, gestureResultCallback, null)) { 194 | Log.e(TAG, "Gesture was not dispatched"); 195 | gestureList.clear(); 196 | return; 197 | } 198 | } 199 | 200 | private AccessibilityService.GestureResultCallback gestureResultCallback = 201 | new AccessibilityService.GestureResultCallback() { 202 | @Override 203 | public void onCompleted(GestureDescription gestureDescription) { 204 | synchronized (lock) { 205 | gestureList.remove(0); 206 | if (gestureList.isEmpty()) 207 | return; 208 | dispatchGestureHandler(); 209 | } 210 | 211 | super.onCompleted(gestureDescription); 212 | } 213 | 214 | @Override 215 | public void onCancelled(GestureDescription gestureDescription) { 216 | synchronized (lock) { 217 | Log.w(TAG, "Gesture canceled"); 218 | gestureList.remove(0); 219 | super.onCancelled(gestureDescription); 220 | } 221 | } 222 | }; 223 | 224 | public void backButtonClick() { 225 | Log.d(TAG, "Back button pressed"); 226 | instance.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK); 227 | } 228 | 229 | public void homeButtonClick() { 230 | Log.d(TAG, "Home button pressed"); 231 | instance.performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME); 232 | } 233 | 234 | public void recentButtonClick() { 235 | Log.d(TAG, "Recent button pressed"); 236 | instance.performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS); 237 | } 238 | 239 | public void powerButtonClick() { 240 | Log.d(TAG, "Power button pressed"); 241 | instance.performGlobalAction(AccessibilityService.GLOBAL_ACTION_POWER_DIALOG); 242 | } 243 | 244 | public void lockButtonClick() { 245 | Log.d(TAG, "Lock button pressed"); 246 | if (!isScreenOff()) 247 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 248 | instance.performGlobalAction(AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN); 249 | } else { 250 | AdminActivity.lockScreen(instance); 251 | } 252 | else 253 | wakeScreenIfNecessary(); 254 | } 255 | 256 | private boolean isScreenOff() { 257 | PowerManager pm = (PowerManager) instance.getSystemService(Context.POWER_SERVICE); 258 | return !pm.isInteractive(); 259 | } 260 | 261 | @SuppressWarnings("deprecation") 262 | private void wakeScreenIfNecessary() { 263 | PowerManager pm = (PowerManager) instance.getSystemService(Context.POWER_SERVICE); 264 | if (pm.isInteractive()) { 265 | return; 266 | } 267 | 268 | PowerManager.WakeLock screenLock = 269 | pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | 270 | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG); 271 | screenLock.acquire(); 272 | screenLock.release(); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/NetworkHelper.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.LinkAddress; 6 | import android.net.LinkProperties; 7 | import android.net.Network; 8 | import android.net.NetworkCapabilities; 9 | import android.util.Log; 10 | import java.util.LinkedList; 11 | import java.util.List; 12 | 13 | public class NetworkHelper { 14 | private static final String TAG = NetworkHelper.class.getSimpleName(); 15 | 16 | private ConnectivityManager connectivityManager; 17 | private OnNetworkChangeListener onNetworkChangeListener; 18 | 19 | public class IpInfo { 20 | public String interfaceName; 21 | public String interfaceType; 22 | public List addresses; 23 | } 24 | 25 | public interface OnNetworkChangeListener { 26 | void onChange(); 27 | } 28 | 29 | public NetworkHelper(Context context, OnNetworkChangeListener callback) { 30 | onNetworkChangeListener = callback; 31 | 32 | connectivityManager = 33 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 34 | connectivityManager.registerDefaultNetworkCallback(networkCallback); 35 | } 36 | 37 | public void close() { 38 | connectivityManager.unregisterNetworkCallback(networkCallback); 39 | } 40 | 41 | private final ConnectivityManager.NetworkCallback networkCallback = 42 | new ConnectivityManager.NetworkCallback() { 43 | 44 | @Override 45 | public void onAvailable(Network network) { 46 | Log.d(TAG, "Network available"); 47 | super.onAvailable(network); 48 | onNetworkChangeListener.onChange(); 49 | } 50 | 51 | @Override 52 | public void onLost(Network network) { 53 | Log.d(TAG, "Network lost"); 54 | super.onLost(network); 55 | onNetworkChangeListener.onChange(); 56 | } 57 | }; 58 | 59 | private String getInterfaceType(NetworkCapabilities networkCapabilities) { 60 | String interfaceType; 61 | 62 | if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) 63 | interfaceType = "Mobile"; 64 | else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) 65 | interfaceType = "Wi-Fi"; 66 | else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) 67 | interfaceType = "Bluetooth"; 68 | else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) 69 | interfaceType = "Ethernet"; 70 | else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) 71 | interfaceType = "VPN"; 72 | else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) 73 | interfaceType = "Wi-Fi Aware"; 74 | else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN)) 75 | interfaceType = "LoWPAN"; 76 | else 77 | interfaceType = "Unknown"; 78 | 79 | return interfaceType; 80 | } 81 | 82 | public List getIpInfo() { 83 | List ipInfoList = new LinkedList<>(); 84 | 85 | Network[] networks = connectivityManager.getAllNetworks(); 86 | for (Network network : networks) { 87 | LinkProperties linkProperties = connectivityManager.getLinkProperties(network); 88 | NetworkCapabilities networkCapabilities = 89 | connectivityManager.getNetworkCapabilities(network); 90 | 91 | if (linkProperties == null || networkCapabilities == null) { 92 | Log.e(TAG, "Failed to get network properties"); 93 | continue; 94 | } 95 | 96 | String interfaceName = linkProperties.getInterfaceName(); 97 | if (interfaceName == null) { 98 | Log.e(TAG, "Failed to get interface name"); 99 | continue; 100 | } 101 | 102 | IpInfo ipInfo = new IpInfo(); 103 | ipInfo.interfaceName = interfaceName; 104 | ipInfo.interfaceType = getInterfaceType(networkCapabilities); 105 | ipInfo.addresses = new LinkedList<>(); 106 | List addresses = linkProperties.getLinkAddresses(); 107 | for (LinkAddress address : addresses) { 108 | if (address.getAddress().isLinkLocalAddress()) 109 | continue; 110 | ipInfo.addresses.add(address); 111 | } 112 | 113 | ipInfoList.add(ipInfo); 114 | } 115 | 116 | return ipInfoList; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/PermissionHelper.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.pm.PackageManager; 6 | import android.os.Build; 7 | import android.util.Log; 8 | 9 | import androidx.core.app.ActivityCompat; 10 | import androidx.core.content.ContextCompat; 11 | 12 | public class PermissionHelper { 13 | private static final String TAG = PermissionHelper.class.getSimpleName(); 14 | 15 | public interface OnPermissionGrantedListener { 16 | void onAccessNetworkStatePermissionGranted(boolean isGranted); 17 | void onInternetPermissionGranted(boolean isGranted); 18 | void onReadExternalStoragePermissionGranted(boolean isGranted); 19 | void onWakeLockPermissionGranted(boolean isGranted); 20 | void onForegroundServicePermissionGranted(boolean isGranted); 21 | void onRecordAudioPermissionGranted(boolean isGranted); 22 | void onCameraPermissionGranted(boolean isGranted); 23 | } 24 | 25 | private static final int PERM_ACCESS_NETWORK_STATE = 0; 26 | private static final int PERM_INTERNET = 1; 27 | private static final int PERM_READ_EXTERNAL_STORAGE = 2; 28 | private static final int PERM_WAKE_LOCK = 3; 29 | private static final int PERM_FOREGROUND_SERVICE = 4; 30 | private static final int PERM_RECORD_AUDIO = 5; 31 | private static final int PERM_CAMERA = 5; 32 | 33 | private Activity activity; 34 | private OnPermissionGrantedListener onPermissionGrantedListener; 35 | 36 | public PermissionHelper(Activity a, OnPermissionGrantedListener listener) { 37 | activity = a; 38 | onPermissionGrantedListener = listener; 39 | } 40 | 41 | public void requestAccessNetworkStatePermission() { 42 | if (ContextCompat.checkSelfPermission(activity.getApplicationContext(), 43 | Manifest.permission.ACCESS_NETWORK_STATE) == PackageManager.PERMISSION_GRANTED) { 44 | Log.d(TAG, "Network state permission granted"); 45 | onPermissionGrantedListener.onAccessNetworkStatePermissionGranted(true); 46 | return; 47 | } 48 | 49 | ActivityCompat.requestPermissions(activity, new String[]{ 50 | Manifest.permission.ACCESS_NETWORK_STATE }, PERM_ACCESS_NETWORK_STATE); 51 | } 52 | 53 | public void requestInternetPermission() { 54 | if (onPermissionGrantedListener == null) 55 | return; 56 | 57 | if (ContextCompat.checkSelfPermission(activity.getApplicationContext(), 58 | Manifest.permission.INTERNET) == PackageManager.PERMISSION_GRANTED) { 59 | Log.d(TAG, "Internet permission granted"); 60 | onPermissionGrantedListener.onInternetPermissionGranted(true); 61 | return; 62 | } 63 | 64 | ActivityCompat.requestPermissions(activity, new String[]{ Manifest.permission.INTERNET }, 65 | PERM_INTERNET); 66 | } 67 | 68 | public void requestReadExternalStoragePermission() { 69 | if (ContextCompat.checkSelfPermission(activity.getApplicationContext(), 70 | Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { 71 | Log.d(TAG, "Read external storage permission granted"); 72 | onPermissionGrantedListener.onReadExternalStoragePermissionGranted(true); 73 | return; 74 | } 75 | 76 | ActivityCompat.requestPermissions(activity, 77 | new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE }, 78 | PERM_READ_EXTERNAL_STORAGE); 79 | } 80 | 81 | public void requestWakeLockPermission() { 82 | if (ContextCompat.checkSelfPermission(activity.getApplicationContext(), 83 | Manifest.permission.WAKE_LOCK) == PackageManager.PERMISSION_GRANTED) { 84 | Log.d(TAG, "Wake lock permission granted"); 85 | onPermissionGrantedListener.onWakeLockPermissionGranted(true); 86 | return; 87 | } 88 | 89 | ActivityCompat.requestPermissions(activity, 90 | new String[]{ Manifest.permission.WAKE_LOCK }, 91 | PERM_WAKE_LOCK); 92 | } 93 | 94 | public void requestForegroundServicePermission() { 95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 96 | if (ContextCompat.checkSelfPermission(activity.getApplicationContext(), 97 | Manifest.permission.FOREGROUND_SERVICE) == PackageManager.PERMISSION_GRANTED) { 98 | Log.d(TAG, "Foreground service permission granted"); 99 | onPermissionGrantedListener.onForegroundServicePermissionGranted(true); 100 | return; 101 | } 102 | 103 | ActivityCompat.requestPermissions(activity, 104 | new String[]{ Manifest.permission.FOREGROUND_SERVICE }, 105 | PERM_FOREGROUND_SERVICE); 106 | } else { 107 | onPermissionGrantedListener.onForegroundServicePermissionGranted(true); 108 | } 109 | } 110 | 111 | public void requestRecordAudioPermission() { 112 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 113 | if (ContextCompat.checkSelfPermission(activity.getApplicationContext(), 114 | Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { 115 | Log.d(TAG, "Audio permission granted"); 116 | onPermissionGrantedListener.onRecordAudioPermissionGranted(true); 117 | return; 118 | } 119 | 120 | ActivityCompat.requestPermissions(activity, 121 | new String[]{ Manifest.permission.RECORD_AUDIO }, 122 | PERM_RECORD_AUDIO); 123 | } else { 124 | onPermissionGrantedListener.onRecordAudioPermissionGranted(true); 125 | } 126 | } 127 | 128 | public void requestCameraPermission() { 129 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 130 | if (ContextCompat.checkSelfPermission(activity.getApplicationContext(), 131 | Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { 132 | Log.d(TAG, "Camera permission granted"); 133 | onPermissionGrantedListener.onCameraPermissionGranted(true); 134 | return; 135 | } 136 | 137 | ActivityCompat.requestPermissions(activity, 138 | new String[]{ Manifest.permission.CAMERA }, 139 | PERM_CAMERA); 140 | } else { 141 | onPermissionGrantedListener.onCameraPermissionGranted(true); 142 | } 143 | } 144 | 145 | public void onRequestPermissionsResult(int requestCode, String[] permissions, 146 | int[] grantResults) { 147 | switch (requestCode) { 148 | case PERM_ACCESS_NETWORK_STATE: 149 | if (grantResults.length > 0 150 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 151 | Log.d(TAG, "Network state permission granted"); 152 | onPermissionGrantedListener.onAccessNetworkStatePermissionGranted(true); 153 | } else { 154 | Log.d(TAG, "Network state permission denied"); 155 | onPermissionGrantedListener.onAccessNetworkStatePermissionGranted(false); 156 | } 157 | break; 158 | case PERM_INTERNET: 159 | if (grantResults.length > 0 160 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 161 | Log.d(TAG, "Internet permission granted"); 162 | onPermissionGrantedListener.onInternetPermissionGranted(true); 163 | } else { 164 | Log.d(TAG, "Internet permission denied"); 165 | onPermissionGrantedListener.onInternetPermissionGranted(false); 166 | } 167 | break; 168 | case PERM_READ_EXTERNAL_STORAGE: 169 | if (grantResults.length > 0 170 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 171 | Log.d(TAG, "External storage permission granted"); 172 | onPermissionGrantedListener.onReadExternalStoragePermissionGranted(true); 173 | } else { 174 | Log.d(TAG, "External storage permission denied"); 175 | onPermissionGrantedListener.onReadExternalStoragePermissionGranted(false); 176 | } 177 | break; 178 | case PERM_WAKE_LOCK: 179 | if (grantResults.length > 0 180 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 181 | Log.d(TAG, "Wake lock permission granted"); 182 | onPermissionGrantedListener.onWakeLockPermissionGranted(true); 183 | } else { 184 | Log.d(TAG, "Wake lock permission denied"); 185 | onPermissionGrantedListener.onWakeLockPermissionGranted(false); 186 | } 187 | break; 188 | case PERM_FOREGROUND_SERVICE: 189 | if (grantResults.length > 0 190 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 191 | Log.d(TAG, "Foreground service permission granted"); 192 | onPermissionGrantedListener.onForegroundServicePermissionGranted(true); 193 | } else { 194 | Log.d(TAG, "Foreground service permission denied"); 195 | onPermissionGrantedListener.onForegroundServicePermissionGranted(false); 196 | } 197 | break; 198 | case PERM_RECORD_AUDIO: 199 | if (grantResults.length > 0 200 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 201 | Log.d(TAG, "Record audio permission granted"); 202 | onPermissionGrantedListener.onRecordAudioPermissionGranted(true); 203 | } else { 204 | Log.d(TAG, "Record audio permission denied"); 205 | onPermissionGrantedListener.onRecordAudioPermissionGranted(false); 206 | } 207 | break; 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.os.Bundle; 4 | import android.view.MenuItem; 5 | 6 | import androidx.appcompat.app.ActionBar; 7 | import androidx.appcompat.app.AppCompatActivity; 8 | import androidx.core.app.NavUtils; 9 | 10 | public class SettingsActivity extends AppCompatActivity { 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.activity_settings); 15 | 16 | ActionBar actionBar = this.getSupportActionBar(); 17 | if (actionBar != null) { 18 | actionBar.setDisplayHomeAsUpEnabled(true); 19 | } 20 | } 21 | 22 | @Override 23 | public boolean onOptionsItemSelected(MenuItem item) { 24 | int id = item.getItemId(); 25 | if (id == android.R.id.home) { 26 | NavUtils.navigateUpFromSameTask(this); 27 | } 28 | return super.onOptionsItemSelected(item); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/SettingsFragment.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.os.Bundle; 4 | import android.text.Editable; 5 | import android.text.InputFilter; 6 | import android.text.InputType; 7 | import android.text.TextWatcher; 8 | import android.widget.Button; 9 | import android.widget.EditText; 10 | 11 | import androidx.preference.EditTextPreference; 12 | import androidx.preference.PreferenceFragmentCompat; 13 | 14 | public class SettingsFragment extends PreferenceFragmentCompat { 15 | private static final int PORT_MIN = 1024; 16 | private static final int PORT_MAX = 65535; 17 | 18 | @Override 19 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 20 | addPreferencesFromResource(R.xml.root_preferences); 21 | 22 | EditTextPreference editTextPreference = getPreferenceManager(). 23 | findPreference("port"); 24 | editTextPreference.setOnBindEditTextListener(new EditTextPreference. 25 | OnBindEditTextListener() { 26 | @Override 27 | public void onBindEditText(final EditText editText) { 28 | editText.setInputType(InputType.TYPE_CLASS_NUMBER); 29 | editText.setFilters(new InputFilter[]{ new InputFilter.LengthFilter(5) }); 30 | editText.addTextChangedListener(new TextWatcher() { 31 | @Override 32 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 33 | } 34 | 35 | @Override 36 | public void onTextChanged(CharSequence s, int start, int before, int count) { 37 | } 38 | 39 | @Override 40 | public void afterTextChanged(Editable editable) { 41 | int number; 42 | try { 43 | number = Integer.parseInt(editable.toString()); 44 | } catch (Exception e) { 45 | number = 0; 46 | } 47 | Button okButton = editText.getRootView().findViewById(android.R.id.button1); 48 | okButton.setEnabled(number >= PORT_MIN && number <= PORT_MAX); 49 | } 50 | }); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/SettingsHelper.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.util.Log; 6 | 7 | import androidx.preference.PreferenceManager; 8 | 9 | public class SettingsHelper { 10 | public interface OnSettingsChangeListener { 11 | void onPortChange(int port); 12 | } 13 | 14 | private static final String TAG = SettingsHelper.class.getSimpleName(); 15 | 16 | private static final int HTTP_SERVER_PORT_DEFAULT = 8080; 17 | private static final String SETTINGS_NAME_PORT = "port"; 18 | private static final String SETTINGS_NAME_REMOTE_CONTROL = "remote_control"; 19 | private OnSettingsChangeListener onSettingsChangeListener; 20 | 21 | private SharedPreferenceChangeListener sharedPreferenceChangeListener; 22 | private SharedPreferences sharedPreferences; 23 | 24 | SettingsHelper(Context context, OnSettingsChangeListener listener) { 25 | sharedPreferenceChangeListener = new SharedPreferenceChangeListener(); 26 | sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); 27 | sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); 28 | onSettingsChangeListener = listener; 29 | } 30 | 31 | public void close() { 32 | onSettingsChangeListener = null; 33 | sharedPreferences. 34 | unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); 35 | sharedPreferenceChangeListener = null; 36 | sharedPreferences = null; 37 | } 38 | 39 | public int getPort() { 40 | int port; 41 | String portString = sharedPreferences.getString(SETTINGS_NAME_PORT, 42 | Integer.toString(HTTP_SERVER_PORT_DEFAULT)); 43 | try { 44 | port = Integer.parseInt(portString); 45 | } catch (Exception e) { 46 | Log.d(TAG, "Failed to parse port settings"); 47 | port = HTTP_SERVER_PORT_DEFAULT; 48 | } 49 | 50 | return port; 51 | } 52 | 53 | public void setRemoteControlEnabled(boolean enabled) { 54 | SharedPreferences.Editor editor = sharedPreferences.edit(); 55 | editor.putBoolean(SETTINGS_NAME_REMOTE_CONTROL, enabled); 56 | editor.commit(); 57 | } 58 | 59 | public boolean isRemoteControlEnabled() { 60 | boolean isEnabled; 61 | try { 62 | isEnabled = sharedPreferences.getBoolean(SETTINGS_NAME_REMOTE_CONTROL, false); 63 | } catch (Exception e) { 64 | Log.e(TAG, "Failed to parse remote control settings"); 65 | isEnabled = false; 66 | } 67 | 68 | return isEnabled; 69 | } 70 | 71 | private class SharedPreferenceChangeListener implements 72 | SharedPreferences.OnSharedPreferenceChangeListener { 73 | @Override 74 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 75 | if (onSettingsChangeListener == null) 76 | return; 77 | if (key.equals(SETTINGS_NAME_PORT)) { 78 | onSettingsChangeListener.onPortChange(getPort()); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/TurnServer.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import retrofit2.http.Header; 4 | import retrofit2.http.PUT; 5 | import retrofit2.Call; 6 | 7 | public interface TurnServer { 8 | @PUT("/_turn/") 9 | Call getIceCandidates(@Header("Authorization") String authkey); 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/TurnServerPojo.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import com.google.gson.annotations.Expose; 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | import java.util.List; 7 | 8 | public class TurnServerPojo { 9 | 10 | @SerializedName("s") 11 | @Expose 12 | public Integer s; 13 | @SerializedName("p") 14 | @Expose 15 | public String p; 16 | @SerializedName("e") 17 | @Expose 18 | public Object e; 19 | @SerializedName("v") 20 | @Expose 21 | public IceServerList iceServerList; 22 | 23 | class IceServerList { 24 | 25 | @SerializedName("iceServers") 26 | @Expose 27 | public List iceServers = null; 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/Utils.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import java.util.Random; 4 | 5 | public class Utils { 6 | public static String randomString(int len) { 7 | char [] chars = new char[len]; 8 | String symbols = "0123456789abcdefghijklmnopqrstuvwxyz"; 9 | 10 | Random rand = new Random(); 11 | for (int i = 0; i < len; i++) { 12 | int index = rand.nextInt(len); 13 | chars[i] = symbols.charAt(index); 14 | } 15 | return String.copyValueOf(chars); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/bbogush/web_screen/WebRtcManager.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.media.projection.MediaProjection; 6 | import android.util.Base64; 7 | import android.util.DisplayMetrics; 8 | import android.util.Log; 9 | import android.view.Display; 10 | import android.view.WindowManager; 11 | 12 | import androidx.annotation.NonNull; 13 | 14 | import org.json.JSONException; 15 | import org.json.JSONObject; 16 | import org.webrtc.AudioSource; 17 | import org.webrtc.AudioTrack; 18 | import org.webrtc.CameraEnumerator; 19 | import org.webrtc.DefaultVideoDecoderFactory; 20 | import org.webrtc.DefaultVideoEncoderFactory; 21 | import org.webrtc.EglBase; 22 | import org.webrtc.IceCandidate; 23 | import org.webrtc.Logging; 24 | import org.webrtc.MediaConstraints; 25 | import org.webrtc.MediaStream; 26 | import org.webrtc.PeerConnection; 27 | import org.webrtc.PeerConnectionFactory; 28 | import org.webrtc.ScreenCapturerAndroid; 29 | import org.webrtc.SessionDescription; 30 | import org.webrtc.SurfaceTextureHelper; 31 | import org.webrtc.VideoCapturer; 32 | import org.webrtc.VideoSource; 33 | import org.webrtc.VideoTrack; 34 | 35 | import java.io.IOException; 36 | import java.io.UnsupportedEncodingException; 37 | import java.util.ArrayList; 38 | import java.util.List; 39 | 40 | import retrofit2.Call; 41 | import retrofit2.Callback; 42 | import retrofit2.Response; 43 | import retrofit2.Retrofit; 44 | import retrofit2.converter.gson.GsonConverterFactory; 45 | 46 | public class WebRtcManager { 47 | private static final String TAG = WebRtcManager.class.getSimpleName(); 48 | 49 | private static final boolean ENABLE_INTEL_VP8_ENCODER = true; 50 | private static final boolean ENABLE_H264_HIGH_PROFILE = true; 51 | private static final int FRAMES_PER_SECOND = 30; 52 | private static final String SDP_PARAM = "sdp"; 53 | private static final String ICE_PARAM = "ice"; 54 | 55 | private VideoCapturer videoCapturer; 56 | private EglBase rootEglBase; 57 | private PeerConnectionFactory peerConnectionFactory; 58 | private MediaConstraints audioConstraints; 59 | private MediaConstraints videoConstraints; 60 | private VideoTrack localVideoTrack; 61 | private AudioSource audioSource; 62 | private AudioTrack localAudioTrack; 63 | private PeerConnection localPeer = null; 64 | private MediaConstraints sdpConstraints; 65 | private HttpServer server; 66 | 67 | List peerIceServers = new ArrayList<>(); 68 | private List iceServers = null; 69 | 70 | private Display display; 71 | private DisplayMetrics screenMetrics = new DisplayMetrics(); 72 | private Thread rotationDetectorThread = null; 73 | 74 | public WebRtcManager(Intent intent, Context context, HttpServer server) { 75 | this.server = server; 76 | //XXX getIceServers(); 77 | WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); 78 | display = wm.getDefaultDisplay(); 79 | 80 | createMediaProjection(intent); 81 | initWebRTC(context); 82 | } 83 | 84 | public void close() { 85 | stop(); 86 | stopRotationDetector(); 87 | destroyMediaProjection(); 88 | } 89 | 90 | private void createMediaProjection(Intent intent) { 91 | videoCapturer = new ScreenCapturerAndroid(intent, 92 | new MediaProjection.Callback() { 93 | @Override 94 | public void onStop() { 95 | super.onStop(); 96 | Log.e(TAG, "User has revoked media projection permissions"); 97 | } 98 | }); 99 | } 100 | 101 | private void destroyMediaProjection() { 102 | try { 103 | videoCapturer.stopCapture(); 104 | } catch (InterruptedException e) { 105 | e.printStackTrace(); 106 | return; 107 | } 108 | videoCapturer = null; 109 | } 110 | 111 | private void initWebRTC(Context context) { 112 | rootEglBase = EglBase.create(); 113 | 114 | PeerConnectionFactory.InitializationOptions initializationOptions = 115 | PeerConnectionFactory.InitializationOptions.builder(context) 116 | .createInitializationOptions(); 117 | PeerConnectionFactory.initialize(initializationOptions); 118 | 119 | PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); 120 | DefaultVideoEncoderFactory defaultVideoEncoderFactory = new DefaultVideoEncoderFactory( 121 | rootEglBase.getEglBaseContext(), ENABLE_INTEL_VP8_ENCODER, 122 | ENABLE_H264_HIGH_PROFILE); 123 | DefaultVideoDecoderFactory defaultVideoDecoderFactory = new 124 | DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext()); 125 | peerConnectionFactory = PeerConnectionFactory.builder() 126 | .setOptions(options) 127 | .setVideoEncoderFactory(defaultVideoEncoderFactory) 128 | .setVideoDecoderFactory(defaultVideoDecoderFactory) 129 | .createPeerConnectionFactory(); 130 | 131 | //XXX enable camera 132 | //videoCapturer = createCameraCapturer(new Camera1Enumerator(false)); 133 | 134 | audioConstraints = new MediaConstraints(); 135 | videoConstraints = new MediaConstraints(); 136 | 137 | SurfaceTextureHelper surfaceTextureHelper; 138 | surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", 139 | rootEglBase.getEglBaseContext()); 140 | VideoSource videoSource = 141 | peerConnectionFactory.createVideoSource(videoCapturer.isScreencast()); 142 | videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver()); 143 | 144 | localVideoTrack = peerConnectionFactory.createVideoTrack("100", videoSource); 145 | 146 | //TODO audioSource = peerConnectionFactory.createAudioSource(audioConstraints); 147 | //TODO localAudioTrack = peerConnectionFactory.createAudioTrack("101", audioSource); 148 | 149 | display.getRealMetrics(screenMetrics); 150 | if (videoCapturer != null) { 151 | videoCapturer.startCapture(screenMetrics.widthPixels, screenMetrics.heightPixels, 152 | FRAMES_PER_SECOND); 153 | startRotationDetector(); 154 | } 155 | } 156 | 157 | public void start(HttpServer server) { 158 | Log.d(TAG, "WebRTC start"); 159 | createPeerConnection(); 160 | doCall(server); 161 | } 162 | 163 | public void stop() { 164 | Log.d(TAG, "WebRTC stop"); 165 | if (localPeer == null) 166 | return; 167 | localPeer.close(); 168 | localPeer = null; 169 | } 170 | 171 | private void createPeerConnection() { 172 | PeerConnection.RTCConfiguration rtcConfig = 173 | new PeerConnection.RTCConfiguration(peerIceServers); 174 | // TCP candidates are only useful when connecting to a server that supports 175 | // ICE-TCP. 176 | rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; 177 | rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE; 178 | rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE; 179 | rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy 180 | .GATHER_CONTINUALLY; 181 | rtcConfig.keyType = PeerConnection.KeyType.ECDSA; 182 | localPeer = peerConnectionFactory.createPeerConnection(rtcConfig, 183 | new CustomPeerConnectionObserver("localPeerCreation") { 184 | @Override 185 | public void onIceCandidate(IceCandidate iceCandidate) { 186 | super.onIceCandidate(iceCandidate); 187 | onIceCandidateReceived(iceCandidate); 188 | } 189 | 190 | @Override 191 | public void onAddStream(MediaStream mediaStream) { 192 | super.onAddStream(mediaStream); 193 | Log.d(TAG, "Unexpected remote stream received."); 194 | } 195 | }); 196 | 197 | addStreamToLocalPeer(); 198 | } 199 | 200 | public void onIceCandidateReceived(IceCandidate iceCandidate) { 201 | JSONObject messageJson = new JSONObject(); 202 | JSONObject iceJson = new JSONObject(); 203 | try { 204 | iceJson.put("type", "candidate"); 205 | iceJson.put("label", iceCandidate.sdpMLineIndex); 206 | iceJson.put("id", iceCandidate.sdpMid); 207 | iceJson.put("candidate", iceCandidate.sdp); 208 | 209 | messageJson.put("type", "ice"); 210 | messageJson.put("ice", iceJson); 211 | 212 | String messageJsonStr = messageJson.toString(); 213 | //XXX broadcast 214 | server.send(messageJson.toString()); 215 | Log.d(TAG, "Send ICE candidates: " + messageJsonStr); 216 | } catch (Exception e) { 217 | e.printStackTrace(); 218 | return; 219 | } 220 | } 221 | 222 | private void addStreamToLocalPeer() { 223 | MediaStream stream = peerConnectionFactory.createLocalMediaStream("102"); 224 | //TODO stream.addTrack(localAudioTrack); 225 | stream.addTrack(localVideoTrack); 226 | localPeer.addStream(stream); 227 | } 228 | 229 | private void doCall(HttpServer server) { 230 | sdpConstraints = new MediaConstraints(); 231 | //TODO sdpConstraints.mandatory.add( 232 | // new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")); 233 | sdpConstraints.mandatory.add( 234 | new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true")); 235 | localPeer.createOffer(new CustomSdpObserver("localCreateOffer") { 236 | @Override 237 | public void onCreateSuccess(SessionDescription sessionDescription) { 238 | super.onCreateSuccess(sessionDescription); 239 | localPeer.setLocalDescription(new CustomSdpObserver("localSetLocalDesc"), 240 | sessionDescription); 241 | 242 | JSONObject messageJson = new JSONObject(); 243 | JSONObject sdpJson = new JSONObject(); 244 | try { 245 | sdpJson.put("type", sessionDescription.type.canonicalForm()); 246 | sdpJson.put("sdp", sessionDescription.description); 247 | 248 | messageJson.put("type", "sdp"); 249 | messageJson.put("sdp", sdpJson); 250 | } catch (JSONException e) { 251 | e.printStackTrace(); 252 | return; 253 | } 254 | 255 | String messageJsonStr = messageJson.toString(); 256 | try { 257 | server.send(messageJsonStr); 258 | } catch (IOException e) { 259 | e.printStackTrace(); 260 | return; 261 | } 262 | Log.d(TAG, "Send SDP: " + messageJsonStr); 263 | } 264 | }, sdpConstraints); 265 | } 266 | 267 | public void onAnswerReceived(JSONObject data) { 268 | JSONObject json; 269 | try { 270 | json = data.getJSONObject(SDP_PARAM); 271 | } catch (JSONException e) { 272 | e.printStackTrace(); 273 | return; 274 | } 275 | 276 | Log.d(TAG, "Remote SDP received: " + json.toString()); 277 | 278 | try { 279 | localPeer.setRemoteDescription(new CustomSdpObserver("localSetRemote"), 280 | new SessionDescription(SessionDescription.Type.fromCanonicalForm(json.getString( 281 | "type").toLowerCase()), json.getString("sdp"))); 282 | } catch (JSONException e) { 283 | e.printStackTrace(); 284 | } 285 | } 286 | 287 | public void onIceCandidateReceived(JSONObject data) { 288 | JSONObject json; 289 | try { 290 | json = data.getJSONObject(ICE_PARAM); 291 | } catch (JSONException e) { 292 | e.printStackTrace(); 293 | return; 294 | } 295 | 296 | Log.d(TAG, "ICE candidate received: " + json.toString()); 297 | 298 | try { 299 | localPeer.addIceCandidate(new IceCandidate(json.getString("id"), json.getInt("label"), 300 | json.getString("candidate"))); 301 | } catch (JSONException e) { 302 | e.printStackTrace(); 303 | } 304 | } 305 | 306 | private VideoCapturer createCameraCapturer(CameraEnumerator enumerator) { 307 | Log.d(TAG, new Object(){}.getClass().getEnclosingMethod().getName()); 308 | final String[] deviceNames = enumerator.getDeviceNames(); 309 | 310 | // First, try to find front facing camera 311 | Logging.d(TAG, "Looking for front facing cameras."); 312 | for (String deviceName : deviceNames) { 313 | if (enumerator.isFrontFacing(deviceName)) { 314 | Logging.d(TAG, "Creating front facing camera capturer."); 315 | VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null); 316 | 317 | if (videoCapturer != null) { 318 | return videoCapturer; 319 | } 320 | } 321 | } 322 | 323 | // Front facing camera not found, try something else 324 | Logging.d(TAG, "Looking for other cameras."); 325 | for (String deviceName : deviceNames) { 326 | if (!enumerator.isFrontFacing(deviceName)) { 327 | Logging.d(TAG, "Creating other camera capturer."); 328 | VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null); 329 | 330 | if (videoCapturer != null) { 331 | return videoCapturer; 332 | } 333 | } 334 | } 335 | 336 | return null; 337 | } 338 | 339 | public void getIceServers() { 340 | final String API_ENDPOINT = "https://global.xirsys.net"; 341 | 342 | Log.d(TAG, "getIceServers"); 343 | 344 | byte[] data = new byte[0]; 345 | try { 346 | data = (":").getBytes("UTF-8"); 347 | } catch (UnsupportedEncodingException e) { 348 | e.printStackTrace(); 349 | return; 350 | } 351 | Log.d(TAG, "getIceServers2"); 352 | 353 | String authToken = "Basic " + Base64.encodeToString(data, Base64.NO_WRAP); 354 | Retrofit retrofit = new Retrofit.Builder() 355 | .baseUrl(API_ENDPOINT) 356 | .addConverterFactory(GsonConverterFactory.create()) 357 | .build(); 358 | Log.d(TAG, "getIceServers3"); 359 | TurnServer turnServer = retrofit.create(TurnServer.class); 360 | Log.d(TAG, "getIceServers4"); 361 | turnServer.getIceCandidates(authToken).enqueue(new Callback() { 362 | @Override 363 | public void onResponse(@NonNull Call call, 364 | @NonNull Response response) { 365 | Log.d(TAG, "getIceServers Response"); 366 | TurnServerPojo body = response.body(); 367 | if (body != null) 368 | iceServers = body.iceServerList.iceServers; 369 | 370 | Log.d(TAG, "getIceServers iceServers=" + iceServers); 371 | 372 | for (IceServer iceServer : iceServers) { 373 | if (iceServer.credential == null) { 374 | PeerConnection.IceServer peerIceServer = PeerConnection.IceServer 375 | .builder(iceServer.url).createIceServer(); 376 | peerIceServers.add(peerIceServer); 377 | } else { 378 | PeerConnection.IceServer peerIceServer = PeerConnection.IceServer 379 | .builder(iceServer.url) 380 | .setUsername(iceServer.username) 381 | .setPassword(iceServer.credential) 382 | .createIceServer(); 383 | peerIceServers.add(peerIceServer); 384 | } 385 | } 386 | Log.d(TAG, "IceServers:\n" + iceServers.toString()); 387 | } 388 | 389 | @Override 390 | public void onFailure(@NonNull Call call, @NonNull Throwable t) { 391 | t.printStackTrace(); 392 | } 393 | }); 394 | } 395 | 396 | private void startRotationDetector() { 397 | Runnable runnable = new Runnable() { 398 | @Override 399 | public void run() { 400 | Log.d(TAG, "Rotation detector start"); 401 | display.getRealMetrics(screenMetrics); 402 | while (true) { 403 | DisplayMetrics metrics = new DisplayMetrics(); 404 | display.getRealMetrics(metrics); 405 | if (metrics.widthPixels != screenMetrics.widthPixels || 406 | metrics.heightPixels != screenMetrics.heightPixels) { 407 | Log.d(TAG, "Rotation detected\n" + "w=" + metrics.widthPixels + " h=" + 408 | metrics.heightPixels + " d=" + metrics.densityDpi); 409 | screenMetrics = metrics; 410 | if (videoCapturer != null) { 411 | try { 412 | videoCapturer.stopCapture(); 413 | } catch (Exception e) { 414 | e.printStackTrace(); 415 | } 416 | videoCapturer.startCapture(screenMetrics.widthPixels, 417 | screenMetrics.heightPixels, FRAMES_PER_SECOND); 418 | } 419 | } 420 | try { 421 | Thread.sleep(500); 422 | } catch (InterruptedException e) { 423 | Log.d(TAG, "Rotation detector exit"); 424 | Thread.interrupted(); 425 | break; 426 | } 427 | } 428 | } 429 | }; 430 | rotationDetectorThread = new Thread(runnable); 431 | rotationDetectorThread.start(); 432 | } 433 | 434 | private void stopRotationDetector() { 435 | rotationDetectorThread.interrupt(); 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /app/src/main/java/fi/iki/elonen/NanoWSD.java: -------------------------------------------------------------------------------- 1 | package fi.iki.elonen; 2 | 3 | /* 4 | * #%L 5 | * NanoHttpd-Websocket 6 | * %% 7 | * Copyright (C) 2012 - 2015 nanohttpd 8 | * %% 9 | * Redistribution and use in source and binary forms, with or without modification, 10 | * are permitted provided that the following conditions are met: 11 | * 12 | * 1. Redistributions of source code must retain the above copyright notice, this 13 | * list of conditions and the following disclaimer. 14 | * 15 | * 2. Redistributions in binary form must reproduce the above copyright notice, 16 | * this list of conditions and the following disclaimer in the documentation 17 | * and/or other materials provided with the distribution. 18 | * 19 | * 3. Neither the name of the nanohttpd nor the names of its contributors 20 | * may be used to endorse or promote products derived from this software without 21 | * specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 26 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 31 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 32 | * OF THE POSSIBILITY OF SUCH DAMAGE. 33 | * #L% 34 | */ 35 | 36 | import java.io.EOFException; 37 | import java.io.IOException; 38 | import java.io.InputStream; 39 | import java.io.OutputStream; 40 | import java.nio.charset.CharacterCodingException; 41 | import java.nio.charset.Charset; 42 | import java.security.MessageDigest; 43 | import java.security.NoSuchAlgorithmException; 44 | import java.util.Arrays; 45 | import java.util.LinkedList; 46 | import java.util.List; 47 | import java.util.Map; 48 | import java.util.logging.Level; 49 | import java.util.logging.Logger; 50 | 51 | import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode; 52 | import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseFrame; 53 | import fi.iki.elonen.NanoWSD.WebSocketFrame.OpCode; 54 | 55 | public abstract class NanoWSD extends NanoHTTPD { 56 | 57 | public static enum State { 58 | UNCONNECTED, 59 | CONNECTING, 60 | OPEN, 61 | CLOSING, 62 | CLOSED 63 | } 64 | 65 | public static abstract class WebSocket { 66 | 67 | private final InputStream in; 68 | 69 | private OutputStream out; 70 | 71 | private WebSocketFrame.OpCode continuousOpCode = null; 72 | 73 | private final List continuousFrames = new LinkedList(); 74 | 75 | private State state = State.UNCONNECTED; 76 | 77 | private final NanoHTTPD.IHTTPSession handshakeRequest; 78 | 79 | private final NanoHTTPD.Response handshakeResponse = new NanoHTTPD.Response(NanoHTTPD.Response.Status.SWITCH_PROTOCOL, null, (InputStream) null, 0) { 80 | 81 | @Override 82 | protected void send(OutputStream out) { 83 | WebSocket.this.out = out; 84 | WebSocket.this.state = State.CONNECTING; 85 | super.send(out); 86 | WebSocket.this.state = State.OPEN; 87 | WebSocket.this.onOpen(); 88 | readWebsocket(); 89 | } 90 | }; 91 | 92 | public WebSocket(NanoHTTPD.IHTTPSession handshakeRequest) { 93 | this.handshakeRequest = handshakeRequest; 94 | this.in = handshakeRequest.getInputStream(); 95 | 96 | this.handshakeResponse.addHeader(NanoWSD.HEADER_UPGRADE, NanoWSD.HEADER_UPGRADE_VALUE); 97 | this.handshakeResponse.addHeader(NanoWSD.HEADER_CONNECTION, NanoWSD.HEADER_CONNECTION_VALUE); 98 | } 99 | 100 | public boolean isOpen() { 101 | return state == State.OPEN; 102 | } 103 | 104 | protected abstract void onOpen(); 105 | 106 | protected abstract void onClose(CloseCode code, String reason, boolean initiatedByRemote); 107 | 108 | protected abstract void onMessage(WebSocketFrame message); 109 | 110 | protected abstract void onPong(WebSocketFrame pong); 111 | 112 | protected abstract void onException(IOException exception); 113 | 114 | /** 115 | * Debug method. Do not Override unless for debug purposes! 116 | * 117 | * @param frame 118 | * The received WebSocket Frame. 119 | */ 120 | protected void debugFrameReceived(WebSocketFrame frame) { 121 | } 122 | 123 | /** 124 | * Debug method. Do not Override unless for debug purposes!
125 | * This method is called before actually sending the frame. 126 | * 127 | * @param frame 128 | * The sent WebSocket Frame. 129 | */ 130 | protected void debugFrameSent(WebSocketFrame frame) { 131 | } 132 | 133 | public void close(CloseCode code, String reason, boolean initiatedByRemote) throws IOException { 134 | State oldState = this.state; 135 | this.state = State.CLOSING; 136 | if (oldState == State.OPEN) { 137 | sendFrame(new CloseFrame(code, reason)); 138 | } else { 139 | doClose(code, reason, initiatedByRemote); 140 | } 141 | } 142 | 143 | private void doClose(CloseCode code, String reason, boolean initiatedByRemote) { 144 | if (this.state == State.CLOSED) { 145 | return; 146 | } 147 | if (this.in != null) { 148 | try { 149 | this.in.close(); 150 | } catch (IOException e) { 151 | NanoWSD.LOG.log(Level.FINE, "close failed", e); 152 | } 153 | } 154 | if (this.out != null) { 155 | try { 156 | this.out.close(); 157 | } catch (IOException e) { 158 | NanoWSD.LOG.log(Level.FINE, "close failed", e); 159 | } 160 | } 161 | this.state = State.CLOSED; 162 | onClose(code, reason, initiatedByRemote); 163 | } 164 | 165 | // --------------------------------IO-------------------------------------- 166 | 167 | public NanoHTTPD.IHTTPSession getHandshakeRequest() { 168 | return this.handshakeRequest; 169 | } 170 | 171 | public NanoHTTPD.Response getHandshakeResponse() { 172 | return this.handshakeResponse; 173 | } 174 | 175 | private void handleCloseFrame(WebSocketFrame frame) throws IOException { 176 | CloseCode code = CloseCode.NormalClosure; 177 | String reason = ""; 178 | if (frame instanceof CloseFrame) { 179 | code = ((CloseFrame) frame).getCloseCode(); 180 | reason = ((CloseFrame) frame).getCloseReason(); 181 | } 182 | if (this.state == State.CLOSING) { 183 | // Answer for my requested close 184 | doClose(code, reason, false); 185 | } else { 186 | close(code, reason, true); 187 | } 188 | } 189 | 190 | private void handleFrameFragment(WebSocketFrame frame) throws IOException { 191 | if (frame.getOpCode() != OpCode.Continuation) { 192 | // First 193 | if (this.continuousOpCode != null) { 194 | throw new WebSocketException(CloseCode.ProtocolError, "Previous continuous frame sequence not completed."); 195 | } 196 | this.continuousOpCode = frame.getOpCode(); 197 | this.continuousFrames.clear(); 198 | this.continuousFrames.add(frame); 199 | } else if (frame.isFin()) { 200 | // Last 201 | if (this.continuousOpCode == null) { 202 | throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started."); 203 | } 204 | this.continuousFrames.add(frame); 205 | onMessage(new WebSocketFrame(this.continuousOpCode, this.continuousFrames)); 206 | this.continuousOpCode = null; 207 | this.continuousFrames.clear(); 208 | } else if (this.continuousOpCode == null) { 209 | // Unexpected 210 | throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started."); 211 | } else { 212 | // Intermediate 213 | this.continuousFrames.add(frame); 214 | } 215 | } 216 | 217 | private void handleWebsocketFrame(WebSocketFrame frame) throws IOException { 218 | debugFrameReceived(frame); 219 | if (frame.getOpCode() == OpCode.Close) { 220 | handleCloseFrame(frame); 221 | } else if (frame.getOpCode() == OpCode.Ping) { 222 | sendFrame(new WebSocketFrame(OpCode.Pong, true, frame.getBinaryPayload())); 223 | } else if (frame.getOpCode() == OpCode.Pong) { 224 | onPong(frame); 225 | } else if (!frame.isFin() || frame.getOpCode() == OpCode.Continuation) { 226 | handleFrameFragment(frame); 227 | } else if (this.continuousOpCode != null) { 228 | throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence not completed."); 229 | } else if (frame.getOpCode() == OpCode.Text || frame.getOpCode() == OpCode.Binary) { 230 | onMessage(frame); 231 | } else { 232 | throw new WebSocketException(CloseCode.ProtocolError, "Non control or continuous frame expected."); 233 | } 234 | } 235 | 236 | // --------------------------------Close----------------------------------- 237 | 238 | public void ping(byte[] payload) throws IOException { 239 | sendFrame(new WebSocketFrame(OpCode.Ping, true, payload)); 240 | } 241 | 242 | // --------------------------------Public 243 | // Facade--------------------------- 244 | 245 | private void readWebsocket() { 246 | try { 247 | while (this.state == State.OPEN) { 248 | handleWebsocketFrame(WebSocketFrame.read(this.in)); 249 | } 250 | } catch (CharacterCodingException e) { 251 | onException(e); 252 | doClose(CloseCode.InvalidFramePayloadData, e.toString(), false); 253 | } catch (IOException e) { 254 | onException(e); 255 | if (e instanceof WebSocketException) { 256 | doClose(((WebSocketException) e).getCode(), ((WebSocketException) e).getReason(), false); 257 | } 258 | } finally { 259 | doClose(CloseCode.InternalServerError, "Handler terminated without closing the connection.", false); 260 | } 261 | } 262 | 263 | public void send(byte[] payload) throws IOException { 264 | sendFrame(new WebSocketFrame(OpCode.Binary, true, payload)); 265 | } 266 | 267 | public void send(String payload) throws IOException { 268 | sendFrame(new WebSocketFrame(OpCode.Text, true, payload)); 269 | } 270 | 271 | public synchronized void sendFrame(WebSocketFrame frame) throws IOException { 272 | debugFrameSent(frame); 273 | frame.write(this.out); 274 | } 275 | } 276 | 277 | public static class WebSocketException extends IOException { 278 | 279 | private static final long serialVersionUID = 1L; 280 | 281 | private final CloseCode code; 282 | 283 | private final String reason; 284 | 285 | public WebSocketException(CloseCode code, String reason) { 286 | this(code, reason, null); 287 | } 288 | 289 | public WebSocketException(CloseCode code, String reason, Exception cause) { 290 | super(code + ": " + reason, cause); 291 | this.code = code; 292 | this.reason = reason; 293 | } 294 | 295 | public WebSocketException(Exception cause) { 296 | this(CloseCode.InternalServerError, cause.toString(), cause); 297 | } 298 | 299 | public CloseCode getCode() { 300 | return this.code; 301 | } 302 | 303 | public String getReason() { 304 | return this.reason; 305 | } 306 | } 307 | 308 | public static class WebSocketFrame { 309 | 310 | public static enum CloseCode { 311 | NormalClosure(1000), 312 | GoingAway(1001), 313 | ProtocolError(1002), 314 | UnsupportedData(1003), 315 | NoStatusRcvd(1005), 316 | AbnormalClosure(1006), 317 | InvalidFramePayloadData(1007), 318 | PolicyViolation(1008), 319 | MessageTooBig(1009), 320 | MandatoryExt(1010), 321 | InternalServerError(1011), 322 | TLSHandshake(1015); 323 | 324 | public static CloseCode find(int value) { 325 | for (CloseCode code : values()) { 326 | if (code.getValue() == value) { 327 | return code; 328 | } 329 | } 330 | return null; 331 | } 332 | 333 | private final int code; 334 | 335 | private CloseCode(int code) { 336 | this.code = code; 337 | } 338 | 339 | public int getValue() { 340 | return this.code; 341 | } 342 | } 343 | 344 | public static class CloseFrame extends WebSocketFrame { 345 | 346 | private static byte[] generatePayload(CloseCode code, String closeReason) throws CharacterCodingException { 347 | if (code != null) { 348 | byte[] reasonBytes = text2Binary(closeReason); 349 | byte[] payload = new byte[reasonBytes.length + 2]; 350 | payload[0] = (byte) (code.getValue() >> 8 & 0xFF); 351 | payload[1] = (byte) (code.getValue() & 0xFF); 352 | System.arraycopy(reasonBytes, 0, payload, 2, reasonBytes.length); 353 | return payload; 354 | } else { 355 | return new byte[0]; 356 | } 357 | } 358 | 359 | private CloseCode _closeCode; 360 | 361 | private String _closeReason; 362 | 363 | public CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException { 364 | super(OpCode.Close, true, generatePayload(code, closeReason)); 365 | } 366 | 367 | private CloseFrame(WebSocketFrame wrap) throws CharacterCodingException { 368 | super(wrap); 369 | assert wrap.getOpCode() == OpCode.Close; 370 | if (wrap.getBinaryPayload().length >= 2) { 371 | this._closeCode = CloseCode.find((wrap.getBinaryPayload()[0] & 0xFF) << 8 | wrap.getBinaryPayload()[1] & 0xFF); 372 | this._closeReason = binary2Text(getBinaryPayload(), 2, getBinaryPayload().length - 2); 373 | } 374 | } 375 | 376 | public CloseCode getCloseCode() { 377 | return this._closeCode; 378 | } 379 | 380 | public String getCloseReason() { 381 | return this._closeReason; 382 | } 383 | } 384 | 385 | public static enum OpCode { 386 | Continuation(0), 387 | Text(1), 388 | Binary(2), 389 | Close(8), 390 | Ping(9), 391 | Pong(10); 392 | 393 | public static OpCode find(byte value) { 394 | for (OpCode opcode : values()) { 395 | if (opcode.getValue() == value) { 396 | return opcode; 397 | } 398 | } 399 | return null; 400 | } 401 | 402 | private final byte code; 403 | 404 | private OpCode(int code) { 405 | this.code = (byte) code; 406 | } 407 | 408 | public byte getValue() { 409 | return this.code; 410 | } 411 | 412 | public boolean isControlFrame() { 413 | return this == Close || this == Ping || this == Pong; 414 | } 415 | } 416 | 417 | public static final Charset TEXT_CHARSET = Charset.forName("UTF-8"); 418 | 419 | public static String binary2Text(byte[] payload) throws CharacterCodingException { 420 | return new String(payload, WebSocketFrame.TEXT_CHARSET); 421 | } 422 | 423 | public static String binary2Text(byte[] payload, int offset, int length) throws CharacterCodingException { 424 | return new String(payload, offset, length, WebSocketFrame.TEXT_CHARSET); 425 | } 426 | 427 | private static int checkedRead(int read) throws IOException { 428 | if (read < 0) { 429 | throw new EOFException(); 430 | } 431 | return read; 432 | } 433 | 434 | public static WebSocketFrame read(InputStream in) throws IOException { 435 | byte head = (byte) checkedRead(in.read()); 436 | boolean fin = (head & 0x80) != 0; 437 | OpCode opCode = OpCode.find((byte) (head & 0x0F)); 438 | if ((head & 0x70) != 0) { 439 | throw new WebSocketException(CloseCode.ProtocolError, "The reserved bits (" + Integer.toBinaryString(head & 0x70) + ") must be 0."); 440 | } 441 | if (opCode == null) { 442 | throw new WebSocketException(CloseCode.ProtocolError, "Received frame with reserved/unknown opcode " + (head & 0x0F) + "."); 443 | } else if (opCode.isControlFrame() && !fin) { 444 | throw new WebSocketException(CloseCode.ProtocolError, "Fragmented control frame."); 445 | } 446 | 447 | WebSocketFrame frame = new WebSocketFrame(opCode, fin); 448 | frame.readPayloadInfo(in); 449 | frame.readPayload(in); 450 | if (frame.getOpCode() == OpCode.Close) { 451 | return new CloseFrame(frame); 452 | } else { 453 | return frame; 454 | } 455 | } 456 | 457 | public static byte[] text2Binary(String payload) throws CharacterCodingException { 458 | return payload.getBytes(WebSocketFrame.TEXT_CHARSET); 459 | } 460 | 461 | private OpCode opCode; 462 | 463 | private boolean fin; 464 | 465 | private byte[] maskingKey; 466 | 467 | private byte[] payload; 468 | 469 | // --------------------------------GETTERS--------------------------------- 470 | 471 | private transient int _payloadLength; 472 | 473 | private transient String _payloadString; 474 | 475 | private WebSocketFrame(OpCode opCode, boolean fin) { 476 | setOpCode(opCode); 477 | setFin(fin); 478 | } 479 | 480 | public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload) { 481 | this(opCode, fin, payload, null); 482 | } 483 | 484 | public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload, byte[] maskingKey) { 485 | this(opCode, fin); 486 | setMaskingKey(maskingKey); 487 | setBinaryPayload(payload); 488 | } 489 | 490 | public WebSocketFrame(OpCode opCode, boolean fin, String payload) throws CharacterCodingException { 491 | this(opCode, fin, payload, null); 492 | } 493 | 494 | public WebSocketFrame(OpCode opCode, boolean fin, String payload, byte[] maskingKey) throws CharacterCodingException { 495 | this(opCode, fin); 496 | setMaskingKey(maskingKey); 497 | setTextPayload(payload); 498 | } 499 | 500 | public WebSocketFrame(OpCode opCode, List fragments) throws WebSocketException { 501 | setOpCode(opCode); 502 | setFin(true); 503 | 504 | long _payloadLength = 0; 505 | for (WebSocketFrame inter : fragments) { 506 | _payloadLength += inter.getBinaryPayload().length; 507 | } 508 | if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) { 509 | throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded."); 510 | } 511 | this._payloadLength = (int) _payloadLength; 512 | byte[] payload = new byte[this._payloadLength]; 513 | int offset = 0; 514 | for (WebSocketFrame inter : fragments) { 515 | System.arraycopy(inter.getBinaryPayload(), 0, payload, offset, inter.getBinaryPayload().length); 516 | offset += inter.getBinaryPayload().length; 517 | } 518 | setBinaryPayload(payload); 519 | } 520 | 521 | public WebSocketFrame(WebSocketFrame clone) { 522 | setOpCode(clone.getOpCode()); 523 | setFin(clone.isFin()); 524 | setBinaryPayload(clone.getBinaryPayload()); 525 | setMaskingKey(clone.getMaskingKey()); 526 | } 527 | 528 | public byte[] getBinaryPayload() { 529 | return this.payload; 530 | } 531 | 532 | public byte[] getMaskingKey() { 533 | return this.maskingKey; 534 | } 535 | 536 | public OpCode getOpCode() { 537 | return this.opCode; 538 | } 539 | 540 | // --------------------------------SERIALIZATION--------------------------- 541 | 542 | public String getTextPayload() { 543 | if (this._payloadString == null) { 544 | try { 545 | this._payloadString = binary2Text(getBinaryPayload()); 546 | } catch (CharacterCodingException e) { 547 | throw new RuntimeException("Undetected CharacterCodingException", e); 548 | } 549 | } 550 | return this._payloadString; 551 | } 552 | 553 | public boolean isFin() { 554 | return this.fin; 555 | } 556 | 557 | public boolean isMasked() { 558 | return this.maskingKey != null && this.maskingKey.length == 4; 559 | } 560 | 561 | private String payloadToString() { 562 | if (this.payload == null) { 563 | return "null"; 564 | } else { 565 | final StringBuilder sb = new StringBuilder(); 566 | sb.append('[').append(this.payload.length).append("b] "); 567 | if (getOpCode() == OpCode.Text) { 568 | String text = getTextPayload(); 569 | if (text.length() > 100) { 570 | sb.append(text.substring(0, 100)).append("..."); 571 | } else { 572 | sb.append(text); 573 | } 574 | } else { 575 | sb.append("0x"); 576 | for (int i = 0; i < Math.min(this.payload.length, 50); ++i) { 577 | sb.append(Integer.toHexString(this.payload[i] & 0xFF)); 578 | } 579 | if (this.payload.length > 50) { 580 | sb.append("..."); 581 | } 582 | } 583 | return sb.toString(); 584 | } 585 | } 586 | 587 | private void readPayload(InputStream in) throws IOException { 588 | this.payload = new byte[this._payloadLength]; 589 | int read = 0; 590 | while (read < this._payloadLength) { 591 | read += checkedRead(in.read(this.payload, read, this._payloadLength - read)); 592 | } 593 | 594 | if (isMasked()) { 595 | for (int i = 0; i < this.payload.length; i++) { 596 | this.payload[i] ^= this.maskingKey[i % 4]; 597 | } 598 | } 599 | 600 | // Test for Unicode errors 601 | if (getOpCode() == OpCode.Text) { 602 | this._payloadString = binary2Text(getBinaryPayload()); 603 | } 604 | } 605 | 606 | // --------------------------------ENCODING-------------------------------- 607 | 608 | private void readPayloadInfo(InputStream in) throws IOException { 609 | byte b = (byte) checkedRead(in.read()); 610 | boolean masked = (b & 0x80) != 0; 611 | 612 | this._payloadLength = (byte) (0x7F & b); 613 | if (this._payloadLength == 126) { 614 | // checkedRead must return int for this to work 615 | this._payloadLength = (checkedRead(in.read()) << 8 | checkedRead(in.read())) & 0xFFFF; 616 | if (this._payloadLength < 126) { 617 | throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 2byte length. (not using minimal length encoding)"); 618 | } 619 | } else if (this._payloadLength == 127) { 620 | long _payloadLength = 621 | (long) checkedRead(in.read()) << 56 | (long) checkedRead(in.read()) << 48 | (long) checkedRead(in.read()) << 40 | (long) checkedRead(in.read()) << 32 622 | | checkedRead(in.read()) << 24 | checkedRead(in.read()) << 16 | checkedRead(in.read()) << 8 | checkedRead(in.read()); 623 | if (_payloadLength < 65536) { 624 | throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 4byte length. (not using minimal length encoding)"); 625 | } 626 | if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) { 627 | throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded."); 628 | } 629 | this._payloadLength = (int) _payloadLength; 630 | } 631 | 632 | if (this.opCode.isControlFrame()) { 633 | if (this._payloadLength > 125) { 634 | throw new WebSocketException(CloseCode.ProtocolError, "Control frame with payload length > 125 bytes."); 635 | } 636 | if (this.opCode == OpCode.Close && this._payloadLength == 1) { 637 | throw new WebSocketException(CloseCode.ProtocolError, "Received close frame with payload len 1."); 638 | } 639 | } 640 | 641 | if (masked) { 642 | this.maskingKey = new byte[4]; 643 | int read = 0; 644 | while (read < this.maskingKey.length) { 645 | read += checkedRead(in.read(this.maskingKey, read, this.maskingKey.length - read)); 646 | } 647 | } 648 | } 649 | 650 | public void setBinaryPayload(byte[] payload) { 651 | this.payload = payload; 652 | this._payloadLength = payload.length; 653 | this._payloadString = null; 654 | } 655 | 656 | public void setFin(boolean fin) { 657 | this.fin = fin; 658 | } 659 | 660 | public void setMaskingKey(byte[] maskingKey) { 661 | if (maskingKey != null && maskingKey.length != 4) { 662 | throw new IllegalArgumentException("MaskingKey " + Arrays.toString(maskingKey) + " hasn't length 4"); 663 | } 664 | this.maskingKey = maskingKey; 665 | } 666 | 667 | public void setOpCode(OpCode opcode) { 668 | this.opCode = opcode; 669 | } 670 | 671 | public void setTextPayload(String payload) throws CharacterCodingException { 672 | this.payload = text2Binary(payload); 673 | this._payloadLength = payload.length(); 674 | this._payloadString = payload; 675 | } 676 | 677 | // --------------------------------CONSTANTS------------------------------- 678 | 679 | public void setUnmasked() { 680 | setMaskingKey(null); 681 | } 682 | 683 | @Override 684 | public String toString() { 685 | final StringBuilder sb = new StringBuilder("WS["); 686 | sb.append(getOpCode()); 687 | sb.append(", ").append(isFin() ? "fin" : "inter"); 688 | sb.append(", ").append(isMasked() ? "masked" : "unmasked"); 689 | sb.append(", ").append(payloadToString()); 690 | sb.append(']'); 691 | return sb.toString(); 692 | } 693 | 694 | // ------------------------------------------------------------------------ 695 | 696 | public void write(OutputStream out) throws IOException { 697 | byte header = 0; 698 | if (this.fin) { 699 | header |= 0x80; 700 | } 701 | header |= this.opCode.getValue() & 0x0F; 702 | out.write(header); 703 | 704 | this._payloadLength = getBinaryPayload().length; 705 | if (this._payloadLength <= 125) { 706 | out.write(isMasked() ? 0x80 | (byte) this._payloadLength : (byte) this._payloadLength); 707 | } else if (this._payloadLength <= 0xFFFF) { 708 | out.write(isMasked() ? 0xFE : 126); 709 | out.write(this._payloadLength >>> 8); 710 | out.write(this._payloadLength); 711 | } else { 712 | out.write(isMasked() ? 0xFF : 127); 713 | out.write(this._payloadLength >>> 56 & 0); // integer only 714 | // contains 715 | // 31 bit 716 | out.write(this._payloadLength >>> 48 & 0); 717 | out.write(this._payloadLength >>> 40 & 0); 718 | out.write(this._payloadLength >>> 32 & 0); 719 | out.write(this._payloadLength >>> 24); 720 | out.write(this._payloadLength >>> 16); 721 | out.write(this._payloadLength >>> 8); 722 | out.write(this._payloadLength); 723 | } 724 | 725 | if (isMasked()) { 726 | out.write(this.maskingKey); 727 | for (int i = 0; i < this._payloadLength; i++) { 728 | out.write(getBinaryPayload()[i] ^ this.maskingKey[i % 4]); 729 | } 730 | } else { 731 | out.write(getBinaryPayload()); 732 | } 733 | out.flush(); 734 | } 735 | } 736 | 737 | /** 738 | * logger to log to. 739 | */ 740 | private static final Logger LOG = Logger.getLogger(NanoWSD.class.getName()); 741 | 742 | public static final String HEADER_UPGRADE = "upgrade"; 743 | 744 | public static final String HEADER_UPGRADE_VALUE = "websocket"; 745 | 746 | public static final String HEADER_CONNECTION = "connection"; 747 | 748 | public static final String HEADER_CONNECTION_VALUE = "Upgrade"; 749 | 750 | public static final String HEADER_WEBSOCKET_VERSION = "sec-websocket-version"; 751 | 752 | public static final String HEADER_WEBSOCKET_VERSION_VALUE = "13"; 753 | 754 | public static final String HEADER_WEBSOCKET_KEY = "sec-websocket-key"; 755 | 756 | public static final String HEADER_WEBSOCKET_ACCEPT = "sec-websocket-accept"; 757 | 758 | public static final String HEADER_WEBSOCKET_PROTOCOL = "sec-websocket-protocol"; 759 | 760 | private final static String WEBSOCKET_KEY_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 761 | 762 | private final static char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); 763 | 764 | /** 765 | * Translates the specified byte array into Base64 string. 766 | *

767 | * Android has android.util.Base64, sun has sun.misc.Base64Encoder, Java 8 768 | * hast java.util.Base64, I have this from stackoverflow: 769 | * http://stackoverflow.com/a/4265472 770 | *

771 | * 772 | * @param buf 773 | * the byte array (not null) 774 | * @return the translated Base64 string (not null) 775 | */ 776 | private static String encodeBase64(byte[] buf) { 777 | int size = buf.length; 778 | char[] ar = new char[(size + 2) / 3 * 4]; 779 | int a = 0; 780 | int i = 0; 781 | while (i < size) { 782 | byte b0 = buf[i++]; 783 | byte b1 = i < size ? buf[i++] : 0; 784 | byte b2 = i < size ? buf[i++] : 0; 785 | 786 | int mask = 0x3F; 787 | ar[a++] = NanoWSD.ALPHABET[b0 >> 2 & mask]; 788 | ar[a++] = NanoWSD.ALPHABET[(b0 << 4 | (b1 & 0xFF) >> 4) & mask]; 789 | ar[a++] = NanoWSD.ALPHABET[(b1 << 2 | (b2 & 0xFF) >> 6) & mask]; 790 | ar[a++] = NanoWSD.ALPHABET[b2 & mask]; 791 | } 792 | switch (size % 3) { 793 | case 1: 794 | ar[--a] = '='; 795 | case 2: 796 | ar[--a] = '='; 797 | } 798 | return new String(ar); 799 | } 800 | 801 | public static String makeAcceptKey(String key) throws NoSuchAlgorithmException { 802 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 803 | String text = key + NanoWSD.WEBSOCKET_KEY_MAGIC; 804 | md.update(text.getBytes(), 0, text.length()); 805 | byte[] sha1hash = md.digest(); 806 | return encodeBase64(sha1hash); 807 | } 808 | 809 | public NanoWSD(int port) { 810 | super(port); 811 | } 812 | 813 | public NanoWSD(String hostname, int port) { 814 | super(hostname, port); 815 | } 816 | 817 | private boolean isWebSocketConnectionHeader(Map headers) { 818 | String connection = headers.get(NanoWSD.HEADER_CONNECTION); 819 | return connection != null && connection.toLowerCase().contains(NanoWSD.HEADER_CONNECTION_VALUE.toLowerCase()); 820 | } 821 | 822 | protected boolean isWebsocketRequested(IHTTPSession session) { 823 | Map headers = session.getHeaders(); 824 | String upgrade = headers.get(NanoWSD.HEADER_UPGRADE); 825 | boolean isCorrectConnection = isWebSocketConnectionHeader(headers); 826 | boolean isUpgrade = NanoWSD.HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade); 827 | return isUpgrade && isCorrectConnection; 828 | } 829 | 830 | // --------------------------------Listener-------------------------------- 831 | 832 | protected abstract WebSocket openWebSocket(IHTTPSession handshake); 833 | 834 | @Override 835 | public Response serve(final IHTTPSession session) { 836 | Map headers = session.getHeaders(); 837 | if (isWebsocketRequested(session)) { 838 | if (!NanoWSD.HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION))) { 839 | return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, 840 | "Invalid Websocket-Version " + headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION)); 841 | } 842 | 843 | if (!headers.containsKey(NanoWSD.HEADER_WEBSOCKET_KEY)) { 844 | return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Missing Websocket-Key"); 845 | } 846 | 847 | WebSocket webSocket = openWebSocket(session); 848 | Response handshakeResponse = webSocket.getHandshakeResponse(); 849 | try { 850 | handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(NanoWSD.HEADER_WEBSOCKET_KEY))); 851 | } catch (NoSuchAlgorithmException e) { 852 | return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, 853 | "The SHA-1 Algorithm required for websockets is not available on the server."); 854 | } 855 | 856 | if (headers.containsKey(NanoWSD.HEADER_WEBSOCKET_PROTOCOL)) { 857 | handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_PROTOCOL, headers.get(NanoWSD.HEADER_WEBSOCKET_PROTOCOL).split(",")[0]); 858 | } 859 | 860 | return handshakeResponse; 861 | } else { 862 | return serveHttp(session); 863 | } 864 | } 865 | 866 | protected Response serveHttp(final IHTTPSession session) { 867 | return super.serve(session); 868 | } 869 | 870 | /** 871 | * not all websockets implementations accept gzip compression. 872 | */ 873 | @Override 874 | protected boolean useGzipWhenAccepted(Response r) { 875 | return false; 876 | } 877 | } 878 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_stat_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/drawable-hdpi/ic_stat_name.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_stat_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/drawable-mdpi/ic_stat_name.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_stat_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/drawable-xhdpi/ic_stat_name.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_stat_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/drawable-xxhdpi/ic_stat_name.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_stat_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_button_off.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_button_on.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/wifi_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/drawable/wifi_icon.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_admin.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 13 | 14 | 19 | 20 | 24 | 25 | 31 | 32 | 33 | 34 | 44 | 45 | 55 | 56 | 61 | 67 | 68 | 75 | 76 | 83 | 84 | 85 | 86 | 98 | 99 | 110 | 111 | 112 | 113 | 114 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-ru-rRU/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebScreen 4 | Перейдите по следующей ссылке в браузере 5 | Отключить дистанционное управление 6 | Включить дистанционное управление 7 | Начать совместное использование экрана 8 | Остановить совместное использование экрана 9 | Нет Wi-Fi соединения 10 | Настройки 11 | Общие 12 | Порт 13 | Порт сервера для входящих соединений. Допустимые значения: 1025-65535.\nПо умолчанию: 8080. 14 | ca-app-pub-3940256099942544/6300978111 15 | ca-app-pub-6312357173588580/8818719441 16 | Предоставить разрешение администратора для блокировки экрана 17 | Ошибка: порт %d уже используется. 18 | -------------------------------------------------------------------------------- /app/src/main/res/values-uk-rUA/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebScreen 4 | Відкрийте наступне посилання у браузері 5 | Увімкнути дистанційне керування 6 | Вимкнути дистанційне керування 7 | Почати спільний доступ до екрана 8 | Зупинити спільний доступ до екрана 9 | Немає Wi-Fi з’єднання 10 | Налаштування 11 | Загальні 12 | Порт 13 | Порт сервера для вхідних з\'єднань. Дозволені значення: 1025-65535.\nЗа замовчуванням: 8080. 14 | ca-app-pub-3940256099942544/6300978111 15 | ca-app-pub-6312357173588580/8818719441 16 | Надайте дозвіл адміністратора для блокування екрану 17 | Помилка: порт %d вже використовується. 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #5BA4ED 4 | #2D63A8 5 | #5DC6D7 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | WebScreen 3 | Visit following link in browser 4 | Enable remote control 5 | Disable remote control 6 | Start screen sharing 7 | Stop screen sharing 8 | No Wi-Fi connection 9 | Settings 10 | General 11 | Port 12 | Server port for incoming connections. Allowed values: 1025-65535.\nDefault: 8080. 13 | ca-app-pub-3940256099942544/6300978111 14 | ca-app-pub-6312357173588580/8818719441 15 | Provide admin permission for screen lock 16 | Error: the port %d is already in use. 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 15 | 16 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/xml/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/mouse_accessibility_service_config.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/xml/policies.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/xml/root_preferences.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 12 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/test/java/com/bbogush/web_screen/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.bbogush.web_screen; 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() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath "com.android.tools.build:gradle:4.0.0-beta05" 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | allprojects { 23 | tasks.withType(JavaCompile) { 24 | options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun May 03 14:13:28 EEST 2020 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 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /img/main_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 52 | 55 | 56 | 60 | 65 | Web 77 | 78 | -------------------------------------------------------------------------------- /img/web_hi_res_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbogush/web_screen/8955fd6141312b4b1cf442c0571095acecae16cb/img/web_hi_res_512.png -------------------------------------------------------------------------------- /scripts/chrome_unsec_webrtc.sh: -------------------------------------------------------------------------------- 1 | google-chrome --unsafely-treat-insecure-origin-as-secure="http://192.168.2.102:8080" --user-data-dir=/tmp/ --autoplay-policy=no-user-gesture-required 2 | -------------------------------------------------------------------------------- /scripts/gen_https_key.sh: -------------------------------------------------------------------------------- 1 | keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.bks -storepass password -validity 9999 -keysize 2048 -ext SAN=DNS:localhost,IP:127.0.0.1 -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath /usr/share/java/bcprov-jdk16-1.46.jar 2 | keytool -providerpath /usr/share/java/bcprov-jdk16-1.46.jar -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider -keystore keystore.bks -storepass password -storetype BKS -list 3 | -------------------------------------------------------------------------------- /scripts/http_forward.sh: -------------------------------------------------------------------------------- 1 | adb forward tcp:8080 tcp:8080 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "web_screen" --------------------------------------------------------------------------------