├── .gitignore ├── README.md ├── agent ├── .gitignore ├── api │ ├── build.gradle │ └── src │ │ └── main │ │ ├── java │ │ └── me │ │ │ └── jiahuan │ │ │ └── openrc │ │ │ └── agent │ │ │ ├── ApiApplication.java │ │ │ ├── ApiRoutes.java │ │ │ ├── ApplicationExceptionHandler.java │ │ │ ├── config │ │ │ ├── WebConfig.java │ │ │ └── WebSocketConfig.java │ │ │ ├── controller │ │ │ ├── ClientWebSocketController.java │ │ │ ├── DeviceController.java │ │ │ └── DeviceWebSocketController.java │ │ │ ├── pojo │ │ │ ├── DeviceInfo.java │ │ │ ├── Size.java │ │ │ └── WebSocketMessage.java │ │ │ └── runtime │ │ │ └── Runtime.java │ │ └── resources │ │ └── application.properties ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── command.md ├── customer ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── Decoder.js │ ├── Player.js │ ├── YUVCanvas.js │ ├── avc.wasm │ ├── favicon.ico │ ├── index.html │ └── stream.js └── src │ ├── App.vue │ ├── assets │ └── logo.png │ ├── components │ └── H264Player.vue │ ├── main.js │ ├── plugins │ ├── axios.js │ └── element.js │ ├── router │ └── index.js │ ├── store │ └── index.js │ └── views │ ├── DeviceDetail.vue │ └── Devices.vue ├── device ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── me │ │ │ └── jiahuan │ │ │ └── openrc │ │ │ └── device │ │ │ ├── App.kt │ │ │ ├── AppConstants.kt │ │ │ ├── background │ │ │ ├── Background.kt │ │ │ ├── communication │ │ │ │ └── BackgroundSocketService.kt │ │ │ ├── connection │ │ │ │ ├── AppWebSocketClient.kt │ │ │ │ ├── AppWebSocketServer.kt │ │ │ │ ├── Connection.kt │ │ │ │ ├── WebSocketClientConnection.kt │ │ │ │ └── WebSocketServerConnection.kt │ │ │ ├── controller │ │ │ │ └── Controller.kt │ │ │ ├── device │ │ │ │ ├── Device.kt │ │ │ │ ├── DeviceInfo.kt │ │ │ │ ├── DisplayInfo.kt │ │ │ │ ├── Pointer.java │ │ │ │ ├── PointersState.java │ │ │ │ ├── ScreenInfo.kt │ │ │ │ └── SurfaceControl.kt │ │ │ ├── encoder │ │ │ │ ├── ScreenEncoder.kt │ │ │ │ └── TestScreenRecorder.kt │ │ │ ├── manager │ │ │ │ ├── DisplayManager.kt │ │ │ │ ├── InputManager.kt │ │ │ │ └── ServiceManager.kt │ │ │ ├── model │ │ │ │ ├── Point.kt │ │ │ │ ├── PointF.kt │ │ │ │ └── Size.kt │ │ │ └── net │ │ │ │ └── model │ │ │ │ ├── WebSocketMessage.kt │ │ │ │ └── input │ │ │ │ ├── InputEventAction.kt │ │ │ │ ├── InputEventData.kt │ │ │ │ └── InputEventDescription.kt │ │ │ ├── foreground │ │ │ ├── activity │ │ │ │ └── MainActivity.kt │ │ │ ├── base │ │ │ │ ├── BaseActivity.kt │ │ │ │ ├── BaseFragment.kt │ │ │ │ └── BaseViewModel.kt │ │ │ ├── data │ │ │ │ └── SettingPreferenceDataStore.kt │ │ │ ├── event │ │ │ │ └── AppEventCenter.kt │ │ │ ├── fragment │ │ │ │ ├── HomeFragment.kt │ │ │ │ └── SettingFragment.kt │ │ │ ├── model │ │ │ │ ├── StatusInfo.kt │ │ │ │ └── StatusInfoSummary.kt │ │ │ ├── service │ │ │ │ └── ForegroundSocketService.kt │ │ │ ├── utils │ │ │ │ ├── DeviceUtils.kt │ │ │ │ └── ShellUtils.kt │ │ │ ├── view │ │ │ │ └── EnableSTFBackgroundServiceAlertDialog.kt │ │ │ └── viewmodel │ │ │ │ └── HomeFragmentViewModel.kt │ │ │ ├── model │ │ │ └── Setting.kt │ │ │ └── utils │ │ │ └── FileUtils.kt │ │ └── res │ │ ├── drawable │ │ ├── drawable_icon_done.xml │ │ └── drawable_icon_error.xml │ │ ├── layout │ │ ├── layout_activity_main.xml │ │ └── layout_fragment_home.xml │ │ ├── menu │ │ └── menu_navigation.xml │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-sw360dp-v13 │ │ └── values-preference.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── layout_fragment_setting.xml │ │ └── network_security_config.xml ├── background_run.sh ├── build.gradle ├── foreground_run.sh ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystore │ ├── debug.keystore │ └── release.keystore ├── openrc_device_settings.json └── settings.gradle └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # open-rc 2 | 3 | 基于 Scrcpy 的 Android 远程方案 4 | 5 | 6 | 7 | ### 名词解释 8 | 9 | - customer 10 | 11 | 控制端,可以是多种形式的,如 web、chrome 插件、App 等。 12 | 13 | - device 14 | 15 | 被控制设备,Android 设备。需要安装 device 目录下的被控端的 App。 16 | 17 | - agent 18 | 19 | 管理被控设备的服务,转发控制指令以及设备桌面数据。 20 | -------------------------------------------------------------------------------- /agent/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | -------------------------------------------------------------------------------- /agent/api/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation("org.springframework.boot:spring-boot-starter-web") 3 | implementation("org.springframework.boot:spring-boot-starter-websocket") 4 | implementation("org.springframework.boot:spring-boot-starter-validation") 5 | implementation("org.springframework.boot:spring-boot-starter-logging") 6 | implementation("com.google.code.gson:gson") 7 | } -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/ApiApplication.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ApiApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(ApiApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/ApiRoutes.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent; 2 | 3 | public class ApiRoutes { 4 | public static final String API_DEVICES = "/device/list"; 5 | public static final String API_DEVICE = "/device/{deviceId}"; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/ApplicationExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent; 2 | 3 | import org.springframework.web.bind.annotation.ControllerAdvice; 4 | import org.springframework.web.bind.annotation.ExceptionHandler; 5 | import org.springframework.web.bind.annotation.ResponseBody; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | 10 | @ControllerAdvice 11 | public class ApplicationExceptionHandler { 12 | 13 | /** 14 | * 捕获所有异常 15 | */ 16 | @ExceptionHandler(Throwable.class) 17 | @ResponseBody 18 | public String handleAllException(HttpServletRequest request, 19 | HttpServletResponse response, 20 | Throwable error) { 21 | return ""; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.cors.CorsConfiguration; 6 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 7 | import org.springframework.web.filter.CorsFilter; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | 10 | @Configuration 11 | public class WebConfig implements WebMvcConfigurer { 12 | @Bean 13 | public CorsFilter corsFilter() { 14 | CorsConfiguration config = new CorsConfiguration(); 15 | config.addAllowedOrigin("*"); 16 | config.setAllowCredentials(true); 17 | config.addAllowedMethod("*"); 18 | config.addAllowedHeader("*"); 19 | UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); 20 | configSource.registerCorsConfiguration("/**", config); 21 | return new CorsFilter(configSource); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.server.standard.ServerEndpointExporter; 6 | 7 | @Configuration 8 | public class WebSocketConfig { 9 | @Bean 10 | public ServerEndpointExporter serverEndpointExporter(){ 11 | return new ServerEndpointExporter(); 12 | } 13 | } -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/controller/ClientWebSocketController.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent.controller; 2 | 3 | import me.jiahuan.openrc.agent.runtime.Runtime; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import javax.websocket.*; 9 | import javax.websocket.server.ServerEndpoint; 10 | import java.io.IOException; 11 | import java.util.List; 12 | 13 | @RestController 14 | @ServerEndpoint("/ws/client") 15 | public class ClientWebSocketController { 16 | 17 | private final Logger logger = LoggerFactory.getLogger(ClientWebSocketController.class); 18 | 19 | private String deviceId; 20 | 21 | @OnOpen 22 | public void onOpen(Session session) { 23 | logger.info("client onOpen"); 24 | logger.info(session.getRequestParameterMap().toString()); 25 | List deviceIds = session.getRequestParameterMap().get("deviceId"); 26 | if (deviceIds != null && deviceIds.size() > 0) { 27 | deviceId = deviceIds.get(0); 28 | if (deviceId != null) { 29 | Runtime.clientSessionHashMap.put(deviceId, session); 30 | } 31 | } 32 | } 33 | 34 | @OnMessage 35 | public void onMessage(Session session, String message) { 36 | logger.info("client onMessage message = " + message); 37 | Session deviceSession = Runtime.deviceSessionHashMap.get(deviceId); 38 | if(deviceSession == null) { 39 | return; 40 | } 41 | try { 42 | deviceSession.getBasicRemote().sendText(message); 43 | } catch (IOException e) { 44 | e.printStackTrace(); 45 | } 46 | } 47 | 48 | @OnClose 49 | public void onClose(Session session) { 50 | logger.info("client onClose"); 51 | Runtime.clientSessionHashMap.remove(deviceId); 52 | } 53 | 54 | @OnError 55 | public void onError(Session session, Throwable error) { 56 | logger.info("client onError"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/controller/DeviceController.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent.controller; 2 | 3 | import me.jiahuan.openrc.agent.ApiRoutes; 4 | import me.jiahuan.openrc.agent.runtime.Runtime; 5 | import me.jiahuan.openrc.agent.pojo.DeviceInfo; 6 | import org.springframework.validation.annotation.Validated; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | @RestController 15 | @Validated 16 | public class DeviceController { 17 | @GetMapping(ApiRoutes.API_DEVICES) 18 | public List query() { 19 | return Runtime.deviceList; 20 | } 21 | 22 | @GetMapping(ApiRoutes.API_DEVICE) 23 | public Optional query(@PathVariable("deviceId") String deviceId) { 24 | return Runtime.deviceList.stream().filter((deviceInfo -> deviceInfo.getDeviceId().equals(deviceId))).findFirst(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/controller/DeviceWebSocketController.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent.controller; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonSyntaxException; 5 | import me.jiahuan.openrc.agent.runtime.Runtime; 6 | import me.jiahuan.openrc.agent.pojo.DeviceInfo; 7 | import me.jiahuan.openrc.agent.pojo.WebSocketMessage; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import javax.websocket.*; 13 | import javax.websocket.server.ServerEndpoint; 14 | import java.io.IOException; 15 | import java.nio.ByteBuffer; 16 | import java.util.UUID; 17 | 18 | @RestController 19 | @ServerEndpoint("/ws/device") 20 | public class DeviceWebSocketController { 21 | private final static Gson GSON = new Gson(); 22 | 23 | private final Logger logger = LoggerFactory.getLogger(DeviceWebSocketController.class); 24 | 25 | private final String deviceId = UUID.randomUUID().toString(); 26 | 27 | @OnOpen 28 | public void onOpen(Session session) { 29 | logger.info("onOpen deviceId = " + deviceId); 30 | session.setMaxBinaryMessageBufferSize(1024000); 31 | session.setMaxTextMessageBufferSize(1024000); 32 | session.setMaxIdleTimeout(0); 33 | } 34 | 35 | @OnMessage 36 | public void onMessage(Session session, String message) { 37 | logger.info("onMessage message = " + message + ", session = " + session); 38 | WebSocketMessage webSocketMessage = null; 39 | try { 40 | webSocketMessage = GSON.fromJson(message, WebSocketMessage.class); 41 | } catch (JsonSyntaxException e) { 42 | e.printStackTrace(); 43 | } 44 | 45 | if (webSocketMessage == null) { 46 | return; 47 | } 48 | 49 | final String name = webSocketMessage.getName(); 50 | 51 | if (name == null) { 52 | return; 53 | } 54 | 55 | if ("device_join".equals(name)) { 56 | DeviceInfo deviceInfo = GSON.fromJson(webSocketMessage.getData(), DeviceInfo.class); 57 | deviceInfo.setDeviceId(deviceId); 58 | Runtime.deviceList.add(deviceInfo); 59 | Runtime.deviceSessionHashMap.put(deviceId, session); 60 | } 61 | } 62 | 63 | @OnMessage 64 | public void onMessage(Session session, byte[] messages) { 65 | Session clientSession = Runtime.clientSessionHashMap.get(deviceId); 66 | if (clientSession == null) { 67 | return; 68 | } 69 | try { 70 | clientSession.getBasicRemote().sendBinary(ByteBuffer.wrap(messages)); 71 | } catch (IOException e) { 72 | e.printStackTrace(); 73 | } 74 | } 75 | 76 | @OnClose 77 | public void onClose(Session session) { 78 | logger.info("onClose session = " + session); 79 | Runtime.deviceList.removeIf(deviceInfo -> deviceId.equals(deviceInfo.getDeviceId())); 80 | Runtime.deviceSessionHashMap.remove(deviceId); 81 | } 82 | 83 | @OnError 84 | public void onError(Session session, Throwable error) { 85 | logger.info("onError error = " + error.getMessage() + ", session = " + session); 86 | // onError 会继续走 onClose 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/pojo/DeviceInfo.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent.pojo; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Builder 7 | @Data 8 | public class DeviceInfo { 9 | private String deviceId; 10 | private String manufacturer; 11 | private String model; 12 | private String os; 13 | private String osVersion; 14 | private Size size; 15 | } 16 | -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/pojo/Size.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent.pojo; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Builder 7 | @Data 8 | public class Size { 9 | private int width; 10 | private int height; 11 | } 12 | -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/pojo/WebSocketMessage.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent.pojo; 2 | 3 | import com.google.gson.JsonElement; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class WebSocketMessage { 8 | private String name; 9 | private String guid; 10 | private JsonElement data; 11 | } 12 | -------------------------------------------------------------------------------- /agent/api/src/main/java/me/jiahuan/openrc/agent/runtime/Runtime.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.agent.runtime; 2 | 3 | import me.jiahuan.openrc.agent.pojo.DeviceInfo; 4 | 5 | import javax.websocket.Session; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | public class Runtime { 11 | public static List deviceList = new ArrayList<>(); 12 | 13 | public static ConcurrentHashMap deviceSessionHashMap = new ConcurrentHashMap<>(); 14 | public static ConcurrentHashMap clientSessionHashMap = new ConcurrentHashMap<>(); 15 | } 16 | -------------------------------------------------------------------------------- /agent/api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8888 -------------------------------------------------------------------------------- /agent/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | } 4 | 5 | repositories { 6 | jcenter() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | classpath "org.springframework.boot:spring-boot-gradle-plugin:2.3.0.RELEASE" 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | subprojects { 23 | apply plugin: 'java' 24 | apply plugin: 'org.springframework.boot' 25 | apply plugin: 'io.spring.dependency-management' 26 | 27 | sourceCompatibility = 1.8 28 | targetCompatibility = 1.8 29 | 30 | dependencies { 31 | compileOnly("org.projectlombok:lombok:1.18.12") 32 | annotationProcessor 'org.projectlombok:lombok:1.18.12' 33 | testImplementation('org.springframework.boot:spring-boot-starter-test') { 34 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /agent/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuanyu/open-rc/12b3946a8396fcc06ed24809350876590391ad43/agent/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /agent/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /agent/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /agent/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /agent/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'agent' 2 | include 'api' 3 | -------------------------------------------------------------------------------- /command.md: -------------------------------------------------------------------------------- 1 | CLASSPATH=/data/local/tmp/stf-server.jar app_process /data/local/tmp me.jiahuan.stf.server.Server 2 | 3 | CLASSPATH=$(echo /data/app/com.package.name-*/base.apk) nohup app_process /system/bin --nice-name=process_name com.package.name.Main > /dev/null 2>&1 & 4 | 5 | adb shell CLASSPATH=$(adb shell pm path me.jiahuan.openrc.device) app_process /system/bin --nice-name=openrc_device_background me.jiahuan.openrc.device.background.Background -------------------------------------------------------------------------------- /customer/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /customer/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Customize configuration 19 | See [Configuration Reference](https://cli.vuejs.org/config/). 20 | -------------------------------------------------------------------------------- /customer/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /customer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.6.5", 11 | "element-ui": "^2.4.5", 12 | "vue": "^2.6.11", 13 | "vue-router": "^3.2.0", 14 | "vuex": "^3.4.0" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "~4.4.0", 18 | "@vue/cli-plugin-router": "~4.4.0", 19 | "@vue/cli-plugin-vuex": "~4.4.0", 20 | "@vue/cli-service": "~4.4.0", 21 | "axios": "^0.18.0", 22 | "vue-cli-plugin-axios": "0.0.4", 23 | "vue-cli-plugin-element": "^1.0.1", 24 | "vue-template-compiler": "^2.6.11" 25 | }, 26 | "browserslist": [ 27 | "> 1%", 28 | "last 2 versions", 29 | "not dead" 30 | ] 31 | } -------------------------------------------------------------------------------- /customer/public/Player.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | usage: 5 | 6 | p = new Player({ 7 | useWorker: , 8 | workerFile: // give path to Decoder.js 9 | webgl: true | false | "auto" // defaults to "auto" 10 | }); 11 | 12 | // canvas property represents the canvas node 13 | // put it somewhere in the dom 14 | p.canvas; 15 | 16 | p.webgl; // contains the used rendering mode. if you pass auto to webgl you can see what auto detection resulted in 17 | 18 | p.decode(); 19 | 20 | 21 | */ 22 | 23 | 24 | 25 | // universal module definition 26 | (function (root, factory) { 27 | if (typeof define === 'function' && define.amd) { 28 | // AMD. Register as an anonymous module. 29 | define(["./Decoder", "./YUVCanvas"], factory); 30 | } else if (typeof exports === 'object') { 31 | // Node. Does not work with strict CommonJS, but 32 | // only CommonJS-like environments that support module.exports, 33 | // like Node. 34 | module.exports = factory(require("./Decoder"), require("./YUVCanvas")); 35 | } else { 36 | // Browser globals (root is window) 37 | root.Player = factory(root.Decoder, root.YUVCanvas); 38 | } 39 | }(this, function (Decoder, WebGLCanvas) { 40 | "use strict"; 41 | 42 | 43 | var nowValue = Decoder.nowValue; 44 | 45 | 46 | var Player = function(parOptions){ 47 | var self = this; 48 | this._config = parOptions || {}; 49 | 50 | this.render = true; 51 | if (this._config.render === false){ 52 | this.render = false; 53 | }; 54 | 55 | this.nowValue = nowValue; 56 | 57 | this._config.workerFile = this._config.workerFile || "Decoder.js"; 58 | if (this._config.preserveDrawingBuffer){ 59 | this._config.contextOptions = this._config.contextOptions || {}; 60 | this._config.contextOptions.preserveDrawingBuffer = true; 61 | }; 62 | 63 | var webgl = "auto"; 64 | if (this._config.webgl === true){ 65 | webgl = true; 66 | }else if (this._config.webgl === false){ 67 | webgl = false; 68 | }; 69 | 70 | if (webgl == "auto"){ 71 | webgl = true; 72 | try{ 73 | if (!window.WebGLRenderingContext) { 74 | // the browser doesn't even know what WebGL is 75 | webgl = false; 76 | } else { 77 | var canvas = document.createElement('canvas'); 78 | var ctx = canvas.getContext("webgl"); 79 | if (!ctx) { 80 | // browser supports WebGL but initialization failed. 81 | webgl = false; 82 | }; 83 | }; 84 | }catch(e){ 85 | webgl = false; 86 | }; 87 | }; 88 | 89 | this.webgl = webgl; 90 | 91 | // choose functions 92 | if (this.webgl){ 93 | this.createCanvasObj = this.createCanvasWebGL; 94 | this.renderFrame = this.renderFrameWebGL; 95 | }else{ 96 | this.createCanvasObj = this.createCanvasRGB; 97 | this.renderFrame = this.renderFrameRGB; 98 | }; 99 | 100 | 101 | var lastWidth; 102 | var lastHeight; 103 | var onPictureDecoded = function(buffer, width, height, infos) { 104 | self.onPictureDecoded(buffer, width, height, infos); 105 | 106 | var startTime = nowValue(); 107 | 108 | if (!buffer || !self.render) { 109 | return; 110 | }; 111 | 112 | self.renderFrame({ 113 | canvasObj: self.canvasObj, 114 | data: buffer, 115 | width: width, 116 | height: height 117 | }); 118 | 119 | if (self.onRenderFrameComplete){ 120 | self.onRenderFrameComplete({ 121 | data: buffer, 122 | width: width, 123 | height: height, 124 | infos: infos, 125 | canvasObj: self.canvasObj 126 | }); 127 | }; 128 | 129 | }; 130 | 131 | // provide size 132 | 133 | if (!this._config.size){ 134 | this._config.size = {}; 135 | }; 136 | this._config.size.width = this._config.size.width || 200; 137 | this._config.size.height = this._config.size.height || 200; 138 | 139 | if (this._config.useWorker){ 140 | var worker = new Worker(this._config.workerFile); 141 | this.worker = worker; 142 | worker.addEventListener('message', function(e) { 143 | var data = e.data; 144 | if (data.consoleLog){ 145 | console.log(data.consoleLog); 146 | return; 147 | }; 148 | 149 | onPictureDecoded.call(self, new Uint8Array(data.buf, 0, data.length), data.width, data.height, data.infos); 150 | 151 | }, false); 152 | 153 | worker.postMessage({type: "Broadway.js - Worker init", options: { 154 | rgb: !webgl, 155 | memsize: this.memsize, 156 | reuseMemory: this._config.reuseMemory ? true : false 157 | }}); 158 | 159 | if (this._config.transferMemory){ 160 | this.decode = function(parData, parInfo){ 161 | // no copy 162 | // instead we are transfering the ownership of the buffer 163 | // dangerous!!! 164 | 165 | worker.postMessage({buf: parData.buffer, offset: parData.byteOffset, length: parData.length, info: parInfo}, [parData.buffer]); // Send data to our worker. 166 | }; 167 | 168 | }else{ 169 | this.decode = function(parData, parInfo){ 170 | // Copy the sample so that we only do a structured clone of the 171 | // region of interest 172 | var copyU8 = new Uint8Array(parData.length); 173 | copyU8.set( parData, 0, parData.length ); 174 | worker.postMessage({buf: copyU8.buffer, offset: 0, length: parData.length, info: parInfo}, [copyU8.buffer]); // Send data to our worker. 175 | }; 176 | 177 | }; 178 | 179 | if (this._config.reuseMemory){ 180 | this.recycleMemory = function(parArray){ 181 | //this.beforeRecycle(); 182 | worker.postMessage({reuse: parArray.buffer}, [parArray.buffer]); // Send data to our worker. 183 | //this.afterRecycle(); 184 | }; 185 | } 186 | 187 | }else{ 188 | 189 | this.decoder = new Decoder({ 190 | rgb: !webgl 191 | }); 192 | this.decoder.onPictureDecoded = onPictureDecoded; 193 | 194 | this.decode = function(parData, parInfo){ 195 | self.decoder.decode(parData, parInfo); 196 | }; 197 | 198 | }; 199 | 200 | 201 | 202 | if (this.render){ 203 | this.canvasObj = this.createCanvasObj({ 204 | contextOptions: this._config.contextOptions 205 | }); 206 | this.canvas = this.canvasObj.canvas; 207 | }; 208 | 209 | this.domNode = this.canvas; 210 | 211 | lastWidth = this._config.size.width; 212 | lastHeight = this._config.size.height; 213 | 214 | }; 215 | 216 | Player.prototype = { 217 | 218 | onPictureDecoded: function(buffer, width, height, infos){}, 219 | 220 | // call when memory of decoded frames is not used anymore 221 | recycleMemory: function(buf){ 222 | }, 223 | /*beforeRecycle: function(){}, 224 | afterRecycle: function(){},*/ 225 | 226 | // for both functions options is: 227 | // 228 | // width 229 | // height 230 | // enableScreenshot 231 | // 232 | // returns a object that has a property canvas which is a html5 canvas 233 | createCanvasWebGL: function(options){ 234 | var canvasObj = this._createBasicCanvasObj(options); 235 | canvasObj.contextOptions = options.contextOptions; 236 | return canvasObj; 237 | }, 238 | 239 | createCanvasRGB: function(options){ 240 | var canvasObj = this._createBasicCanvasObj(options); 241 | return canvasObj; 242 | }, 243 | 244 | // part that is the same for webGL and RGB 245 | _createBasicCanvasObj: function(options){ 246 | options = options || {}; 247 | 248 | var obj = {}; 249 | var width = options.width; 250 | if (!width){ 251 | width = this._config.size.width; 252 | }; 253 | var height = options.height; 254 | if (!height){ 255 | height = this._config.size.height; 256 | }; 257 | obj.canvas = document.createElement('canvas'); 258 | obj.canvas.width = width; 259 | obj.canvas.height = height; 260 | obj.canvas.style.backgroundColor = "#0D0E1B"; 261 | 262 | 263 | return obj; 264 | }, 265 | 266 | // options: 267 | // 268 | // canvas 269 | // data 270 | renderFrameWebGL: function(options){ 271 | 272 | var canvasObj = options.canvasObj; 273 | 274 | var width = options.width || canvasObj.canvas.width; 275 | var height = options.height || canvasObj.canvas.height; 276 | 277 | if (canvasObj.canvas.width !== width || canvasObj.canvas.height !== height || !canvasObj.webGLCanvas){ 278 | canvasObj.canvas.width = width; 279 | canvasObj.canvas.height = height; 280 | canvasObj.webGLCanvas = new WebGLCanvas({ 281 | canvas: canvasObj.canvas, 282 | contextOptions: canvasObj.contextOptions, 283 | width: width, 284 | height: height 285 | }); 286 | }; 287 | 288 | var ylen = width * height; 289 | var uvlen = (width / 2) * (height / 2); 290 | 291 | canvasObj.webGLCanvas.drawNextOutputPicture({ 292 | yData: options.data.subarray(0, ylen), 293 | uData: options.data.subarray(ylen, ylen + uvlen), 294 | vData: options.data.subarray(ylen + uvlen, ylen + uvlen + uvlen) 295 | }); 296 | 297 | var self = this; 298 | self.recycleMemory(options.data); 299 | 300 | }, 301 | renderFrameRGB: function(options){ 302 | var canvasObj = options.canvasObj; 303 | 304 | var width = options.width || canvasObj.canvas.width; 305 | var height = options.height || canvasObj.canvas.height; 306 | 307 | if (canvasObj.canvas.width !== width || canvasObj.canvas.height !== height){ 308 | canvasObj.canvas.width = width; 309 | canvasObj.canvas.height = height; 310 | }; 311 | 312 | var ctx = canvasObj.ctx; 313 | var imgData = canvasObj.imgData; 314 | 315 | if (!ctx){ 316 | canvasObj.ctx = canvasObj.canvas.getContext('2d'); 317 | ctx = canvasObj.ctx; 318 | 319 | canvasObj.imgData = ctx.createImageData(width, height); 320 | imgData = canvasObj.imgData; 321 | }; 322 | 323 | imgData.data.set(options.data); 324 | ctx.putImageData(imgData, 0, 0); 325 | var self = this; 326 | self.recycleMemory(options.data); 327 | 328 | } 329 | 330 | }; 331 | 332 | return Player; 333 | 334 | })); 335 | 336 | -------------------------------------------------------------------------------- /customer/public/YUVCanvas.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Paperspace Co. All rights reserved. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to 6 | // deal in the Software without restriction, including without limitation the 7 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | // sell copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | // IN THE SOFTWARE. 21 | // 22 | 23 | 24 | // universal module definition 25 | (function (root, factory) { 26 | if (typeof define === 'function' && define.amd) { 27 | // AMD. Register as an anonymous module. 28 | define([], factory); 29 | } else if (typeof exports === 'object') { 30 | // Node. Does not work with strict CommonJS, but 31 | // only CommonJS-like environments that support module.exports, 32 | // like Node. 33 | module.exports = factory(); 34 | } else { 35 | // Browser globals (root is window) 36 | root.YUVCanvas = factory(); 37 | } 38 | }(this, function () { 39 | 40 | 41 | /** 42 | * This class can be used to render output pictures from an H264bsdDecoder to a canvas element. 43 | * If available the content is rendered using WebGL. 44 | */ 45 | function YUVCanvas(parOptions) { 46 | 47 | parOptions = parOptions || {}; 48 | 49 | this.canvasElement = parOptions.canvas || document.createElement("canvas"); 50 | this.contextOptions = parOptions.contextOptions; 51 | 52 | this.type = parOptions.type || "yuv420"; 53 | 54 | this.customYUV444 = parOptions.customYUV444; 55 | 56 | this.conversionType = parOptions.conversionType || "rec601"; 57 | 58 | this.width = parOptions.width || 640; 59 | this.height = parOptions.height || 320; 60 | 61 | this.animationTime = parOptions.animationTime || 0; 62 | 63 | this.canvasElement.width = this.width; 64 | this.canvasElement.height = this.height; 65 | 66 | this.initContextGL(); 67 | 68 | if(this.contextGL) { 69 | this.initProgram(); 70 | this.initBuffers(); 71 | this.initTextures(); 72 | }; 73 | 74 | 75 | /** 76 | * Draw the next output picture using WebGL 77 | */ 78 | if (this.type === "yuv420"){ 79 | this.drawNextOuptutPictureGL = function(par) { 80 | var gl = this.contextGL; 81 | var texturePosBuffer = this.texturePosBuffer; 82 | var uTexturePosBuffer = this.uTexturePosBuffer; 83 | var vTexturePosBuffer = this.vTexturePosBuffer; 84 | 85 | var yTextureRef = this.yTextureRef; 86 | var uTextureRef = this.uTextureRef; 87 | var vTextureRef = this.vTextureRef; 88 | 89 | var yData = par.yData; 90 | var uData = par.uData; 91 | var vData = par.vData; 92 | 93 | var width = this.width; 94 | var height = this.height; 95 | 96 | var yDataPerRow = par.yDataPerRow || width; 97 | var yRowCnt = par.yRowCnt || height; 98 | 99 | var uDataPerRow = par.uDataPerRow || (width / 2); 100 | var uRowCnt = par.uRowCnt || (height / 2); 101 | 102 | var vDataPerRow = par.vDataPerRow || uDataPerRow; 103 | var vRowCnt = par.vRowCnt || uRowCnt; 104 | 105 | gl.viewport(0, 0, width, height); 106 | 107 | var tTop = 0; 108 | var tLeft = 0; 109 | var tBottom = height / yRowCnt; 110 | var tRight = width / yDataPerRow; 111 | var texturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]); 112 | 113 | gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer); 114 | gl.bufferData(gl.ARRAY_BUFFER, texturePosValues, gl.DYNAMIC_DRAW); 115 | 116 | if (this.customYUV444){ 117 | tBottom = height / uRowCnt; 118 | tRight = width / uDataPerRow; 119 | }else{ 120 | tBottom = (height / 2) / uRowCnt; 121 | tRight = (width / 2) / uDataPerRow; 122 | }; 123 | var uTexturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]); 124 | 125 | gl.bindBuffer(gl.ARRAY_BUFFER, uTexturePosBuffer); 126 | gl.bufferData(gl.ARRAY_BUFFER, uTexturePosValues, gl.DYNAMIC_DRAW); 127 | 128 | 129 | if (this.customYUV444){ 130 | tBottom = height / vRowCnt; 131 | tRight = width / vDataPerRow; 132 | }else{ 133 | tBottom = (height / 2) / vRowCnt; 134 | tRight = (width / 2) / vDataPerRow; 135 | }; 136 | var vTexturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]); 137 | 138 | gl.bindBuffer(gl.ARRAY_BUFFER, vTexturePosBuffer); 139 | gl.bufferData(gl.ARRAY_BUFFER, vTexturePosValues, gl.DYNAMIC_DRAW); 140 | 141 | 142 | gl.activeTexture(gl.TEXTURE0); 143 | gl.bindTexture(gl.TEXTURE_2D, yTextureRef); 144 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, yDataPerRow, yRowCnt, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, yData); 145 | 146 | gl.activeTexture(gl.TEXTURE1); 147 | gl.bindTexture(gl.TEXTURE_2D, uTextureRef); 148 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, uDataPerRow, uRowCnt, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, uData); 149 | 150 | gl.activeTexture(gl.TEXTURE2); 151 | gl.bindTexture(gl.TEXTURE_2D, vTextureRef); 152 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, vDataPerRow, vRowCnt, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, vData); 153 | 154 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 155 | }; 156 | 157 | }else if (this.type === "yuv422"){ 158 | this.drawNextOuptutPictureGL = function(par) { 159 | var gl = this.contextGL; 160 | var texturePosBuffer = this.texturePosBuffer; 161 | 162 | var textureRef = this.textureRef; 163 | 164 | var data = par.data; 165 | 166 | var width = this.width; 167 | var height = this.height; 168 | 169 | var dataPerRow = par.dataPerRow || (width * 2); 170 | var rowCnt = par.rowCnt || height; 171 | 172 | gl.viewport(0, 0, width, height); 173 | 174 | var tTop = 0; 175 | var tLeft = 0; 176 | var tBottom = height / rowCnt; 177 | var tRight = width / (dataPerRow / 2); 178 | var texturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]); 179 | 180 | gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer); 181 | gl.bufferData(gl.ARRAY_BUFFER, texturePosValues, gl.DYNAMIC_DRAW); 182 | 183 | gl.uniform2f(gl.getUniformLocation(this.shaderProgram, 'resolution'), dataPerRow, height); 184 | 185 | gl.activeTexture(gl.TEXTURE0); 186 | gl.bindTexture(gl.TEXTURE_2D, textureRef); 187 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, dataPerRow, rowCnt, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data); 188 | 189 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 190 | }; 191 | }; 192 | 193 | }; 194 | 195 | /** 196 | * Returns true if the canvas supports WebGL 197 | */ 198 | YUVCanvas.prototype.isWebGL = function() { 199 | return this.contextGL; 200 | }; 201 | 202 | /** 203 | * Create the GL context from the canvas element 204 | */ 205 | YUVCanvas.prototype.initContextGL = function() { 206 | var canvas = this.canvasElement; 207 | var gl = null; 208 | 209 | var validContextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"]; 210 | var nameIndex = 0; 211 | 212 | while(!gl && nameIndex < validContextNames.length) { 213 | var contextName = validContextNames[nameIndex]; 214 | 215 | try { 216 | if (this.contextOptions){ 217 | gl = canvas.getContext(contextName, this.contextOptions); 218 | }else{ 219 | gl = canvas.getContext(contextName); 220 | }; 221 | } catch (e) { 222 | gl = null; 223 | } 224 | 225 | if(!gl || typeof gl.getParameter !== "function") { 226 | gl = null; 227 | } 228 | 229 | ++nameIndex; 230 | }; 231 | 232 | this.contextGL = gl; 233 | }; 234 | 235 | /** 236 | * Initialize GL shader program 237 | */ 238 | YUVCanvas.prototype.initProgram = function() { 239 | var gl = this.contextGL; 240 | 241 | // vertex shader is the same for all types 242 | var vertexShaderScript; 243 | var fragmentShaderScript; 244 | 245 | if (this.type === "yuv420"){ 246 | 247 | vertexShaderScript = [ 248 | 'attribute vec4 vertexPos;', 249 | 'attribute vec4 texturePos;', 250 | 'attribute vec4 uTexturePos;', 251 | 'attribute vec4 vTexturePos;', 252 | 'varying vec2 textureCoord;', 253 | 'varying vec2 uTextureCoord;', 254 | 'varying vec2 vTextureCoord;', 255 | 256 | 'void main()', 257 | '{', 258 | ' gl_Position = vertexPos;', 259 | ' textureCoord = texturePos.xy;', 260 | ' uTextureCoord = uTexturePos.xy;', 261 | ' vTextureCoord = vTexturePos.xy;', 262 | '}' 263 | ].join('\n'); 264 | 265 | fragmentShaderScript = [ 266 | 'precision highp float;', 267 | 'varying highp vec2 textureCoord;', 268 | 'varying highp vec2 uTextureCoord;', 269 | 'varying highp vec2 vTextureCoord;', 270 | 'uniform sampler2D ySampler;', 271 | 'uniform sampler2D uSampler;', 272 | 'uniform sampler2D vSampler;', 273 | 'uniform mat4 YUV2RGB;', 274 | 275 | 'void main(void) {', 276 | ' highp float y = texture2D(ySampler, textureCoord).r;', 277 | ' highp float u = texture2D(uSampler, uTextureCoord).r;', 278 | ' highp float v = texture2D(vSampler, vTextureCoord).r;', 279 | ' gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;', 280 | '}' 281 | ].join('\n'); 282 | 283 | }else if (this.type === "yuv422"){ 284 | vertexShaderScript = [ 285 | 'attribute vec4 vertexPos;', 286 | 'attribute vec4 texturePos;', 287 | 'varying vec2 textureCoord;', 288 | 289 | 'void main()', 290 | '{', 291 | ' gl_Position = vertexPos;', 292 | ' textureCoord = texturePos.xy;', 293 | '}' 294 | ].join('\n'); 295 | 296 | fragmentShaderScript = [ 297 | 'precision highp float;', 298 | 'varying highp vec2 textureCoord;', 299 | 'uniform sampler2D sampler;', 300 | 'uniform highp vec2 resolution;', 301 | 'uniform mat4 YUV2RGB;', 302 | 303 | 'void main(void) {', 304 | 305 | ' highp float texPixX = 1.0 / resolution.x;', 306 | ' highp float logPixX = 2.0 / resolution.x;', // half the resolution of the texture 307 | ' highp float logHalfPixX = 4.0 / resolution.x;', // half of the logical resolution so every 4th pixel 308 | ' highp float steps = floor(textureCoord.x / logPixX);', 309 | ' highp float uvSteps = floor(textureCoord.x / logHalfPixX);', 310 | ' highp float y = texture2D(sampler, vec2((logPixX * steps) + texPixX, textureCoord.y)).r;', 311 | ' highp float u = texture2D(sampler, vec2((logHalfPixX * uvSteps), textureCoord.y)).r;', 312 | ' highp float v = texture2D(sampler, vec2((logHalfPixX * uvSteps) + texPixX + texPixX, textureCoord.y)).r;', 313 | 314 | //' highp float y = texture2D(sampler, textureCoord).r;', 315 | //' gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;', 316 | ' gl_FragColor = vec4(y, u, v, 1.0) * YUV2RGB;', 317 | '}' 318 | ].join('\n'); 319 | }; 320 | 321 | var YUV2RGB = []; 322 | 323 | if (this.conversionType == "rec709") { 324 | // ITU-T Rec. 709 325 | YUV2RGB = [ 326 | 1.16438, 0.00000, 1.79274, -0.97295, 327 | 1.16438, -0.21325, -0.53291, 0.30148, 328 | 1.16438, 2.11240, 0.00000, -1.13340, 329 | 0, 0, 0, 1, 330 | ]; 331 | } else { 332 | // assume ITU-T Rec. 601 333 | YUV2RGB = [ 334 | 1.16438, 0.00000, 1.59603, -0.87079, 335 | 1.16438, -0.39176, -0.81297, 0.52959, 336 | 1.16438, 2.01723, 0.00000, -1.08139, 337 | 0, 0, 0, 1 338 | ]; 339 | }; 340 | 341 | var vertexShader = gl.createShader(gl.VERTEX_SHADER); 342 | gl.shaderSource(vertexShader, vertexShaderScript); 343 | gl.compileShader(vertexShader); 344 | if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { 345 | console.log('Vertex shader failed to compile: ' + gl.getShaderInfoLog(vertexShader)); 346 | } 347 | 348 | var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 349 | gl.shaderSource(fragmentShader, fragmentShaderScript); 350 | gl.compileShader(fragmentShader); 351 | if(!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { 352 | console.log('Fragment shader failed to compile: ' + gl.getShaderInfoLog(fragmentShader)); 353 | } 354 | 355 | var program = gl.createProgram(); 356 | gl.attachShader(program, vertexShader); 357 | gl.attachShader(program, fragmentShader); 358 | gl.linkProgram(program); 359 | if(!gl.getProgramParameter(program, gl.LINK_STATUS)) { 360 | console.log('Program failed to compile: ' + gl.getProgramInfoLog(program)); 361 | } 362 | 363 | gl.useProgram(program); 364 | 365 | var YUV2RGBRef = gl.getUniformLocation(program, 'YUV2RGB'); 366 | gl.uniformMatrix4fv(YUV2RGBRef, false, YUV2RGB); 367 | 368 | this.shaderProgram = program; 369 | }; 370 | 371 | /** 372 | * Initialize vertex buffers and attach to shader program 373 | */ 374 | YUVCanvas.prototype.initBuffers = function() { 375 | var gl = this.contextGL; 376 | var program = this.shaderProgram; 377 | 378 | var vertexPosBuffer = gl.createBuffer(); 379 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer); 380 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW); 381 | 382 | var vertexPosRef = gl.getAttribLocation(program, 'vertexPos'); 383 | gl.enableVertexAttribArray(vertexPosRef); 384 | gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0); 385 | 386 | if (this.animationTime){ 387 | 388 | var animationTime = this.animationTime; 389 | var timePassed = 0; 390 | var stepTime = 15; 391 | 392 | var aniFun = function(){ 393 | 394 | timePassed += stepTime; 395 | var mul = ( 1 * timePassed ) / animationTime; 396 | 397 | if (timePassed >= animationTime){ 398 | mul = 1; 399 | }else{ 400 | setTimeout(aniFun, stepTime); 401 | }; 402 | 403 | var neg = -1 * mul; 404 | var pos = 1 * mul; 405 | 406 | var vertexPosBuffer = gl.createBuffer(); 407 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer); 408 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([pos, pos, neg, pos, pos, neg, neg, neg]), gl.STATIC_DRAW); 409 | 410 | var vertexPosRef = gl.getAttribLocation(program, 'vertexPos'); 411 | gl.enableVertexAttribArray(vertexPosRef); 412 | gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0); 413 | 414 | try{ 415 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 416 | }catch(e){}; 417 | 418 | }; 419 | aniFun(); 420 | 421 | }; 422 | 423 | 424 | 425 | var texturePosBuffer = gl.createBuffer(); 426 | gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer); 427 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW); 428 | 429 | var texturePosRef = gl.getAttribLocation(program, 'texturePos'); 430 | gl.enableVertexAttribArray(texturePosRef); 431 | gl.vertexAttribPointer(texturePosRef, 2, gl.FLOAT, false, 0, 0); 432 | 433 | this.texturePosBuffer = texturePosBuffer; 434 | 435 | if (this.type === "yuv420"){ 436 | var uTexturePosBuffer = gl.createBuffer(); 437 | gl.bindBuffer(gl.ARRAY_BUFFER, uTexturePosBuffer); 438 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW); 439 | 440 | var uTexturePosRef = gl.getAttribLocation(program, 'uTexturePos'); 441 | gl.enableVertexAttribArray(uTexturePosRef); 442 | gl.vertexAttribPointer(uTexturePosRef, 2, gl.FLOAT, false, 0, 0); 443 | 444 | this.uTexturePosBuffer = uTexturePosBuffer; 445 | 446 | 447 | var vTexturePosBuffer = gl.createBuffer(); 448 | gl.bindBuffer(gl.ARRAY_BUFFER, vTexturePosBuffer); 449 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW); 450 | 451 | var vTexturePosRef = gl.getAttribLocation(program, 'vTexturePos'); 452 | gl.enableVertexAttribArray(vTexturePosRef); 453 | gl.vertexAttribPointer(vTexturePosRef, 2, gl.FLOAT, false, 0, 0); 454 | 455 | this.vTexturePosBuffer = vTexturePosBuffer; 456 | }; 457 | 458 | }; 459 | 460 | /** 461 | * Initialize GL textures and attach to shader program 462 | */ 463 | YUVCanvas.prototype.initTextures = function() { 464 | var gl = this.contextGL; 465 | var program = this.shaderProgram; 466 | 467 | if (this.type === "yuv420"){ 468 | 469 | var yTextureRef = this.initTexture(); 470 | var ySamplerRef = gl.getUniformLocation(program, 'ySampler'); 471 | gl.uniform1i(ySamplerRef, 0); 472 | this.yTextureRef = yTextureRef; 473 | 474 | var uTextureRef = this.initTexture(); 475 | var uSamplerRef = gl.getUniformLocation(program, 'uSampler'); 476 | gl.uniform1i(uSamplerRef, 1); 477 | this.uTextureRef = uTextureRef; 478 | 479 | var vTextureRef = this.initTexture(); 480 | var vSamplerRef = gl.getUniformLocation(program, 'vSampler'); 481 | gl.uniform1i(vSamplerRef, 2); 482 | this.vTextureRef = vTextureRef; 483 | 484 | }else if (this.type === "yuv422"){ 485 | // only one texture for 422 486 | var textureRef = this.initTexture(); 487 | var samplerRef = gl.getUniformLocation(program, 'sampler'); 488 | gl.uniform1i(samplerRef, 0); 489 | this.textureRef = textureRef; 490 | 491 | }; 492 | }; 493 | 494 | /** 495 | * Create and configure a single texture 496 | */ 497 | YUVCanvas.prototype.initTexture = function() { 498 | var gl = this.contextGL; 499 | 500 | var textureRef = gl.createTexture(); 501 | gl.bindTexture(gl.TEXTURE_2D, textureRef); 502 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 503 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 504 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 505 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 506 | gl.bindTexture(gl.TEXTURE_2D, null); 507 | 508 | return textureRef; 509 | }; 510 | 511 | /** 512 | * Draw picture data to the canvas. 513 | * If this object is using WebGL, the data must be an I420 formatted ArrayBuffer, 514 | * Otherwise, data must be an RGBA formatted ArrayBuffer. 515 | */ 516 | YUVCanvas.prototype.drawNextOutputPicture = function(width, height, croppingParams, data) { 517 | var gl = this.contextGL; 518 | 519 | if(gl) { 520 | this.drawNextOuptutPictureGL(width, height, croppingParams, data); 521 | } else { 522 | this.drawNextOuptutPictureRGBA(width, height, croppingParams, data); 523 | } 524 | }; 525 | 526 | 527 | 528 | /** 529 | * Draw next output picture using ARGB data on a 2d canvas. 530 | */ 531 | YUVCanvas.prototype.drawNextOuptutPictureRGBA = function(width, height, croppingParams, data) { 532 | var canvas = this.canvasElement; 533 | 534 | var croppingParams = null; 535 | 536 | var argbData = data; 537 | 538 | var ctx = canvas.getContext('2d'); 539 | var imageData = ctx.getImageData(0, 0, width, height); 540 | imageData.data.set(argbData); 541 | 542 | if(croppingParams === null) { 543 | ctx.putImageData(imageData, 0, 0); 544 | } else { 545 | ctx.putImageData(imageData, -croppingParams.left, -croppingParams.top, 0, 0, croppingParams.width, croppingParams.height); 546 | } 547 | }; 548 | 549 | return YUVCanvas; 550 | 551 | })); 552 | -------------------------------------------------------------------------------- /customer/public/avc.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuanyu/open-rc/12b3946a8396fcc06ed24809350876590391ad43/customer/public/avc.wasm -------------------------------------------------------------------------------- /customer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuanyu/open-rc/12b3946a8396fcc06ed24809350876590391ad43/customer/public/favicon.ico -------------------------------------------------------------------------------- /customer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <%= htmlWebpackPlugin.options.title %> 14 | 15 | 16 | 17 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /customer/public/stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Stream = (function stream() { 4 | function constructor(url) { 5 | this.url = url; 6 | } 7 | 8 | constructor.prototype = { 9 | readAll: function(progress, complete) { 10 | var xhr = new XMLHttpRequest(); 11 | var async = true; 12 | xhr.open("GET", this.url, async); 13 | xhr.responseType = "arraybuffer"; 14 | if (progress) { 15 | xhr.onprogress = function (event) { 16 | progress(xhr.response, event.loaded, event.total); 17 | }; 18 | } 19 | xhr.onreadystatechange = function (event) { 20 | if (xhr.readyState === 4) { 21 | complete(xhr.response); 22 | // var byteArray = new Uint8Array(xhr.response); 23 | // var array = Array.prototype.slice.apply(byteArray); 24 | // complete(array); 25 | } 26 | } 27 | xhr.send(null); 28 | } 29 | }; 30 | return constructor; 31 | })(); 32 | 33 | 34 | -------------------------------------------------------------------------------- /customer/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /customer/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuanyu/open-rc/12b3946a8396fcc06ed24809350876590391ad43/customer/src/assets/logo.png -------------------------------------------------------------------------------- /customer/src/components/H264Player.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /customer/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import './plugins/axios' 3 | import App from './App.vue' 4 | import router from './router' 5 | import store from './store' 6 | import './plugins/element.js' 7 | 8 | Vue.config.productionTip = false 9 | 10 | new Vue({ 11 | router, 12 | store, 13 | render: h => h(App) 14 | }).$mount('#app') 15 | -------------------------------------------------------------------------------- /customer/src/plugins/axios.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Vue from 'vue'; 4 | import axios from "axios"; 5 | 6 | // Full config: https://github.com/axios/axios#request-config 7 | // axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || ''; 8 | // axios.defaults.headers.common['Authorization'] = AUTH_TOKEN; 9 | // axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; 10 | 11 | let config = { 12 | // baseURL: process.env.baseURL || process.env.apiUrl || "" 13 | // timeout: 60 * 1000, // Timeout 14 | // withCredentials: true, // Check cross-site Access-Control 15 | }; 16 | 17 | const _axios = axios.create(config); 18 | 19 | _axios.interceptors.request.use( 20 | function(config) { 21 | // Do something before request is sent 22 | return config; 23 | }, 24 | function(error) { 25 | // Do something with request error 26 | return Promise.reject(error); 27 | } 28 | ); 29 | 30 | // Add a response interceptor 31 | _axios.interceptors.response.use( 32 | function(response) { 33 | // Do something with response data 34 | return response; 35 | }, 36 | function(error) { 37 | // Do something with response error 38 | return Promise.reject(error); 39 | } 40 | ); 41 | 42 | Plugin.install = function(Vue, options) { 43 | Vue.axios = _axios; 44 | window.axios = _axios; 45 | Object.defineProperties(Vue.prototype, { 46 | axios: { 47 | get() { 48 | return _axios; 49 | } 50 | }, 51 | $axios: { 52 | get() { 53 | return _axios; 54 | } 55 | }, 56 | }); 57 | }; 58 | 59 | Vue.use(Plugin) 60 | 61 | export default Plugin; 62 | -------------------------------------------------------------------------------- /customer/src/plugins/element.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Element from 'element-ui' 3 | import 'element-ui/lib/theme-chalk/index.css' 4 | 5 | Vue.use(Element) 6 | -------------------------------------------------------------------------------- /customer/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import DeviceDetail from '../views/DeviceDetail.vue' 4 | import Devices from '../views/Devices.vue' 5 | 6 | Vue.use(VueRouter) 7 | 8 | const routes = [ 9 | { 10 | path: '/', 11 | name: 'Devices', 12 | component: Devices 13 | }, 14 | { 15 | path: '/device', 16 | name: 'DeviceDetail', 17 | component: DeviceDetail 18 | } 19 | ] 20 | 21 | const router = new VueRouter({ 22 | routes 23 | }) 24 | 25 | export default router 26 | -------------------------------------------------------------------------------- /customer/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | }, 9 | mutations: { 10 | }, 11 | actions: { 12 | }, 13 | modules: { 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /customer/src/views/DeviceDetail.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | -------------------------------------------------------------------------------- /customer/src/views/Devices.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /device/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | -------------------------------------------------------------------------------- /device/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /device/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 29 8 | 9 | defaultConfig { 10 | applicationId "me.jiahuan.openrc.device" 11 | minSdkVersion 21 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0.0" 15 | } 16 | 17 | compileOptions { 18 | sourceCompatibility JavaVersion.VERSION_1_8 19 | targetCompatibility JavaVersion.VERSION_1_8 20 | } 21 | 22 | signingConfigs { 23 | debug { 24 | storeFile file("../keystore/debug.keystore") 25 | storePassword "123456" 26 | keyAlias "android.keystore" 27 | keyPassword "123456" 28 | } 29 | 30 | release { 31 | storeFile file("../keystore/release.keystore") 32 | storePassword "123456" 33 | keyAlias "android.keystore" 34 | keyPassword "123456" 35 | } 36 | } 37 | 38 | buildTypes { 39 | debug { 40 | splits.abi.enable = false 41 | splits.density.enable = false 42 | signingConfig signingConfigs.debug 43 | minifyEnabled false 44 | useProguard false 45 | debuggable true 46 | } 47 | release { 48 | signingConfig signingConfigs.release 49 | minifyEnabled true 50 | useProguard true 51 | zipAlignEnabled true 52 | shrinkResources true 53 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 54 | } 55 | } 56 | 57 | dataBinding { 58 | enabled true 59 | } 60 | } 61 | 62 | dependencies { 63 | implementation "androidx.appcompat:appcompat:1.1.0" 64 | implementation "androidx.drawerlayout:drawerlayout:1.1.0" 65 | implementation "com.google.android.material:material:1.1.0" 66 | implementation "com.gyf.immersionbar:immersionbar:3.0.0" 67 | implementation "androidx.fragment:fragment-ktx:1.2.5" 68 | implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" 69 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" 70 | implementation "androidx.preference:preference:1.1.1" 71 | implementation "com.google.code.gson:gson:2.8.6" 72 | implementation "org.java-websocket:Java-WebSocket:1.4.0" 73 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_version" 74 | implementation "com.squareup.okhttp3:okhttp:4.7.2" 75 | } -------------------------------------------------------------------------------- /device/app/proguard-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuanyu/open-rc/12b3946a8396fcc06ed24809350876590391ad43/device/app/proguard-rules.pro -------------------------------------------------------------------------------- /device/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/App.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device 2 | 3 | import android.app.Application 4 | import me.jiahuan.openrc.device.foreground.service.ForegroundSocketService 5 | 6 | class App : Application() { 7 | override fun onCreate() { 8 | super.onCreate() 9 | ForegroundSocketService().connect() 10 | } 11 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/AppConstants.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device 2 | 3 | object AppConstants { 4 | const val CODE_SUCCESS = 200 5 | 6 | const val CODE_AUTH = 100 7 | 8 | const val SOCKET_PORT = 32195 9 | 10 | const val STF_DEVICE_SETTING_FILE_NAME = "openrc_device_settings.json" 11 | 12 | const val STF_DEVICE_SETTING_FILE_NAME_PATH = "/sdcard/Android/data/me.jiahuan.openrc.device/files/$STF_DEVICE_SETTING_FILE_NAME" 13 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/Background.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background 2 | 3 | import android.os.Looper 4 | import androidx.annotation.Keep 5 | import me.jiahuan.openrc.device.background.communication.BackgroundSocketService 6 | import me.jiahuan.openrc.device.background.connection.WebSocketClientConnection 7 | import me.jiahuan.openrc.device.background.device.Device 8 | 9 | @Keep 10 | object Background { 11 | @JvmStatic 12 | fun main(args: Array) { 13 | println("Server run") 14 | Looper.prepare() 15 | // 先建立本地 Socket 服务监听 16 | BackgroundSocketService().start() 17 | WebSocketClientConnection(Device()).start() 18 | Looper.loop() 19 | println("Server finish") 20 | } 21 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/communication/BackgroundSocketService.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.communication 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.isActive 6 | import kotlinx.coroutines.launch 7 | import me.jiahuan.openrc.device.AppConstants 8 | import java.io.BufferedReader 9 | import java.io.BufferedWriter 10 | import java.io.InputStreamReader 11 | import java.io.OutputStreamWriter 12 | import java.net.ServerSocket 13 | import java.net.Socket 14 | 15 | /** 16 | * 服务端 Socket 服务 17 | */ 18 | class BackgroundSocketService { 19 | 20 | private fun startListen(serverSocket: ServerSocket) { 21 | CoroutineScope(Dispatchers.IO).launch { 22 | while (isActive) { 23 | val socket = serverSocket.accept() 24 | println("有 Client 接入") 25 | startSocketClient(socket) 26 | } 27 | } 28 | } 29 | 30 | private fun startSocketClient(socket: Socket) { 31 | val bufferedReader = BufferedReader(InputStreamReader(socket.getInputStream())) 32 | val bufferedWriter = BufferedWriter(OutputStreamWriter(socket.getOutputStream())) 33 | CoroutineScope(Dispatchers.IO).launch { 34 | try { 35 | while (isActive) { 36 | val readLine = bufferedReader.readLine() ?: break 37 | if (AppConstants.CODE_AUTH.toString() == readLine) { 38 | bufferedWriter.write(AppConstants.CODE_SUCCESS.toString()) 39 | bufferedWriter.newLine() 40 | bufferedWriter.flush() 41 | println("auth success") 42 | } 43 | } 44 | } catch (e: Exception) { 45 | println("Client 通信发生错误 ${e.message}") 46 | } 47 | println("Client 拜拜") 48 | try { 49 | bufferedReader.close() 50 | } catch (e: Exception) { 51 | e.printStackTrace() 52 | } 53 | try { 54 | bufferedWriter.close() 55 | } catch (e: Exception) { 56 | e.printStackTrace() 57 | } 58 | try { 59 | socket.close() 60 | } catch (e: Exception) { 61 | e.printStackTrace() 62 | } 63 | } 64 | } 65 | 66 | fun start() { 67 | try { 68 | val serverSocket = ServerSocket(AppConstants.SOCKET_PORT) 69 | startListen(serverSocket) 70 | } catch (e: Exception) { 71 | throw AssertionError("无法建立 ${AppConstants.SOCKET_PORT} 端口的 Socket 监听, error = ${e.message}") 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/connection/AppWebSocketClient.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.connection 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Handler 5 | import android.os.Message 6 | import com.google.gson.Gson 7 | import me.jiahuan.openrc.device.AppConstants 8 | import me.jiahuan.openrc.device.model.Setting 9 | import me.jiahuan.openrc.device.utils.FileUtils 10 | import okhttp3.* 11 | import okio.ByteString 12 | import okio.ByteString.Companion.toByteString 13 | import java.io.File 14 | import java.nio.ByteBuffer 15 | import java.util.* 16 | import java.util.concurrent.TimeUnit 17 | import javax.net.ssl.HostnameVerifier 18 | 19 | 20 | /** 21 | * WebSocket服务 22 | */ 23 | class AppWebSocketClient(private val clientEventsHandler: ClientEventsHandler) : WebSocketListener() { 24 | companion object { 25 | private const val TAG = "AppWebSocketClient" 26 | 27 | private const val TIME_GAP_SEND_HEART_BEAT = 30 * 1000L 28 | 29 | private const val TIME_GAP_CHECK_PONG_PACKET = 10 * 1000L 30 | 31 | private const val TIME_GAP_CHECK_CONNECTION = 10 * 1000L 32 | 33 | private const val MSG_WHAT_SEND_HEART_BEAT = 1 34 | 35 | private const val MSG_WHAT_CHECK_PONG = 2 36 | 37 | private const val MSG_WHAT_CHECK_CONNECT = 3 38 | } 39 | 40 | enum class WebSocketConnectState { 41 | // 链接断开 42 | DISCONNECTED, 43 | 44 | // 正在断开链接 45 | DISCONNECTING, 46 | 47 | // 正在连接 48 | CONNECTING, 49 | 50 | // 已连接 51 | CONNECTED, 52 | } 53 | 54 | // 当前Socket链接状态 55 | private var currentWebSocketConnectState = WebSocketConnectState.DISCONNECTED 56 | 57 | // 当前WebSocket链接 58 | private var webSocket: WebSocket? = null 59 | 60 | // 检查是否有pong包 61 | private var checkPongSuccess = false 62 | 63 | // Handler 64 | @SuppressLint("HandlerLeak") 65 | private val handler = object : Handler() { 66 | override fun handleMessage(msg: Message) { 67 | when (msg.what) { 68 | // 发送心跳 69 | MSG_WHAT_SEND_HEART_BEAT -> { 70 | if (sendHeaderBeatPacket()) { 71 | postCheckPong() 72 | postSendHeartBeat(false) 73 | } else { 74 | reconnect() 75 | } 76 | } 77 | MSG_WHAT_CHECK_PONG -> { 78 | if (!checkPongSuccess) { 79 | reconnect() 80 | } 81 | } 82 | MSG_WHAT_CHECK_CONNECT -> { 83 | if (currentWebSocketConnectState != WebSocketConnectState.CONNECTED) { 84 | reconnect() 85 | } else { 86 | postCheckConnection() 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | override fun onOpen(webSocket: WebSocket, response: Response) { 94 | println("onOpen webSocket = $webSocket, response = $response") 95 | this.webSocket = webSocket 96 | this.currentWebSocketConnectState = WebSocketConnectState.CONNECTED 97 | clientEventsHandler.onOpen(webSocket, response) 98 | checkPongSuccess = false 99 | // postSendHeartBeat(true) 100 | } 101 | 102 | override fun onMessage(webSocket: WebSocket, text: String) { 103 | if (this.webSocket == webSocket) { 104 | println("onMessage webSocket = $webSocket, text = $text") 105 | clientEventsHandler.onMessage(webSocket, text) 106 | } 107 | } 108 | 109 | override fun onMessage(webSocket: WebSocket, bytes: ByteString) { 110 | println("onMessage webSocket = $webSocket, bytes = $bytes") 111 | clientEventsHandler.onMessage(webSocket, bytes) 112 | } 113 | 114 | override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { 115 | if (this.webSocket == webSocket) { 116 | println("onClosing webSocket = $webSocket, code = $code, reason = $reason") 117 | this.currentWebSocketConnectState = WebSocketConnectState.DISCONNECTING 118 | clientEventsHandler.onClosing(webSocket, code, reason) 119 | } 120 | } 121 | 122 | override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { 123 | if (this.webSocket == webSocket) { 124 | println("onClosed webSocket = $webSocket, code = $code, reason = $reason") 125 | this.currentWebSocketConnectState = WebSocketConnectState.DISCONNECTED 126 | clientEventsHandler.onClosed(webSocket, code, reason) 127 | this.webSocket = null 128 | } 129 | } 130 | 131 | override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { 132 | if (this.webSocket == webSocket) { 133 | println("onFailure webSocket = $webSocket, t = $t, response = $response") 134 | this.currentWebSocketConnectState = WebSocketConnectState.DISCONNECTED 135 | clientEventsHandler.onFailure(webSocket, t, response) 136 | this.webSocket = null 137 | } 138 | } 139 | 140 | /** 141 | * 连接 142 | */ 143 | fun connect() { 144 | // 判断当前Socket连接状态,是否需要连接 145 | if (currentWebSocketConnectState == WebSocketConnectState.DISCONNECTED) { 146 | println("connect()") 147 | cleanHandlerMessages() 148 | this.currentWebSocketConnectState = WebSocketConnectState.CONNECTING 149 | getOkHttpClient().newWebSocket(obtainRequest(), this) 150 | postCheckConnection() 151 | } else { 152 | println("connect(), Socket处于 $currentWebSocketConnectState") 153 | } 154 | } 155 | 156 | 157 | /** 158 | * 断开链接 159 | */ 160 | fun disconnect() { 161 | println("disconnect(), 请求断开链接") 162 | try { 163 | // 这里调用 close 不会回调 closed 方法 164 | webSocket?.close(1000, "I'm done") 165 | } catch (e: Exception) { 166 | println(e.message) 167 | } 168 | if (this.currentWebSocketConnectState != WebSocketConnectState.DISCONNECTED) { 169 | this.currentWebSocketConnectState = WebSocketConnectState.DISCONNECTED 170 | } 171 | this.webSocket = null 172 | cleanHandlerMessages() 173 | } 174 | 175 | /** 176 | * 重新链接 177 | */ 178 | fun reconnect() { 179 | println("reconnect(), 请求重新连接") 180 | disconnect() 181 | handler.postDelayed({ 182 | connect() 183 | }, 1000) 184 | } 185 | 186 | /** 187 | * 发送数据 188 | */ 189 | fun send(text: String): Boolean { 190 | println("send(), 发送 text = $text") 191 | return webSocket?.send(text) == true 192 | } 193 | 194 | fun send(byteBuffer: ByteBuffer): Boolean { 195 | return webSocket?.send(byteBuffer.toByteString()) == true 196 | } 197 | 198 | /** 199 | * 清理Handler数据 200 | */ 201 | private fun cleanHandlerMessages() { 202 | handler.removeCallbacksAndMessages(null) 203 | checkPongSuccess = false 204 | } 205 | 206 | /** 207 | * 发送心跳包 208 | */ 209 | private fun sendHeaderBeatPacket(): Boolean { 210 | checkPongSuccess = false 211 | return send(String.format("{\"guid\": \"%s\",\"name\": \"ping\"}", UUID.randomUUID())) 212 | } 213 | 214 | /** 215 | * 通知Handler发送心跳包 216 | */ 217 | private fun postSendHeartBeat(immediately: Boolean) { 218 | if (immediately) { 219 | handler.removeMessages(MSG_WHAT_SEND_HEART_BEAT) 220 | handler.sendEmptyMessage(MSG_WHAT_SEND_HEART_BEAT) 221 | } else { 222 | if (!handler.hasMessages(MSG_WHAT_SEND_HEART_BEAT)) { 223 | handler.sendEmptyMessageDelayed(MSG_WHAT_SEND_HEART_BEAT, TIME_GAP_SEND_HEART_BEAT) 224 | } 225 | } 226 | } 227 | 228 | /** 229 | * 通知handler检查pong包 230 | */ 231 | private fun postCheckPong() { 232 | handler.removeMessages(MSG_WHAT_CHECK_PONG) 233 | handler.sendEmptyMessageDelayed(MSG_WHAT_CHECK_PONG, TIME_GAP_CHECK_PONG_PACKET) 234 | } 235 | 236 | /** 237 | * 通知handler检查当前链接 238 | */ 239 | private fun postCheckConnection() { 240 | handler.removeMessages(MSG_WHAT_CHECK_CONNECT) 241 | handler.sendEmptyMessageDelayed(MSG_WHAT_CHECK_CONNECT, TIME_GAP_CHECK_CONNECTION) 242 | } 243 | 244 | private fun getOkHttpClient(): OkHttpClient { 245 | val builder = OkHttpClient.Builder() 246 | .connectTimeout(15, TimeUnit.SECONDS) 247 | .writeTimeout(15, TimeUnit.SECONDS) 248 | .readTimeout(15, TimeUnit.SECONDS) 249 | .hostnameVerifier(HostnameVerifier { _, _ -> true }) 250 | return builder.build() 251 | } 252 | 253 | private fun obtainRequest(): Request { 254 | val file = File(AppConstants.STF_DEVICE_SETTING_FILE_NAME_PATH) 255 | if (!file.exists()) { 256 | println("配置文件不存在") 257 | throw AssertionError("配置文件不存在") 258 | } 259 | val setting = try { 260 | val settingContent = FileUtils.getFileContent(file) 261 | println("settingContent = $settingContent") 262 | if (settingContent.isNullOrBlank()) { 263 | Setting() 264 | } else { 265 | Gson().fromJson(settingContent, Setting::class.java) 266 | } 267 | } catch (e: Exception) { 268 | println(e.message) 269 | null 270 | } 271 | val address = setting?.agentAddress 272 | if (address.isNullOrBlank()) { 273 | println("agent 地址未配置") 274 | throw AssertionError("agent 地址未配置") 275 | } 276 | return Request.Builder() 277 | .url("ws://$address/ws/device") 278 | .build() 279 | } 280 | 281 | interface ClientEventsHandler { 282 | fun onOpen(webSocket: WebSocket, response: Response) 283 | fun onMessage(webSocket: WebSocket, bytes: ByteString) 284 | fun onMessage(webSocket: WebSocket, text: String) 285 | fun onClosing(webSocket: WebSocket, code: Int, reason: String) 286 | fun onClosed(webSocket: WebSocket, code: Int, reason: String) 287 | fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) 288 | } 289 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/connection/AppWebSocketServer.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.connection 2 | 3 | import org.java_websocket.WebSocket 4 | import org.java_websocket.handshake.ClientHandshake 5 | import org.java_websocket.server.WebSocketServer 6 | import java.net.InetSocketAddress 7 | import java.nio.ByteBuffer 8 | 9 | 10 | class AppWebSocketServer(private val serverEventsHandler: ServerEventsHandler) : WebSocketServer(InetSocketAddress(DEFAULT_PORT_NUMBER)) { 11 | companion object { 12 | private const val DEFAULT_PORT_NUMBER = 8887 13 | } 14 | 15 | interface ServerEventsHandler { 16 | fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) 17 | fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) 18 | fun onMessage(conn: WebSocket?, message: String?) 19 | fun onMessage(conn: WebSocket?, message: ByteBuffer?) 20 | fun onError(conn: WebSocket?, ex: Exception?) 21 | fun onStart() 22 | } 23 | 24 | override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) { 25 | serverEventsHandler.onOpen(conn, handshake) 26 | } 27 | 28 | override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) { 29 | serverEventsHandler.onClose(conn, code, reason, remote) 30 | } 31 | 32 | override fun onMessage(conn: WebSocket?, message: String?) { 33 | serverEventsHandler.onMessage(conn, message) 34 | } 35 | 36 | override fun onMessage(conn: WebSocket?, message: ByteBuffer?) { 37 | serverEventsHandler.onMessage(conn, message) 38 | } 39 | 40 | override fun onStart() { 41 | serverEventsHandler.onStart() 42 | } 43 | 44 | override fun onError(conn: WebSocket?, ex: java.lang.Exception?) { 45 | serverEventsHandler.onError(conn, ex) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/connection/Connection.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.connection 2 | 3 | import me.jiahuan.openrc.device.background.controller.Controller 4 | import me.jiahuan.openrc.device.background.device.Device 5 | import java.nio.ByteBuffer 6 | 7 | /** 8 | * 数据传输链接 9 | */ 10 | abstract class Connection(protected val device: Device) { 11 | 12 | protected val controller by lazy { 13 | Controller(this, device) 14 | } 15 | 16 | abstract fun start() 17 | 18 | abstract fun stop() 19 | 20 | abstract fun send(byteBuffer: ByteBuffer) 21 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/connection/WebSocketClientConnection.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.connection 2 | 3 | import android.os.Build 4 | import com.google.gson.Gson 5 | import me.jiahuan.openrc.device.background.device.Device 6 | import me.jiahuan.openrc.device.background.device.DeviceInfo 7 | import me.jiahuan.openrc.device.background.encoder.ScreenEncoder 8 | import me.jiahuan.openrc.device.background.manager.ServiceManager 9 | import me.jiahuan.openrc.device.background.net.model.WebSocketMessage 10 | import me.jiahuan.openrc.device.background.net.model.input.InputEventData 11 | import okhttp3.Response 12 | import okhttp3.WebSocket 13 | import okio.ByteString 14 | import java.nio.ByteBuffer 15 | 16 | 17 | /** 18 | * WebSocket 服务端 19 | */ 20 | class WebSocketClientConnection(device: Device) : Connection(device), AppWebSocketClient.ClientEventsHandler { 21 | 22 | companion object { 23 | private val gson = Gson() 24 | } 25 | 26 | private val appWebSocketClient = AppWebSocketClient(this) 27 | 28 | override fun start() { 29 | appWebSocketClient.connect() 30 | ScreenEncoder(this, device).start() 31 | } 32 | 33 | override fun stop() { 34 | appWebSocketClient.disconnect() 35 | } 36 | 37 | override fun onOpen(webSocket: WebSocket, response: Response) { 38 | println("onOpen") 39 | val displayInfo = ServiceManager().displayManager.getDisplayInfo(0) 40 | val deviceInfo = gson.toJson( 41 | WebSocketMessage( 42 | "device_join", 43 | gson.toJsonTree( 44 | DeviceInfo( 45 | manufacturer = Build.MANUFACTURER, 46 | model = Build.MODEL, 47 | os = "android", 48 | osVersion = Build.VERSION.RELEASE, 49 | size = displayInfo.size 50 | ) 51 | ) 52 | ) 53 | ) 54 | // 发送设备信息 55 | appWebSocketClient.send(deviceInfo) 56 | } 57 | 58 | override fun onMessage(webSocket: WebSocket, bytes: ByteString) { 59 | 60 | } 61 | 62 | override fun onMessage(webSocket: WebSocket, text: String) { 63 | println("onMessage, text = $text") 64 | try { 65 | val webSocketMessage = try { 66 | gson.fromJson(text, WebSocketMessage::class.java) 67 | } catch (e: Exception) { 68 | println(e.message) 69 | null 70 | } 71 | 72 | if (webSocketMessage != null) { 73 | when (webSocketMessage.name) { 74 | "input_event" -> { 75 | controller.disposeInputEvent(gson.fromJson(webSocketMessage.data, InputEventData::class.java)) 76 | } 77 | } 78 | } 79 | } catch (e: Exception) { 80 | } 81 | } 82 | 83 | override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { 84 | println("onClosing") 85 | } 86 | 87 | override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { 88 | println("onClosed") 89 | } 90 | 91 | override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { 92 | println("onFailure") 93 | } 94 | 95 | override fun send(byteBuffer: ByteBuffer) { 96 | appWebSocketClient.send(byteBuffer) 97 | } 98 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/connection/WebSocketServerConnection.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.connection 2 | 3 | import me.jiahuan.openrc.device.background.device.Device 4 | import me.jiahuan.openrc.device.background.encoder.ScreenEncoder 5 | import org.java_websocket.WebSocket 6 | import org.java_websocket.handshake.ClientHandshake 7 | import java.nio.ByteBuffer 8 | 9 | /** 10 | * WebSocket 服务端 11 | */ 12 | class WebSocketServerConnection(device: Device) : Connection(device), AppWebSocketServer.ServerEventsHandler { 13 | 14 | private val appWebSocketServer = AppWebSocketServer(this) 15 | 16 | private val screenEncoderMap = HashMap() 17 | 18 | private val connections = ArrayList() 19 | 20 | init { 21 | appWebSocketServer.isReuseAddr = true 22 | } 23 | 24 | override fun start() { 25 | appWebSocketServer.start() 26 | ScreenEncoder(this, device).start() 27 | } 28 | 29 | override fun stop() { 30 | appWebSocketServer.stop() 31 | } 32 | 33 | override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) { 34 | println("onOpen") 35 | if (conn != null) { 36 | connections.add(conn) 37 | } 38 | } 39 | 40 | override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) { 41 | println("onClose code = $code, reason = $reason, remote = $remote") 42 | if (conn != null) { 43 | connections.remove(conn) 44 | } 45 | } 46 | 47 | override fun onMessage(conn: WebSocket?, message: String?) { 48 | println("onMessage message = $message") 49 | } 50 | 51 | override fun onMessage(conn: WebSocket?, message: ByteBuffer?) { 52 | println("onMessage message = $message") 53 | } 54 | 55 | override fun onError(conn: WebSocket?, ex: Exception?) { 56 | println("onError = ${ex?.message}") 57 | } 58 | 59 | override fun onStart() { 60 | println("onStart") 61 | } 62 | 63 | override fun send(byteBuffer: ByteBuffer) { 64 | connections.forEach { 65 | it.send(byteBuffer) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/controller/Controller.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.controller 2 | 3 | import android.os.SystemClock 4 | import android.view.InputDevice 5 | import android.view.MotionEvent 6 | import me.jiahuan.openrc.device.background.connection.Connection 7 | import me.jiahuan.openrc.device.background.device.Device 8 | import me.jiahuan.openrc.device.background.device.PointersState 9 | import me.jiahuan.openrc.device.background.net.model.input.InputEventAction 10 | import me.jiahuan.openrc.device.background.net.model.input.InputEventData 11 | 12 | /** 13 | * 控制相关 14 | */ 15 | class Controller(private val connection: Connection, private val device: Device) { 16 | private val DEVICE_ID_VIRTUAL = -1 17 | 18 | private var lastTouchDown = 0L 19 | private val pointersState = PointersState() 20 | private val pointerProperties = arrayOfNulls(PointersState.MAX_POINTERS) 21 | private val pointerCoords = arrayOfNulls(PointersState.MAX_POINTERS) 22 | 23 | init { 24 | initPointers() 25 | } 26 | 27 | private fun initPointers() { 28 | for (i in 0 until PointersState.MAX_POINTERS) { 29 | val props = MotionEvent.PointerProperties() 30 | props.toolType = MotionEvent.TOOL_TYPE_FINGER 31 | val coords = MotionEvent.PointerCoords() 32 | coords.orientation = 0f 33 | coords.size = 1f 34 | pointerProperties[i] = props 35 | pointerCoords[i] = coords 36 | } 37 | } 38 | 39 | fun disposeInputEvent(inputEventData: InputEventData) { 40 | val pointerIndex = pointersState.getPointerIndex(0) 41 | if (pointerIndex == -1) { 42 | return 43 | } 44 | val point = inputEventData.description.coordinate 45 | val pointer = pointersState.get(pointerIndex) 46 | pointer.point = point 47 | pointer.pressure = 1.0f 48 | pointer.isUp = InputEventAction.ACTION_UP.code == inputEventData.action 49 | val pointerCount = pointersState.update(pointerProperties, pointerCoords) 50 | val now = SystemClock.uptimeMillis() 51 | when (inputEventData.action) { 52 | InputEventAction.ACTION_DOWN.code -> { 53 | lastTouchDown = now 54 | val result = device.injectEvent(MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, pointerCoords, 0, 0, 1.0f, 1.0f, -1, 0, InputDevice.SOURCE_TOUCHSCREEN, 0)) 55 | println("ACTION_DOWN_RESULT = $result") 56 | } 57 | InputEventAction.ACTION_MOVE.code -> { 58 | val result = device.injectEvent(MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_MOVE, pointerCount, pointerProperties, pointerCoords, 0, 0, 1.0f, 1.0f, -1, 0, InputDevice.SOURCE_TOUCHSCREEN, 0)) 59 | println("ACTION_MOVE_RESULT = $result") 60 | } 61 | InputEventAction.ACTION_UP.code -> { 62 | val result = device.injectEvent(MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, pointerCoords, 0, 0, 1.0f, 1.0f, -1, 0, InputDevice.SOURCE_TOUCHSCREEN, 0)) 63 | println("ACTION_UP_RESULT = $result") 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/device/Device.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.device 2 | 3 | import android.os.Build 4 | import android.os.SystemClock 5 | import android.view.InputDevice 6 | import android.view.InputEvent 7 | import android.view.KeyCharacterMap 8 | import android.view.KeyEvent 9 | import me.jiahuan.openrc.device.background.manager.InputManager 10 | import me.jiahuan.openrc.device.background.manager.ServiceManager 11 | 12 | 13 | /** 14 | * 设备 15 | */ 16 | class Device { 17 | private val serviceManager = ServiceManager() 18 | 19 | val displayId = 0 20 | 21 | val displayInfo = serviceManager.displayManager.getDisplayInfo(displayId) 22 | 23 | val screenInfo = ScreenInfo.computeScreenInfo(displayInfo, 720, -1) 24 | 25 | val supportInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q 26 | 27 | fun injectEvent(inputEvent: InputEvent): Boolean { 28 | if (!supportInputEvents) { 29 | return false 30 | } 31 | return serviceManager.inputManager.injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC) 32 | } 33 | 34 | fun injectKeycode(keyCode: Int): Boolean { 35 | return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0) 36 | } 37 | 38 | private fun injectKeyEvent(action: Int, keyCode: Int, repeat: Int, metaState: Int): Boolean { 39 | val now = SystemClock.uptimeMillis() 40 | val event = KeyEvent( 41 | now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, 42 | InputDevice.SOURCE_KEYBOARD 43 | ) 44 | return injectEvent(event) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/device/DeviceInfo.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.device 2 | 3 | import me.jiahuan.openrc.device.background.model.Size 4 | 5 | /** 6 | * 设备信息 7 | */ 8 | class DeviceInfo( 9 | var manufacturer: String, 10 | var model: String, 11 | var os: String, 12 | var osVersion: String, 13 | var size: Size 14 | ) -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/device/DisplayInfo.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.device 2 | 3 | import me.jiahuan.openrc.device.background.model.Size 4 | 5 | class DisplayInfo(val displayId: Int, val size: Size, val rotation: Int, val layerStack: Int, val flags: Int) { 6 | override fun toString(): String { 7 | return "DisplayInfo(displayId=$displayId, size=$size, rotation=$rotation, layerStack=$layerStack, flags=$flags)" 8 | } 9 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/device/Pointer.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.device; 2 | 3 | import me.jiahuan.openrc.device.background.model.Point; 4 | 5 | public class Pointer { 6 | 7 | private final long id; 8 | 9 | private final int localId; 10 | 11 | private Point point; 12 | private float pressure; 13 | private boolean up; 14 | 15 | public Pointer(long id, int localId) { 16 | this.id = id; 17 | this.localId = localId; 18 | } 19 | 20 | public long getId() { 21 | return id; 22 | } 23 | 24 | public int getLocalId() { 25 | return localId; 26 | } 27 | 28 | public Point getPoint() { 29 | return point; 30 | } 31 | 32 | public void setPoint(Point point) { 33 | this.point = point; 34 | } 35 | 36 | public float getPressure() { 37 | return pressure; 38 | } 39 | 40 | public void setPressure(float pressure) { 41 | this.pressure = pressure; 42 | } 43 | 44 | public boolean isUp() { 45 | return up; 46 | } 47 | 48 | public void setUp(boolean up) { 49 | this.up = up; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/device/PointersState.java: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.device; 2 | 3 | import android.view.MotionEvent; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import me.jiahuan.openrc.device.background.model.Point; 9 | 10 | public class PointersState { 11 | 12 | public static final int MAX_POINTERS = 10; 13 | 14 | private final List pointers = new ArrayList<>(); 15 | 16 | private int indexOf(long id) { 17 | for (int i = 0; i < pointers.size(); ++i) { 18 | Pointer pointer = pointers.get(i); 19 | if (pointer.getId() == id) { 20 | return i; 21 | } 22 | } 23 | return -1; 24 | } 25 | 26 | private boolean isLocalIdAvailable(int localId) { 27 | for (int i = 0; i < pointers.size(); ++i) { 28 | Pointer pointer = pointers.get(i); 29 | if (pointer.getLocalId() == localId) { 30 | return false; 31 | } 32 | } 33 | return true; 34 | } 35 | 36 | private int nextUnusedLocalId() { 37 | for (int localId = 0; localId < MAX_POINTERS; ++localId) { 38 | if (isLocalIdAvailable(localId)) { 39 | return localId; 40 | } 41 | } 42 | return -1; 43 | } 44 | 45 | public Pointer get(int index) { 46 | return pointers.get(index); 47 | } 48 | 49 | public int getPointerIndex(long id) { 50 | int index = indexOf(id); 51 | if (index != -1) { 52 | // already exists, return it 53 | return index; 54 | } 55 | if (pointers.size() >= MAX_POINTERS) { 56 | // it's full 57 | return -1; 58 | } 59 | // id 0 is reserved for mouse events 60 | int localId = nextUnusedLocalId(); 61 | if (localId == -1) { 62 | throw new AssertionError("pointers.size() < maxFingers implies that a local id is available"); 63 | } 64 | Pointer pointer = new Pointer(id, localId); 65 | pointers.add(pointer); 66 | // return the index of the pointer 67 | return pointers.size() - 1; 68 | } 69 | 70 | /** 71 | * Initialize the motion event parameters. 72 | * 73 | * @param props the pointer properties 74 | * @param coords the pointer coordinates 75 | * @return The number of items initialized (the number of pointers). 76 | */ 77 | public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) { 78 | int count = pointers.size(); 79 | System.out.println("count = " + count); 80 | 81 | for (int i = 0; i < count; ++i) { 82 | Pointer pointer = pointers.get(i); 83 | 84 | // id 0 is reserved for mouse events 85 | props[i].id = pointer.getLocalId(); 86 | 87 | Point point = pointer.getPoint(); 88 | coords[i].x = point.getX(); 89 | coords[i].y = point.getY(); 90 | coords[i].pressure = pointer.getPressure(); 91 | } 92 | cleanUp(); 93 | return count; 94 | } 95 | 96 | /** 97 | * Remove all pointers which are UP. 98 | */ 99 | private void cleanUp() { 100 | for (int i = pointers.size() - 1; i >= 0; --i) { 101 | Pointer pointer = pointers.get(i); 102 | if (pointer.isUp()) { 103 | pointers.remove(i); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/device/ScreenInfo.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.device 2 | 3 | import android.graphics.Rect 4 | import me.jiahuan.openrc.device.background.model.Size 5 | 6 | class ScreenInfo( 7 | /** 8 | * 手机的屏幕物理尺寸,可能被裁剪 9 | */ 10 | val contentRect: Rect, 11 | /** 12 | * 视频流尺寸,可能裁剪 13 | */ 14 | val unlockedVideoSize: Size, 15 | /** 16 | * 设备方向 (0, 1, 2 or 3) 17 | */ 18 | val deviceRotation: Int, 19 | /** 20 | * 选定视频方向 (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW) 21 | */ 22 | private val lockedVideoOrientation: Int 23 | ) { 24 | 25 | companion object { 26 | private fun computeVideoSize(w: Int, h: Int, maxSize: Int): Size { 27 | var contentWidth = w 28 | var contentHeight = h 29 | contentWidth = contentWidth and 7.inv() 30 | contentHeight = contentHeight and 7.inv() 31 | if (maxSize > 0) { 32 | val portrait = contentHeight > contentWidth 33 | var major = if (portrait) contentHeight else contentWidth 34 | var minor = if (portrait) contentWidth else contentHeight 35 | if (major > maxSize) { 36 | val minorExact = minor * maxSize / major 37 | minor = minorExact + 4 and 7.inv() 38 | major = maxSize 39 | } 40 | contentWidth = if (portrait) minor else major 41 | contentHeight = if (portrait) major else minor 42 | } 43 | return Size(contentWidth, contentHeight) 44 | } 45 | 46 | fun computeScreenInfo(displayInfo: DisplayInfo, maxSize: Int, lockedVideoOrientation: Int): ScreenInfo { 47 | val rotation = displayInfo.rotation 48 | val deviceSize = displayInfo.size 49 | val contentRect = Rect(0, 0, deviceSize.width, deviceSize.height) 50 | return ScreenInfo(contentRect, computeVideoSize(contentRect.width(), contentRect.height(), maxSize), rotation, lockedVideoOrientation) 51 | } 52 | } 53 | 54 | val videoSize: Size 55 | get() = if (videoRotation % 2 == 0) { 56 | unlockedVideoSize 57 | } else unlockedVideoSize.rotate() 58 | 59 | val videoRotation: Int 60 | get() = if (lockedVideoOrientation == -1) { 61 | 0 62 | } else (deviceRotation + 4 - lockedVideoOrientation) % 4 63 | 64 | override fun toString(): String { 65 | return "ScreenInfo(contentRect=$contentRect, unlockedVideoSize=$unlockedVideoSize, deviceRotation=$deviceRotation, lockedVideoOrientation=$lockedVideoOrientation)" 66 | } 67 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/device/SurfaceControl.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.device 2 | 3 | import android.graphics.Rect 4 | import android.os.IBinder 5 | import android.view.Surface 6 | 7 | object SurfaceControl { 8 | private var CLASS: Class<*> = try { 9 | Class.forName("android.view.SurfaceControl") 10 | } catch (e: ClassNotFoundException) { 11 | throw AssertionError(e) 12 | } 13 | 14 | fun openTransaction() { 15 | try { 16 | CLASS.getMethod("openTransaction").invoke(null) 17 | } catch (e: Exception) { 18 | throw AssertionError(e) 19 | } 20 | } 21 | 22 | fun closeTransaction() { 23 | try { 24 | CLASS.getMethod("closeTransaction").invoke(null) 25 | } catch (e: Exception) { 26 | throw AssertionError(e) 27 | } 28 | } 29 | 30 | fun setDisplayProjection(displayToken: IBinder?, orientation: Int, layerStackRect: Rect?, displayRect: Rect?) { 31 | try { 32 | CLASS.getMethod("setDisplayProjection", IBinder::class.java, Int::class.javaPrimitiveType, Rect::class.java, Rect::class.java) 33 | .invoke(null, displayToken, orientation, layerStackRect, displayRect) 34 | } catch (e: Exception) { 35 | throw AssertionError(e) 36 | } 37 | } 38 | 39 | fun setDisplayLayerStack(displayToken: IBinder?, layerStack: Int) { 40 | try { 41 | CLASS.getMethod("setDisplayLayerStack", IBinder::class.java, Int::class.javaPrimitiveType).invoke(null, displayToken, layerStack) 42 | } catch (e: Exception) { 43 | throw AssertionError(e) 44 | } 45 | } 46 | 47 | fun setDisplaySurface(displayToken: IBinder?, surface: Surface?) { 48 | try { 49 | CLASS.getMethod("setDisplaySurface", IBinder::class.java, Surface::class.java).invoke(null, displayToken, surface) 50 | } catch (e: Exception) { 51 | throw AssertionError(e) 52 | } 53 | } 54 | 55 | fun createDisplay(name: String?, secure: Boolean): IBinder { 56 | return try { 57 | CLASS.getMethod("createDisplay", String::class.java, Boolean::class.javaPrimitiveType).invoke(null, name, secure) as IBinder 58 | } catch (e: Exception) { 59 | throw AssertionError(e) 60 | } 61 | } 62 | 63 | fun destroyDisplay(displayToken: IBinder?) { 64 | try { 65 | CLASS.getMethod("destroyDisplay", IBinder::class.java).invoke(null, displayToken) 66 | } catch (e: Exception) { 67 | throw AssertionError(e) 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/encoder/ScreenEncoder.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.encoder 2 | 3 | import android.graphics.Rect 4 | import android.media.MediaCodec 5 | import android.media.MediaCodecInfo 6 | import android.media.MediaFormat 7 | import android.os.IBinder 8 | import android.view.Surface 9 | import kotlinx.coroutines.* 10 | import me.jiahuan.openrc.device.background.connection.Connection 11 | import me.jiahuan.openrc.device.background.device.Device 12 | import me.jiahuan.openrc.device.background.device.SurfaceControl 13 | import java.nio.ByteBuffer 14 | 15 | 16 | class ScreenEncoder(private val connection: Connection, private val device: Device) { 17 | 18 | private var mediaCodecJob: Job? = null 19 | 20 | private var sps: ByteArray? = null 21 | private var pps: ByteArray? = null 22 | 23 | private fun createDisplay(): IBinder { 24 | return SurfaceControl.createDisplay("scrcpy", true) 25 | } 26 | 27 | private fun setDisplaySurface(display: IBinder, surface: Surface, orientation: Int, deviceRect: Rect, displayRect: Rect, layerStack: Int) { 28 | SurfaceControl.openTransaction() 29 | try { 30 | SurfaceControl.setDisplaySurface(display, surface) 31 | SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect) 32 | SurfaceControl.setDisplayLayerStack(display, layerStack) 33 | } finally { 34 | SurfaceControl.closeTransaction() 35 | } 36 | } 37 | 38 | private fun configMediaFormat(videoRect: Rect): MediaFormat { 39 | // 创建视频编码所需 40 | val mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoRect.width(), videoRect.height()) 41 | mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC) 42 | // 比特率,RGBA 4字节 43 | mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, videoRect.width() * videoRect.height() * 4 / 2) 44 | // 帧率 45 | mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15) 46 | // 颜色 47 | mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) 48 | // 关键帧 49 | mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10) 50 | mediaFormat.setInteger("level", MediaCodecInfo.CodecProfileLevel.AVCLevel3) 51 | 52 | mediaFormat.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 100_000) 53 | 54 | mediaFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline) 55 | return mediaFormat 56 | } 57 | 58 | private fun configMediaCodeC(videoRect: Rect): MediaCodec { 59 | val mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC) 60 | mediaCodec.configure(configMediaFormat(videoRect), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) 61 | return mediaCodec 62 | } 63 | 64 | fun start() { 65 | val displayInfo = device.displayInfo 66 | println("displayInfo = $displayInfo") 67 | 68 | val screenInfo = device.screenInfo 69 | println("screenInfo = $screenInfo") 70 | 71 | val mediaCodec = configMediaCodeC(screenInfo.videoSize.toRect()) 72 | val display = createDisplay() 73 | setDisplaySurface(display, mediaCodec.createInputSurface(), screenInfo.videoRotation, screenInfo.contentRect, screenInfo.unlockedVideoSize.toRect(), displayInfo.layerStack) 74 | 75 | val bufferInfo = MediaCodec.BufferInfo() 76 | mediaCodec.start() 77 | 78 | mediaCodecJob = CoroutineScope(Dispatchers.IO).launch { 79 | try { 80 | var eof = false 81 | while (!eof && isActive) { 82 | val outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, -1) 83 | if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 84 | // 获取 SPS 和 PPS 数据 85 | println("INFO_OUTPUT_FORMAT_CHANGED") 86 | val outputFormat: MediaFormat = mediaCodec.outputFormat 87 | // 获取编码SPS和PPS信息 88 | val spsByteBuffer = outputFormat.getByteBuffer("csd-0") 89 | if (spsByteBuffer != null) { 90 | sps = ByteArray(spsByteBuffer.remaining()) 91 | spsByteBuffer.get(sps!!, 0, sps!!.size) 92 | } 93 | 94 | val ppsByteBuffer = outputFormat.getByteBuffer("csd-1") 95 | if (ppsByteBuffer != null) { 96 | pps = ByteArray(ppsByteBuffer.remaining()) 97 | ppsByteBuffer.get(pps!!, 0, pps!!.size) 98 | } 99 | } else if (outputBufferIndex >= 0) { 100 | val outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex) 101 | if (outputBuffer != null) { 102 | if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) { 103 | sps?.apply { 104 | connection.send(ByteBuffer.wrap(this)) 105 | } 106 | pps?.apply { 107 | connection.send(ByteBuffer.wrap(this)) 108 | } 109 | } 110 | connection.send(outputBuffer) 111 | } 112 | mediaCodec.releaseOutputBuffer(outputBufferIndex, false) 113 | } 114 | eof = bufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM 115 | } 116 | } catch (e: Exception) { 117 | println("ScreenEncoder error = " + e.message) 118 | } finally { 119 | mediaCodec.stop() 120 | mediaCodec.release() 121 | SurfaceControl.destroyDisplay(display) 122 | } 123 | } 124 | } 125 | 126 | fun stop() { 127 | runBlocking { 128 | mediaCodecJob?.cancelAndJoin() 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/encoder/TestScreenRecorder.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.encoder 2 | 3 | import android.graphics.Rect 4 | import android.media.MediaCodec 5 | import android.media.MediaCodecInfo 6 | import android.media.MediaFormat 7 | import android.media.MediaMuxer 8 | import android.os.IBinder 9 | import android.view.Surface 10 | import me.jiahuan.openrc.device.background.device.SurfaceControl 11 | import me.jiahuan.openrc.device.background.manager.ServiceManager 12 | import me.jiahuan.openrc.device.background.device.ScreenInfo 13 | import java.io.File 14 | 15 | 16 | /** 17 | * 屏幕录制 18 | */ 19 | class TestScreenRecorder() { 20 | 21 | companion object { 22 | private const val WAIT_TIME_US = 50000L 23 | } 24 | 25 | private lateinit var mediaCodec: MediaCodec 26 | 27 | private lateinit var mediaMuxer: MediaMuxer 28 | 29 | private var isMediaEncodeToStop = false 30 | 31 | private var videoTrackIndex = -1 32 | 33 | private var isMediaMuxerStated = false 34 | 35 | private var videoEncoderThread: VideoEncoderThread? = null 36 | 37 | private lateinit var display: IBinder 38 | 39 | init { 40 | initializeMediaCodec() 41 | val screenFile = File("/sdcard/screen.mp4") 42 | screenFile.deleteOnExit() 43 | initMediaMuxer(screenFile) 44 | } 45 | 46 | private fun initializeMediaCodec() { 47 | val serviceManager = ServiceManager() 48 | 49 | val displayInfo = serviceManager.displayManager.getDisplayInfo(0) 50 | val layerStack = displayInfo.layerStack 51 | println("displayInfo = $displayInfo") 52 | // 53 | val screenInfo = ScreenInfo.computeScreenInfo(displayInfo, -1, -1) 54 | println("screenInfo = $screenInfo") 55 | val contentRect = screenInfo.contentRect 56 | val videoRect = screenInfo.videoSize.toRect() 57 | val unlockedVideoRect = screenInfo.unlockedVideoSize.toRect() 58 | val videoRotation = screenInfo.videoRotation 59 | 60 | // 创建视频编码所需 61 | val videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoRect.width(), videoRect.height()) 62 | videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) 63 | // 比特率,RGBA 4字节 64 | videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, videoRect.width() * videoRect.height() * 4) 65 | // 帧率 66 | videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30) 67 | // 关键帧 1秒 68 | videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) 69 | mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC) 70 | mediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) 71 | val surface = mediaCodec.createInputSurface() 72 | display = createDisplay() 73 | setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack) 74 | 75 | println("initialize success") 76 | } 77 | 78 | private fun createDisplay(): IBinder { 79 | return SurfaceControl.createDisplay("scrcpy", true) 80 | } 81 | 82 | private fun setDisplaySurface(display: IBinder, surface: Surface, orientation: Int, deviceRect: Rect, displayRect: Rect, layerStack: Int) { 83 | SurfaceControl.openTransaction() 84 | try { 85 | SurfaceControl.setDisplaySurface(display, surface) 86 | SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect) 87 | SurfaceControl.setDisplayLayerStack(display, layerStack) 88 | } finally { 89 | SurfaceControl.closeTransaction() 90 | } 91 | } 92 | 93 | /** 94 | * 初始化MediaMuxer 95 | */ 96 | private fun initMediaMuxer(file: File) { 97 | mediaMuxer = MediaMuxer(file.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) 98 | } 99 | 100 | /** 101 | * 视频(不包括音频)编码线程 102 | */ 103 | inner class VideoEncoderThread : Thread() { 104 | private var presentationTimeUs = 0L 105 | private var realExit = false 106 | 107 | override fun run() { 108 | try { 109 | val bufferInfo = MediaCodec.BufferInfo() 110 | mediaCodec.start() 111 | 112 | while (true) { 113 | if (realExit) { 114 | mediaCodec.stop() 115 | mediaCodec.release() 116 | mediaMuxer.release() 117 | SurfaceControl.destroyDisplay(display) 118 | break 119 | } 120 | 121 | if (isMediaEncodeToStop && !hasSignalEndOfStream) { 122 | mediaCodec.signalEndOfInputStream() 123 | hasSignalEndOfStream = true 124 | } 125 | 126 | var outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, WAIT_TIME_US) 127 | if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 128 | videoTrackIndex = mediaMuxer.addTrack(mediaCodec.outputFormat) 129 | mediaMuxer.start() 130 | isMediaMuxerStated = true 131 | } else { 132 | if (isMediaMuxerStated) { 133 | if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0 && isMediaEncodeToStop) { 134 | println("视频结束") 135 | realExit = true 136 | continue 137 | } 138 | 139 | while (outputBufferIndex >= 0) { 140 | println("outputBufferIndex = $outputBufferIndex") 141 | val outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex) 142 | if (outputBuffer != null) { 143 | outputBuffer.position(bufferInfo.offset) 144 | outputBuffer.limit(bufferInfo.offset + bufferInfo.size) 145 | if (presentationTimeUs == 0L) { 146 | presentationTimeUs = bufferInfo.presentationTimeUs 147 | } 148 | bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - presentationTimeUs 149 | if (bufferInfo.presentationTimeUs >= 0) { 150 | println("time = ${bufferInfo.presentationTimeUs / 1000000}") 151 | mediaMuxer.writeSampleData(videoTrackIndex, outputBuffer, bufferInfo) 152 | mediaCodec.releaseOutputBuffer(outputBufferIndex, false) 153 | } 154 | } 155 | if (isMediaEncodeToStop && !hasSignalEndOfStream) { 156 | mediaCodec.signalEndOfInputStream() 157 | hasSignalEndOfStream = true 158 | } 159 | outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, WAIT_TIME_US) 160 | } 161 | } 162 | } 163 | } 164 | } catch (e: Exception) { 165 | println(e.message) 166 | } 167 | } 168 | } 169 | 170 | private var hasSignalEndOfStream = false 171 | 172 | fun start() { 173 | videoEncoderThread = VideoEncoderThread() 174 | videoEncoderThread?.start() 175 | } 176 | 177 | fun stop() { 178 | isMediaEncodeToStop = true 179 | videoEncoderThread?.join() 180 | } 181 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/manager/DisplayManager.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.manager 2 | 3 | import android.os.IInterface 4 | import me.jiahuan.openrc.device.background.device.DisplayInfo 5 | import me.jiahuan.openrc.device.background.model.Size 6 | 7 | class DisplayManager(private val manager: IInterface) { 8 | fun getDisplayInfo(displayId: Int): DisplayInfo { 9 | return try { 10 | val displayInfo = manager.javaClass.getMethod("getDisplayInfo", Int::class.javaPrimitiveType).invoke(manager, displayId) ?: throw AssertionError("DisplayId is not correct, support displayIds = $displayIds") 11 | val cls: Class<*> = displayInfo.javaClass 12 | val width = cls.getDeclaredField("logicalWidth").getInt(displayInfo) 13 | val height = cls.getDeclaredField("logicalHeight").getInt(displayInfo) 14 | val rotation = cls.getDeclaredField("rotation").getInt(displayInfo) 15 | val layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo) 16 | val flags = cls.getDeclaredField("flags").getInt(displayInfo) 17 | DisplayInfo(displayId, Size(width, height), rotation, layerStack, flags) 18 | } catch (e: Exception) { 19 | throw AssertionError(e) 20 | } 21 | } 22 | 23 | val displayIds: IntArray 24 | get() = try { 25 | manager.javaClass.getMethod("getDisplayIds").invoke(manager) as IntArray 26 | } catch (e: Exception) { 27 | throw AssertionError(e) 28 | } 29 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/manager/InputManager.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.manager 2 | 3 | import android.os.IInterface 4 | import android.view.InputEvent 5 | import java.lang.reflect.InvocationTargetException 6 | import java.lang.reflect.Method 7 | 8 | class InputManager(private val manager: IInterface) { 9 | companion object { 10 | const val INJECT_INPUT_EVENT_MODE_ASYNC = 0 11 | } 12 | 13 | private var injectInputEventMethod: Method? = null 14 | 15 | fun injectInputEvent(inputEvent: InputEvent?, mode: Int): Boolean { 16 | return try { 17 | if (injectInputEventMethod == null) { 18 | injectInputEventMethod = manager.javaClass.getMethod("injectInputEvent", InputEvent::class.java, Int::class.javaPrimitiveType) 19 | } 20 | injectInputEventMethod?.invoke(manager, inputEvent, mode) as? Boolean ?: false 21 | } catch (e: InvocationTargetException) { 22 | false 23 | } catch (e: IllegalAccessException) { 24 | false 25 | } catch (e: NoSuchMethodException) { 26 | false 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/manager/ServiceManager.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.manager 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.IBinder 5 | import android.os.IInterface 6 | 7 | @SuppressLint("PrivateApi,DiscouragedPrivateApi") 8 | class ServiceManager { 9 | 10 | private val getServiceMethod = try { 11 | Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String::class.java) 12 | } catch (e: Exception) { 13 | throw AssertionError(e) 14 | } 15 | 16 | var displayManager: DisplayManager = DisplayManager(getService("display", "android.hardware.display.IDisplayManager")) 17 | private set 18 | 19 | var inputManager: InputManager = InputManager(getService("input", "android.hardware.input.IInputManager")) 20 | private set 21 | 22 | private fun getService(service: String, type: String): IInterface { 23 | return try { 24 | val binder = getServiceMethod.invoke(null, service) as IBinder 25 | val asInterfaceMethod = Class.forName("$type\$Stub").getMethod("asInterface", IBinder::class.java) 26 | asInterfaceMethod.invoke(null, binder) as IInterface 27 | } catch (e: Exception) { 28 | throw AssertionError(e) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/model/Point.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.model 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | class Point(val x: Int, val y: Int) { 7 | override fun toString(): String { 8 | return "Point(x=$x, y=$y)" 9 | } 10 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/model/PointF.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.model 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | class PointF(val x: Float, val y: Float) { 7 | override fun toString(): String { 8 | return "PointF(x=$x, y=$y)" 9 | } 10 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/model/Size.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.model 2 | 3 | import android.graphics.Rect 4 | import androidx.annotation.Keep 5 | 6 | @Keep 7 | class Size(val width: Int, val height: Int) { 8 | fun rotate(): Size { 9 | return Size(height, width) 10 | } 11 | 12 | fun toRect(): Rect { 13 | return Rect(0, 0, width, height) 14 | } 15 | 16 | override fun toString(): String { 17 | return "Size(width=$width, height=$height)" 18 | } 19 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/net/model/WebSocketMessage.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.net.model 2 | 3 | import androidx.annotation.Keep 4 | import com.google.gson.JsonElement 5 | import java.util.* 6 | 7 | @Keep 8 | class WebSocketMessage( 9 | var name: String, 10 | var data: JsonElement? = null, 11 | var guid: String = UUID.randomUUID().toString() 12 | ) -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/net/model/input/InputEventAction.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.net.model.input 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | enum class InputEventAction(val code: Int, val desc: String) { 7 | ACTION_DOWN(1, "按下"), 8 | ACTION_MOVE(2, "移动"), 9 | ACTION_UP(3, "弹起") 10 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/net/model/input/InputEventData.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.net.model.input 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | class InputEventData(val action: Int, val description: InputEventDescription) -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/background/net/model/input/InputEventDescription.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.background.net.model.input 2 | 3 | import androidx.annotation.Keep 4 | import me.jiahuan.openrc.device.background.model.Point 5 | 6 | @Keep 7 | class InputEventDescription(val coordinate: Point) -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/activity/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.activity 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import androidx.appcompat.app.ActionBarDrawerToggle 6 | import com.google.android.material.navigation.NavigationView 7 | import kotlinx.android.synthetic.main.layout_activity_main.* 8 | import me.jiahuan.openrc.device.R 9 | import me.jiahuan.openrc.device.foreground.base.BaseActivity 10 | import me.jiahuan.openrc.device.foreground.base.BaseViewModel 11 | import me.jiahuan.openrc.device.foreground.fragment.HomeFragment 12 | import me.jiahuan.openrc.device.foreground.fragment.SettingFragment 13 | 14 | 15 | class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { 16 | 17 | companion object { 18 | private const val TAG = "MainActivity" 19 | } 20 | 21 | private var homeFragment: HomeFragment? = null 22 | private var settingFragment: SettingFragment? = null 23 | 24 | override fun initializeViewModel(): BaseViewModel { 25 | return BaseViewModel() 26 | } 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.layout_activity_main) 31 | 32 | setSupportActionBar(id_tool_bar) 33 | 34 | val drawerToggle = ActionBarDrawerToggle( 35 | this, 36 | id_drawer_layout, 37 | id_tool_bar, 38 | R.string.drawer_open, 39 | R.string.drawer_close 40 | ) 41 | id_drawer_layout.addDrawerListener(drawerToggle) 42 | 43 | drawerToggle.syncState() 44 | 45 | id_navigation_view.setNavigationItemSelectedListener(this) 46 | 47 | showFragment(R.id.id_menu_home) 48 | } 49 | 50 | private fun showFragment(id: Int) { 51 | supportFragmentManager.fragments.forEach { 52 | supportFragmentManager.beginTransaction().hide(it).commit() 53 | } 54 | if (id == R.id.id_menu_home) { 55 | val fragment = supportFragmentManager.findFragmentByTag(HomeFragment.TAG) 56 | if (fragment != null) { 57 | supportFragmentManager.beginTransaction().show(fragment).commit() 58 | } else { 59 | if (homeFragment == null) { 60 | homeFragment = HomeFragment() 61 | } 62 | supportFragmentManager.beginTransaction().add(R.id.id_fragment_container, homeFragment!!, HomeFragment.TAG).commit() 63 | } 64 | } else if (id == R.id.id_menu_setting) { 65 | val fragment = supportFragmentManager.findFragmentByTag(SettingFragment.TAG) 66 | if (fragment != null) { 67 | supportFragmentManager.beginTransaction().show(fragment).commit() 68 | } else { 69 | if (settingFragment == null) { 70 | settingFragment = SettingFragment() 71 | } 72 | supportFragmentManager.beginTransaction().add(R.id.id_fragment_container, settingFragment!!, SettingFragment.TAG).commit() 73 | } 74 | } 75 | } 76 | 77 | override fun onNavigationItemSelected(menuItem: MenuItem): Boolean { 78 | menuItem.isChecked = true 79 | showFragment(menuItem.itemId) 80 | id_drawer_layout.closeDrawers() 81 | return true 82 | } 83 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.base 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.app.ProgressDialog 6 | import android.content.Context 7 | import android.content.DialogInterface 8 | import android.content.Intent 9 | import android.content.res.Resources 10 | import android.graphics.Color 11 | import android.os.Build 12 | import android.os.Bundle 13 | import android.view.View 14 | import android.view.WindowManager 15 | import android.widget.Toast 16 | import androidx.appcompat.app.AlertDialog 17 | import androidx.appcompat.app.AppCompatActivity 18 | import androidx.lifecycle.Observer 19 | import java.lang.reflect.InvocationTargetException 20 | 21 | abstract class BaseActivity : AppCompatActivity() { 22 | companion object { 23 | const val EXTRA_RESULT_RAW_DATA = "EXTRA_RESULT_RAW_DATA" 24 | } 25 | 26 | private var progressDialog: ProgressDialog? = null 27 | 28 | /** 29 | * 显示loading框,默认显示LoadingDialog 30 | * 需要自定义重写此方法即可 31 | */ 32 | protected open fun showLoadingDialog(message: String = "") { 33 | if (progressDialog == null) { 34 | progressDialog = ProgressDialog(this) 35 | progressDialog?.setCancelable(false) 36 | } 37 | progressDialog?.setMessage(message) 38 | progressDialog?.show() 39 | } 40 | 41 | /** 42 | * 隐藏loading框 43 | * 需要自定义重写此方法即可 44 | */ 45 | protected open fun dismissLoadDialog() { 46 | progressDialog?.dismiss() 47 | } 48 | 49 | /** 50 | * 显示 Long Toast 提示 51 | * 52 | * @param message 提示信息 53 | */ 54 | protected open fun showLongToast(message: String?) { 55 | if (message == null) { 56 | return 57 | } 58 | Toast.makeText(this, message, Toast.LENGTH_LONG).show() 59 | } 60 | 61 | /** 62 | * 显示 Short Toast 提示 63 | * 64 | * @param message 提示信息 65 | */ 66 | protected open fun showShortToast(message: String?) { 67 | if (message == null) { 68 | return 69 | } 70 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 71 | } 72 | 73 | /** 74 | * 显示Item选择对话框 75 | * 76 | * @param items 选择Item 77 | * @param itemClickListener Item点击事件 78 | */ 79 | protected fun showItemDialog( 80 | items: Array, 81 | itemClickListener: DialogInterface.OnClickListener? = null 82 | ) { 83 | AlertDialog.Builder(this) 84 | .setItems(items, itemClickListener) 85 | .show() 86 | } 87 | 88 | /** 89 | * 显示确认对话框 90 | * 91 | * @param title 标题 92 | * @param message 显示信息 93 | * @param positiveButtonText Positive按钮标题 94 | * @param positiveClickListener Positive按钮点击事件 95 | * @param negativePositiveText Negative按钮标题 96 | * @param negativeClickListener Negative按钮点击事件 97 | * @param cancelable 点击弹框外是否消失 98 | * @param autoDismiss 点击Positive或者Negative是否自动消失 99 | * 100 | */ 101 | protected fun showConfirmDialog( 102 | title: String = "", 103 | message: String = "", 104 | positiveButtonText: String = "", 105 | positiveClickListener: DialogInterface.OnClickListener? = null, 106 | negativePositiveText: String = "", 107 | negativeClickListener: DialogInterface.OnClickListener? = null, 108 | cancelable: Boolean = true, 109 | autoDismiss: Boolean = true 110 | ) { 111 | val dialog = AlertDialog.Builder(this) 112 | .setCancelable(cancelable) 113 | .setTitle(title) 114 | .setMessage(message) 115 | .setPositiveButton(positiveButtonText, positiveClickListener) 116 | .setNegativeButton(negativePositiveText, negativeClickListener) 117 | .show() 118 | 119 | if (!autoDismiss) { 120 | dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { 121 | positiveClickListener?.onClick(dialog, DialogInterface.BUTTON_POSITIVE) 122 | } 123 | 124 | dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { 125 | negativeClickListener?.onClick(dialog, DialogInterface.BUTTON_NEGATIVE) 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * 初始化 ViewModel 132 | */ 133 | protected abstract fun initializeViewModel(): BaseViewModel 134 | 135 | /** 136 | * 设计尺寸,如果子类重写并返回大于0的值,则表示开启适配 137 | * 目前只支持适配宽度 138 | */ 139 | open fun getDesignWidth(): Int { 140 | return 0 141 | } 142 | 143 | /** 144 | * 设置适配宽度 145 | * 适配模式需要使用pt作为尺寸单位 146 | * ScreenSize = sqrt(width * width + height * height) / 72 inch 147 | * 148 | * @param resources 资源对象 149 | * @param designWidth 设计宽度 150 | * 151 | * @return resources 152 | */ 153 | private fun adaptScreen(resources: Resources, designWidth: Int): Resources { 154 | if (designWidth > 0) { 155 | val newXdpi = (resources.displayMetrics.widthPixels * 72f) / designWidth 156 | applyDisplayMetrics(resources, newXdpi) 157 | } 158 | return resources 159 | } 160 | 161 | /** 162 | * 将新的xdpi应用 163 | * 164 | * @param resources 资源对象 165 | * @param newXdpi 新计算出的xdpi 166 | * 167 | * @return resources 168 | */ 169 | private fun applyDisplayMetrics(resources: Resources, newXdpi: Float) { 170 | resources.displayMetrics.xdpi = newXdpi 171 | getApplicationByReflect().resources.displayMetrics.xdpi = newXdpi 172 | } 173 | 174 | /** 175 | * 反射获取Application对象 176 | * 177 | * @return Application 178 | */ 179 | private fun getApplicationByReflect(): Application { 180 | try { 181 | val activityThread = Class.forName("android.app.ActivityThread") 182 | val thread = activityThread.getMethod("currentActivityThread").invoke(null) 183 | val app = activityThread.getMethod("getApplication").invoke(thread) 184 | ?: throw NullPointerException("u should init first") 185 | return app as Application 186 | } catch (e: NoSuchMethodException) { 187 | e.printStackTrace() 188 | } catch (e: IllegalAccessException) { 189 | e.printStackTrace() 190 | } catch (e: InvocationTargetException) { 191 | e.printStackTrace() 192 | } catch (e: ClassNotFoundException) { 193 | e.printStackTrace() 194 | } 195 | 196 | throw NullPointerException("u should init first") 197 | } 198 | 199 | /** 200 | * 获取 Activity,避免在内部类中使用 XXXX.this this@XXXX 带来的麻烦 201 | * 202 | * @return 当前Activity 203 | */ 204 | protected fun getActivity(): Activity { 205 | return this 206 | } 207 | 208 | /** 209 | * 获取 Activity 上下文,避免在内部类中使用 XXXX.this this@XXXX 带来的麻烦 210 | * 211 | * @return 当前 Activity 上下文 212 | */ 213 | protected fun getContext(): Context { 214 | return this 215 | } 216 | 217 | /** 218 | * 设置全屏模式,系统状态栏隐藏,导航栏透明,Android 9刘海区域显示内容,滑动显示状态栏过段时间消失 219 | */ 220 | protected fun setWindowFullScreenMode() { 221 | // >=Android 9 设置刘海显示模式 222 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 223 | val layoutParams = window.attributes 224 | layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 225 | window.attributes = layoutParams 226 | } 227 | window.navigationBarColor = Color.TRANSPARENT 228 | window.statusBarColor = Color.TRANSPARENT 229 | window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 230 | } 231 | 232 | /** 233 | * 设置全屏模式,系统状态栏隐藏,导航栏隐藏,Android 9刘海区域显示内容,滑动显示状态栏过段时间消失 234 | */ 235 | protected fun setWindowTotalFullScreenMode() { 236 | // >=Android 9 设置刘海显示模式 237 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 238 | val layoutParams = window.attributes 239 | layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 240 | window.attributes = layoutParams 241 | } 242 | // 不设置的话返回的时候导航条一片白 243 | window.navigationBarColor = Color.TRANSPARENT 244 | window.statusBarColor = Color.TRANSPARENT 245 | window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 246 | } 247 | 248 | override fun getResources(): Resources { 249 | return adaptScreen(super.getResources(), getDesignWidth()) 250 | } 251 | 252 | override fun onCreate(savedInstanceState: Bundle?) { 253 | super.onCreate(savedInstanceState) 254 | 255 | val viewModel = initializeViewModel() 256 | 257 | viewModel.loadingDialogStateLiveData.observe(this, Observer { loadingDialogWrapper -> 258 | if (loadingDialogWrapper == null) { 259 | dismissLoadDialog() 260 | } else { 261 | if (loadingDialogWrapper.show) { 262 | showLoadingDialog(loadingDialogWrapper.message) 263 | } else { 264 | dismissLoadDialog() 265 | } 266 | } 267 | }) 268 | 269 | viewModel.longToastShowLiveData.observe(this, Observer { messages -> 270 | messages?.forEach { 271 | showLongToast(it) 272 | } 273 | messages?.clear() 274 | }) 275 | 276 | viewModel.shortToastShowLiveData.observe(this, Observer { messages -> 277 | messages?.forEach { 278 | showShortToast(it) 279 | } 280 | messages?.clear() 281 | }) 282 | 283 | viewModel.finishLiveData.observe(this, Observer { result -> 284 | if (result != null) { 285 | setResult( 286 | result.resultCode, 287 | result.data?.putExtra(EXTRA_RESULT_RAW_DATA, intent.extras) 288 | ) 289 | } 290 | finish() 291 | }) 292 | } 293 | 294 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 295 | super.onActivityResult(requestCode, resultCode, data) 296 | val fragments = supportFragmentManager.fragments 297 | if (!fragments.isNullOrEmpty()) { 298 | fragments.forEach { 299 | it.onActivityResult(requestCode, resultCode, data) 300 | } 301 | } 302 | } 303 | 304 | final override fun onBackPressed() { 305 | var hasFragmentDisposeBackPressed = false 306 | val fragment = supportFragmentManager.fragments.filterIsInstance().find { it.isVisible } 307 | if (fragment != null) { 308 | hasFragmentDisposeBackPressed = fragment.onBackPressed() 309 | } 310 | if (!hasFragmentDisposeBackPressed) { 311 | onBackKeyPressed() 312 | } 313 | } 314 | 315 | protected open fun onBackKeyPressed() { 316 | super.onBackPressed() 317 | } 318 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.base 2 | 3 | import android.app.ProgressDialog 4 | import android.content.DialogInterface 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.widget.Toast 8 | import androidx.appcompat.app.AlertDialog 9 | import androidx.fragment.app.Fragment 10 | import androidx.lifecycle.Observer 11 | 12 | abstract class BaseFragment : Fragment() { 13 | 14 | private var progressDialog: ProgressDialog? = null 15 | 16 | /** 17 | * 显示Item选择对话框 18 | * 19 | * @param items 选择Item 20 | * @param itemClickListener Item点击事件 21 | */ 22 | protected fun showItemDialog( 23 | items: Array, 24 | itemClickListener: DialogInterface.OnClickListener? = null 25 | ) { 26 | context?.let { ctx -> 27 | AlertDialog.Builder(ctx) 28 | .setItems(items, itemClickListener) 29 | .show() 30 | } 31 | } 32 | 33 | 34 | /** 35 | * 显示确认对话框 36 | * 37 | * @param title 标题 38 | * @param message 显示信息 39 | * @param positiveButtonText Positive按钮标题 40 | * @param positiveClickListener Positive按钮点击事件 41 | * @param negativePositiveText Negative按钮标题 42 | * @param negativeClickListener Negative按钮点击事件 43 | * @param cancelable 点击弹框外是否消失 44 | * @param autoDismiss 点击Positive或者Negative是否自动消失 45 | * 46 | */ 47 | protected fun showConfirmDialog( 48 | title: String = "", 49 | message: String = "", 50 | positiveButtonText: String = "", 51 | positiveClickListener: DialogInterface.OnClickListener? = null, 52 | negativePositiveText: String = "", 53 | negativeClickListener: DialogInterface.OnClickListener? = null, 54 | cancelable: Boolean = true, 55 | autoDismiss: Boolean = true 56 | ) { 57 | context?.let { ctx -> 58 | val dialog = AlertDialog.Builder(ctx) 59 | .setCancelable(cancelable) 60 | .setTitle(title) 61 | .setMessage(message) 62 | .setPositiveButton(positiveButtonText, positiveClickListener) 63 | .setNegativeButton(negativePositiveText, negativeClickListener) 64 | .show() 65 | 66 | if (!autoDismiss) { 67 | dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { 68 | positiveClickListener?.onClick(dialog, DialogInterface.BUTTON_POSITIVE) 69 | } 70 | 71 | dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { 72 | negativeClickListener?.onClick(dialog, DialogInterface.BUTTON_NEGATIVE) 73 | } 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * 显示loading框,默认显示LoadingDialog 80 | * 需要自定义重写此方法即可 81 | */ 82 | protected open fun showLoadingDialog(message: String = "") { 83 | if (progressDialog == null) { 84 | progressDialog = ProgressDialog(context) 85 | progressDialog?.setCancelable(false) 86 | } 87 | progressDialog?.setMessage(message) 88 | progressDialog?.show() 89 | } 90 | 91 | /** 92 | * 隐藏loading框 93 | * 需要自定义重写此方法即可 94 | */ 95 | protected open fun dismissLoadDialog() { 96 | progressDialog?.dismiss() 97 | } 98 | 99 | /** 100 | * 显示 Long Toast 提示 101 | * 102 | * @param message 提示信息 103 | */ 104 | protected open fun showLongToast(message: String?) { 105 | if (message == null) { 106 | return 107 | } 108 | Toast.makeText(context, message, Toast.LENGTH_LONG).show() 109 | } 110 | 111 | /** 112 | * 显示 Short Toast 提示 113 | * 114 | * @param message 提示信息 115 | */ 116 | protected open fun showShortToast(message: String?) { 117 | if (message == null) { 118 | return 119 | } 120 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show() 121 | } 122 | 123 | /** 124 | * 返回按键 125 | */ 126 | internal fun onBackPressed(): Boolean { 127 | var hasFragmentDisposeBackPressed = false 128 | val fragment = childFragmentManager.fragments.filterIsInstance().find { it.isVisible } 129 | if (fragment != null) { 130 | hasFragmentDisposeBackPressed = fragment.onBackPressed() 131 | } 132 | if (!hasFragmentDisposeBackPressed) { 133 | return onBackKeyPressed() 134 | } 135 | return hasFragmentDisposeBackPressed 136 | } 137 | 138 | protected open fun onBackKeyPressed(): Boolean { 139 | return false 140 | } 141 | 142 | /** 143 | * 初始化 ViewModel 144 | */ 145 | protected abstract fun initializeViewModel(): BaseViewModel 146 | 147 | override fun onCreate(savedInstanceState: Bundle?) { 148 | super.onCreate(savedInstanceState) 149 | 150 | val viewModel = initializeViewModel() 151 | 152 | viewModel.loadingDialogStateLiveData.observe(this, Observer { loadingDialogWrapper -> 153 | if (loadingDialogWrapper == null) { 154 | dismissLoadDialog() 155 | } else { 156 | if (loadingDialogWrapper.show) { 157 | showLoadingDialog(loadingDialogWrapper.message) 158 | } else { 159 | dismissLoadDialog() 160 | } 161 | } 162 | }) 163 | 164 | viewModel.longToastShowLiveData.observe(this, Observer { messages -> 165 | messages?.forEach { 166 | showLongToast(it) 167 | } 168 | messages?.clear() 169 | }) 170 | 171 | viewModel.shortToastShowLiveData.observe(this, Observer { messages -> 172 | messages?.forEach { 173 | showShortToast(it) 174 | } 175 | messages?.clear() 176 | }) 177 | 178 | viewModel.finishLiveData.observe(this, Observer { result -> 179 | if (result != null) { 180 | activity?.setResult( 181 | result.resultCode, 182 | result.data?.putExtra(BaseActivity.EXTRA_RESULT_RAW_DATA, activity?.intent?.extras) 183 | ) 184 | } 185 | activity?.finish() 186 | }) 187 | } 188 | 189 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 190 | super.onActivityResult(requestCode, resultCode, data) 191 | val fragments = childFragmentManager.fragments 192 | if (!fragments.isNullOrEmpty()) { 193 | fragments.forEach { 194 | it.onActivityResult(requestCode, resultCode, data) 195 | } 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.base 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | 8 | open class BaseViewModel : ViewModel() { 9 | // 显示隐藏loadingDialog 10 | val loadingDialogStateLiveData = MutableLiveData() 11 | 12 | // toast 13 | val longToastShowLiveData = MutableLiveData?>() 14 | val shortToastShowLiveData = MutableLiveData?>() 15 | 16 | // finish 17 | val finishLiveData = MutableLiveData() 18 | 19 | protected fun showLoadingDialog(message: String = "") { 20 | loadingDialogStateLiveData.postValue(LoadingDialogWrapper(message, true)) 21 | } 22 | 23 | protected fun dismissLoadingDialog() { 24 | loadingDialogStateLiveData.postValue(LoadingDialogWrapper("", false)) 25 | } 26 | 27 | protected fun finish(result: ActivityResult? = ActivityResult()) { 28 | finishLiveData.postValue(result) 29 | } 30 | 31 | protected fun showLongToast(message: String?) { 32 | if (message == null) { 33 | return 34 | } 35 | var list = longToastShowLiveData.value 36 | if (list == null) { 37 | list = ArrayList() 38 | } 39 | list.add(message) 40 | longToastShowLiveData.postValue(list) 41 | } 42 | 43 | protected fun showShortToast(message: String?) { 44 | if (message == null) { 45 | return 46 | } 47 | var list = shortToastShowLiveData.value 48 | if (list == null) { 49 | list = ArrayList() 50 | } 51 | list.add(message) 52 | shortToastShowLiveData.postValue(list) 53 | } 54 | 55 | class ActivityResult( 56 | var resultCode: Int = Activity.RESULT_CANCELED, 57 | var data: Intent? = null 58 | ) 59 | 60 | class LoadingDialogWrapper( 61 | val message: String, 62 | val show: Boolean 63 | ) 64 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/data/SettingPreferenceDataStore.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.data 2 | 3 | import android.content.Context 4 | import androidx.preference.PreferenceDataStore 5 | import com.google.gson.Gson 6 | import me.jiahuan.openrc.device.AppConstants 7 | import me.jiahuan.openrc.device.R 8 | import me.jiahuan.openrc.device.model.Setting 9 | import me.jiahuan.openrc.device.utils.FileUtils 10 | import java.io.File 11 | 12 | class SettingPreferenceDataStore(private val context: Context) : PreferenceDataStore() { 13 | companion object { 14 | private val gson = Gson() 15 | } 16 | 17 | private val file = File(context.getExternalFilesDir(null), AppConstants.STF_DEVICE_SETTING_FILE_NAME) 18 | 19 | init { 20 | if (!file.exists()) { 21 | file.createNewFile() 22 | } 23 | } 24 | 25 | override fun putString(key: String?, value: String?) { 26 | if (key == context.resources.getString(R.string.setting_key_agent_address)) { 27 | val settings = getSetting() 28 | settings.agentAddress = value 29 | FileUtils.writeFileContent(file, gson.toJson(settings)) 30 | } 31 | } 32 | 33 | override fun getString(key: String?, defValue: String?): String? { 34 | if (key == context.resources.getString(R.string.setting_key_agent_address)) { 35 | return getSetting().agentAddress 36 | } 37 | return defValue 38 | } 39 | 40 | private fun getSetting(): Setting { 41 | val fileContent = FileUtils.getFileContent(file) 42 | if (fileContent.isNullOrBlank()) { 43 | return Setting() 44 | } 45 | return gson.fromJson(fileContent, Setting::class.java) 46 | } 47 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/event/AppEventCenter.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.event 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | 5 | object AppEventCenter { 6 | val backgroundServiceAliveLiveData = MutableLiveData() 7 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/fragment/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.fragment 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.fragment.app.Fragment 9 | import androidx.lifecycle.Observer 10 | import androidx.lifecycle.ViewModelProvider 11 | import me.jiahuan.openrc.device.R 12 | import me.jiahuan.openrc.device.databinding.LayoutFragmentHomeBinding 13 | import me.jiahuan.openrc.device.foreground.event.AppEventCenter 14 | import me.jiahuan.openrc.device.foreground.model.StatusInfo 15 | import me.jiahuan.openrc.device.foreground.model.StatusInfoSummary 16 | import me.jiahuan.openrc.device.foreground.viewmodel.HomeFragmentViewModel 17 | 18 | 19 | class HomeFragment : Fragment() { 20 | 21 | companion object { 22 | const val TAG = "HomeFragment" 23 | } 24 | 25 | private val viewModel by lazy { 26 | ViewModelProvider(this).get(HomeFragmentViewModel::class.java) 27 | } 28 | 29 | private lateinit var binding: LayoutFragmentHomeBinding 30 | 31 | private val statusInfoSummary = StatusInfoSummary() 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | } 36 | 37 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 38 | binding = DataBindingUtil.inflate(inflater, R.layout.layout_fragment_home, container, false) 39 | binding.statusInfoSummary = statusInfoSummary 40 | return binding.root 41 | } 42 | 43 | override fun onHiddenChanged(hidden: Boolean) { 44 | super.onHiddenChanged(hidden) 45 | if (!hidden) { 46 | checkAll() 47 | } 48 | } 49 | 50 | private fun checkAll() { 51 | AppEventCenter.backgroundServiceAliveLiveData.observe(this, Observer { 52 | if (it == null) { 53 | return@Observer 54 | } 55 | showSTFBackgroundServiceStatusInfo(it) 56 | }) 57 | // viewModel.checkBackgroundServiceAlive() 58 | } 59 | 60 | private fun showSTFBackgroundServiceStatusInfo(started: Boolean) { 61 | val stfBackgroundServiceStatusInfo = StatusInfo() 62 | stfBackgroundServiceStatusInfo.success = started 63 | stfBackgroundServiceStatusInfo.errMessage = if (started) { 64 | "STF后台服务已开启" 65 | } else { 66 | "STF后台服务未开启" 67 | } 68 | statusInfoSummary.stfBackgroundServiceStatusInfo = stfBackgroundServiceStatusInfo 69 | } 70 | 71 | override fun onResume() { 72 | super.onResume() 73 | checkAll() 74 | } 75 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/fragment/SettingFragment.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.fragment 2 | 3 | import android.os.Bundle 4 | import androidx.preference.Preference 5 | import androidx.preference.PreferenceFragmentCompat 6 | import me.jiahuan.openrc.device.R 7 | import me.jiahuan.openrc.device.foreground.data.SettingPreferenceDataStore 8 | 9 | 10 | class SettingFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener { 11 | 12 | companion object { 13 | const val TAG = "SettingFragment" 14 | } 15 | 16 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 17 | setPreferencesFromResource(R.xml.layout_fragment_setting, rootKey) 18 | preferenceManager.preferenceDataStore = SettingPreferenceDataStore(requireContext()) 19 | initialize() 20 | } 21 | 22 | override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { 23 | if (preference?.key == resources.getString(R.string.setting_key_agent_address)) { 24 | setAgentAddress(newValue.toString()) 25 | } 26 | return true 27 | } 28 | 29 | private fun initialize() { 30 | findPreference(resources.getString(R.string.setting_key_agent_address))?.onPreferenceChangeListener = this 31 | setAgentAddress(preferenceManager.preferenceDataStore?.getString(resources.getString(R.string.setting_key_agent_address), "")) 32 | } 33 | 34 | private fun setAgentAddress(address: String?) { 35 | findPreference(resources.getString(R.string.setting_key_agent_address))?.summary = if (address.isNullOrBlank()) "未设置" else address 36 | } 37 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/model/StatusInfo.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.model 2 | 3 | import androidx.annotation.Keep 4 | import androidx.databinding.BaseObservable 5 | import androidx.databinding.Bindable 6 | import me.jiahuan.openrc.device.BR 7 | 8 | @Keep 9 | class StatusInfo : BaseObservable() { 10 | @get:Bindable 11 | var success: Boolean = true 12 | set(value) { 13 | field = value 14 | notifyPropertyChanged(BR.success) 15 | } 16 | 17 | @get:Bindable 18 | var errMessage: String = "" 19 | set(value) { 20 | field = value 21 | notifyPropertyChanged(BR.errMessage) 22 | } 23 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/model/StatusInfoSummary.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.model 2 | 3 | import androidx.annotation.Keep 4 | import androidx.databinding.BaseObservable 5 | import androidx.databinding.Bindable 6 | import me.jiahuan.openrc.device.BR 7 | 8 | @Keep 9 | class StatusInfoSummary : BaseObservable() { 10 | // stf 后台状态 11 | @get:Bindable 12 | var stfBackgroundServiceStatusInfo = StatusInfo() 13 | set(value) { 14 | field = value 15 | notifyPropertyChanged(BR.stfBackgroundServiceStatusInfo) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/service/ForegroundSocketService.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.service 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Handler 5 | import android.os.Message 6 | import android.util.Log 7 | import kotlinx.coroutines.* 8 | import me.jiahuan.openrc.device.AppConstants 9 | import me.jiahuan.openrc.device.foreground.event.AppEventCenter 10 | import java.io.BufferedReader 11 | import java.io.BufferedWriter 12 | import java.io.InputStreamReader 13 | import java.io.OutputStreamWriter 14 | import java.net.InetAddress 15 | import java.net.Socket 16 | 17 | class ForegroundSocketService { 18 | 19 | companion object { 20 | private const val TAG = "ForegroundSocketService" 21 | private const val MSG_WHAT_CHECK_CONNECT = 1 22 | private const val TIME_GAP_CHECK_CONNECTION = 5000L 23 | } 24 | 25 | private var socket: Socket? = null 26 | 27 | enum class SocketConnectState { 28 | // 链接断开 29 | DISCONNECTED, 30 | 31 | CONNECTED, 32 | } 33 | 34 | // 当前Socket链接状态 35 | private var currentSocketConnectState = SocketConnectState.DISCONNECTED 36 | 37 | // Handler 38 | @SuppressLint("HandlerLeak") 39 | private val handler = object : Handler() { 40 | override fun handleMessage(msg: Message) { 41 | when (msg.what) { 42 | MSG_WHAT_CHECK_CONNECT -> { 43 | if (currentSocketConnectState != SocketConnectState.CONNECTED) { 44 | reconnect() 45 | } else { 46 | postCheckConnection() 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * 链接 55 | */ 56 | fun connect() { 57 | Log.d(TAG, "connect()") 58 | currentSocketConnectState = SocketConnectState.CONNECTED 59 | CoroutineScope(Dispatchers.IO).launch { 60 | try { 61 | socket = Socket(InetAddress.getLoopbackAddress(), AppConstants.SOCKET_PORT) 62 | socket?.let { socket -> 63 | val bufferedReader = BufferedReader(InputStreamReader(socket.getInputStream())) 64 | val bufferedWriter = BufferedWriter(OutputStreamWriter(socket.getOutputStream())) 65 | bufferedWriter.write(AppConstants.CODE_AUTH.toString()) 66 | bufferedWriter.newLine() 67 | bufferedWriter.flush() 68 | val readLine = bufferedReader.readLine() 69 | AppEventCenter.backgroundServiceAliveLiveData.postValue(AppConstants.CODE_SUCCESS.toString() == readLine) 70 | while (isActive) { 71 | bufferedReader.readLine() ?: break 72 | } 73 | try { 74 | bufferedReader.close() 75 | } catch (e: Exception) { 76 | } 77 | try { 78 | bufferedWriter.close() 79 | } catch (e: Exception) { 80 | } 81 | try { 82 | socket.close() 83 | } catch (e: Exception) { 84 | } 85 | } 86 | } catch (e: Exception) { 87 | } 88 | withContext(Dispatchers.Main) { 89 | AppEventCenter.backgroundServiceAliveLiveData.postValue(false) 90 | currentSocketConnectState = SocketConnectState.DISCONNECTED 91 | cleanHandlerMessages() 92 | postCheckConnection() 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * 断开链接 99 | */ 100 | fun disconnect() { 101 | Log.d(TAG, "disconnect()") 102 | try { 103 | socket?.close() 104 | } catch (e: Exception) { 105 | println(e.message) 106 | } 107 | if (this.currentSocketConnectState != SocketConnectState.DISCONNECTED) { 108 | this.currentSocketConnectState = SocketConnectState.DISCONNECTED 109 | } 110 | this.socket = null 111 | cleanHandlerMessages() 112 | } 113 | 114 | fun reconnect() { 115 | Log.d(TAG, "reconnect()") 116 | disconnect() 117 | handler.postDelayed({ 118 | connect() 119 | }, 1000) 120 | } 121 | 122 | private fun cleanHandlerMessages() { 123 | handler.removeCallbacksAndMessages(null) 124 | } 125 | 126 | /** 127 | * 通知handler检查当前链接 128 | */ 129 | private fun postCheckConnection() { 130 | handler.removeMessages(MSG_WHAT_CHECK_CONNECT) 131 | handler.sendEmptyMessageDelayed(MSG_WHAT_CHECK_CONNECT, TIME_GAP_CHECK_CONNECTION) 132 | } 133 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/utils/DeviceUtils.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.utils 2 | 3 | import java.io.BufferedReader 4 | import java.io.File 5 | import java.io.InputStreamReader 6 | 7 | 8 | object DeviceUtils { 9 | fun isDeviceRooted(): Boolean { 10 | return checkDebugSystem() || checkSuFile() || checkWhichSu() 11 | } 12 | 13 | private fun checkDebugSystem(): Boolean { 14 | val buildTags = android.os.Build.TAGS 15 | return buildTags != null && buildTags.contains("test-keys") 16 | } 17 | 18 | private fun checkSuFile(): Boolean { 19 | val paths = arrayOf("/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su") 20 | for (path in paths) { 21 | if (File(path).exists()) { 22 | return true 23 | } 24 | } 25 | return false 26 | } 27 | 28 | private fun checkWhichSu(): Boolean { 29 | var process: Process? = null 30 | try { 31 | process = Runtime.getRuntime().exec("/system/xbin/which su") 32 | val bufferedReader = BufferedReader(InputStreamReader(process.inputStream)) 33 | if (bufferedReader.readLine() != null) { 34 | return true 35 | } 36 | return false 37 | } catch (e: Exception) { 38 | e.printStackTrace() 39 | } finally { 40 | process?.destroy() 41 | } 42 | return false 43 | } 44 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/utils/ShellUtils.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.utils 2 | 3 | object ShellUtils { 4 | fun exec(command: String) { 5 | try { 6 | Runtime.getRuntime().exec(command) 7 | } catch (e: Exception) { 8 | e.printStackTrace() 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/view/EnableSTFBackgroundServiceAlertDialog.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.view 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import android.content.DialogInterface 6 | 7 | class EnableSTFBackgroundServiceAlertDialog(context: Context) : AlertDialog(context), DialogInterface.OnClickListener { 8 | override fun onClick(dialog: DialogInterface?, which: Int) { 9 | 10 | } 11 | 12 | override fun show() { 13 | setTitle("请启动STF后台服务") 14 | setMessage("xxxxxx") 15 | setButton(BUTTON_POSITIVE, "ADB", this) 16 | super.show() 17 | } 18 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/foreground/viewmodel/HomeFragmentViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.foreground.viewmodel 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import me.jiahuan.openrc.device.foreground.base.BaseViewModel 5 | 6 | class HomeFragmentViewModel : BaseViewModel() { 7 | 8 | val backgroundServiceAliveLiveData = MutableLiveData() 9 | 10 | /** 11 | * 检查后端服务是否存活 12 | */ 13 | // fun checkBackgroundServiceAlive() { 14 | // showLoadingDialog("正在检测STF后台服务是否启动") 15 | // viewModelScope.launch { 16 | // backgroundServiceAliveLiveData.value = withContext(Dispatchers.IO) { 17 | // var socket: Socket? = null 18 | // var bufferedReader: BufferedReader? = null 19 | // var bufferedWriter: BufferedWriter? = null 20 | // try { 21 | // socket = Socket(InetAddress.getLoopbackAddress(), AppConstants.SOCKET_PORT) 22 | // bufferedReader = BufferedReader(InputStreamReader(socket.getInputStream())) 23 | // bufferedWriter = BufferedWriter(OutputStreamWriter(socket.getOutputStream())) 24 | // bufferedWriter.write(AppConstants.CODE_AUTH.toString()) 25 | // bufferedWriter.newLine() 26 | // bufferedWriter.flush() 27 | // val readLine = bufferedReader.readLine() 28 | // Log.d("HomeFragment", "readLine = $readLine") 29 | // return@withContext AppConstants.CODE_SUCCESS.toString() == readLine 30 | // } catch (e: Exception) { 31 | // } finally { 32 | // try { 33 | // bufferedReader?.close() 34 | // } catch (e: Exception) { 35 | // } 36 | // try { 37 | // bufferedWriter?.close() 38 | // } catch (e: Exception) { 39 | // } 40 | // try { 41 | // socket?.close() 42 | // } catch (e: Exception) { 43 | // } 44 | // } 45 | // return@withContext false 46 | // } 47 | // dismissLoadingDialog() 48 | // } 49 | // } 50 | } -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/model/Setting.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.model 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | class Setting( 7 | var agentAddress: String? = null 8 | ) -------------------------------------------------------------------------------- /device/app/src/main/java/me/jiahuan/openrc/device/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package me.jiahuan.openrc.device.utils 2 | 3 | import java.io.File 4 | import java.io.FileInputStream 5 | import java.io.FileOutputStream 6 | 7 | object FileUtils { 8 | fun getFileContent(file: File): String? { 9 | val fileInputStream = FileInputStream(file) 10 | try { 11 | val buffer = ByteArray(1024) 12 | val stringBuilder = StringBuilder() 13 | var len = fileInputStream.read(buffer) 14 | while (len > 0) { 15 | stringBuilder.append(String(buffer, 0, len)) 16 | len = fileInputStream.read(buffer) 17 | } 18 | return stringBuilder.toString() 19 | } catch (e: Exception) { 20 | e.printStackTrace() 21 | } finally { 22 | try { 23 | fileInputStream.close() 24 | } catch (e: Exception) { 25 | } 26 | } 27 | return null 28 | } 29 | 30 | fun writeFileContent(file: File, value: String): Boolean { 31 | val fileOutputStream = FileOutputStream(file) 32 | try { 33 | fileOutputStream.write(value.toByteArray()) 34 | return true 35 | } catch (e: Exception) { 36 | e.printStackTrace() 37 | } finally { 38 | try { 39 | fileOutputStream.close() 40 | } catch (e: Exception) { 41 | } 42 | } 43 | return false 44 | } 45 | } -------------------------------------------------------------------------------- /device/app/src/main/res/drawable/drawable_icon_done.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /device/app/src/main/res/drawable/drawable_icon_error.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /device/app/src/main/res/layout/layout_activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 18 | 19 | 23 | 24 | 25 | 31 | 32 | -------------------------------------------------------------------------------- /device/app/src/main/res/layout/layout_fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 22 | 23 | 26 | 27 | 31 | 32 | 37 | 38 | 44 | 45 | 46 | 47 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /device/app/src/main/res/menu/menu_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 11 | 12 | 13 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /device/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuanyu/open-rc/12b3946a8396fcc06ed24809350876590391ad43/device/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /device/app/src/main/res/values-sw360dp-v13/values-preference.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 0dp 5 | 6 | -------------------------------------------------------------------------------- /device/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4CA1FB 4 | #4CA1FB 5 | #4CA1FB 6 | #49B04D 7 | #FF3434 8 | -------------------------------------------------------------------------------- /device/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | OpenRC被控端 3 | 4 | 5 | 6 | 常规 7 | 8 | 9 | setting_key_agent_address 10 | 11 | setting_key_openrc_background_service 12 | -------------------------------------------------------------------------------- /device/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /device/app/src/main/res/xml/layout_fragment_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /device/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /device/background_run.sh: -------------------------------------------------------------------------------- 1 | adb push openrc_device_settings.json /sdcard/Android/data/me.jiahuan.openrc.device/files/ 2 | adb shell CLASSPATH=$(adb shell pm path me.jiahuan.openrc.device) app_process /system/bin --nice-name=openrc_device_background me.jiahuan.openrc.device.background.Background 3 | -------------------------------------------------------------------------------- /device/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = "1.3.72" 3 | ext.kotlinx_version = '1.3.5' 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath "com.android.tools.build:gradle:4.0.0" 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /device/foreground_run.sh: -------------------------------------------------------------------------------- 1 | ./gradlew clean assembleRelease 2 | adb install ./app/build/outputs/apk/release/app-release.apk 3 | -------------------------------------------------------------------------------- /device/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 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /device/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuanyu/open-rc/12b3946a8396fcc06ed24809350876590391ad43/device/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /device/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jul 01 17:17:22 CST 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 | -------------------------------------------------------------------------------- /device/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 | -------------------------------------------------------------------------------- /device/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 | -------------------------------------------------------------------------------- /device/keystore/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuanyu/open-rc/12b3946a8396fcc06ed24809350876590391ad43/device/keystore/debug.keystore -------------------------------------------------------------------------------- /device/keystore/release.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuanyu/open-rc/12b3946a8396fcc06ed24809350876590391ad43/device/keystore/release.keystore -------------------------------------------------------------------------------- /device/openrc_device_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "agentAddress": "192.168.220.6:8888" 3 | } 4 | -------------------------------------------------------------------------------- /device/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------