├── .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 |
6 |
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 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
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 |
17 |
18 |
19 |
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"
--------------------------------------------------------------------------------