分类 Java 下的文章

背景:

这几天居家隔离,想着之前的远程控制软件不太好用,要么收费,要么难用,之前也实现了安卓的远程控制,想着自己写一款适用于PC之间的通用远程桌面控制工具。

语言:

思来想去加上调研和自己熟悉的情况,决定采用Java来实现,具体原因有如下几点:

  • 个人相对于C、C++,本身对Java熟悉一点。
  • 为了之后实现安卓端,可以复用部分代码。
  • Java自带的Robot可以很方便实现远程控制,已经包含屏幕截屏,图片编码,用户事件处理。
  • 可能可以跨平台,关于资源占用,现代的21世纪(0202年)的电脑,应该都用上了SSD和4G+的内存了。
  • 其他...

知识储备

远程控制系统主要分为两个端:被控端、控制端。

  1. 首先,我们看看被控端需要做什么?

    • 通过socket/websocket接收远程控制请求
    • 获取屏幕截屏
    • 对截屏进行编码压缩(JPEG|自定义协议)
    • 通过socket/websocket发送截屏
    • 通过socket/websocket接收控制端的用户事件
    • 执行用户事件
    • 以上步骤循环
  2. 然后,我们看看控制端需要做什么?

    • 通过socket/websocket发送远程控制请求
    • 通过socket/websocket接收截屏
    • 把截屏在本地渲染出来(Java Canvas|Direct3D|OpenGL)
    • 通过socket/websocket发送用户事件
    • 以上步骤循环

原理简介:
1、使用反射截图,压缩为webp格式图片,把图片编码为base64格式,通过websocket服务发送给前端,前端绘制出图片。
2、websocket服务器使用websocket库:Java-WebSocket-1.4.0-with-dependencies.jar。
3、事件转发也是走websocket,发送事件类型和相关参数,websocket服务做出对应动作。

ScreenCaptor反射截图关键方法:

public static Bitmap getScreencap(int screenWidth, int screenHeight) {
    Bitmap bitmap = null;
    String surfaceClassName;
    ServiceManager serviceManager = new ServiceManager();

    if (screenHeight == 0 || screenWidth ==0){
        screenWidth = serviceManager.getDisplayManager().getDisplayInfo().getSize().getWidth();
        screenHeight = serviceManager.getDisplayManager().getDisplayInfo().getSize().getHeight();
    }

    if (Build.VERSION.SDK_INT <= 17) {
        surfaceClassName = "android.view.Surface";
    } else {
        surfaceClassName = "android.view.SurfaceControl";
    }

    try {
        // api_level >= 27,截图方法:public static Bitmap screenshot(int width, int height) {}
        if (Build.VERSION.SDK_INT <= 27) {

            bitmap = (Bitmap) Class.forName(surfaceClassName).getDeclaredMethod("screenshot", new Class[]{Integer.TYPE, Integer.TYPE})
                    .invoke(null, new Object[]{screenWidth, screenHeight});

        } else {
            // 参考这里https://medium.com/@punpun/android-surfacecontrol-screenshot-changed-in-android-pie-9-0-8baf2c91a068
            // api_level大于27,截图方法变为:public static Bitmap screenshot(Rect sourceCrop, int width, int height, int rotation) {}
            Rect rect = new Rect(0, 0, 0, 0);
            int rotation = serviceManager.getDisplayManager().getDisplayInfo().getRotation();

            bitmap = (Bitmap) Class.forName(surfaceClassName).getDeclaredMethod("screenshot", new Class[]{Rect.class, Integer.TYPE, Integer.TYPE, Integer.TYPE})
                    .invoke(null, new Object[]{rect, screenWidth, screenHeight, rotation});
        }
    } catch (Throwable e) {
        e.printStackTrace();
    }
    return bitmap;
}

websocket服务器,关键方法onMessage:

@Override
public void onMessage(WebSocket webSocket, String s) {

        if (s.equals( "help")) {
            String help = "screencap, keyevent, mouseevent";
            webSocket.send(help);
        }
       else if(s.equals("screencap")){
            String screencap = ScreenCaptor.getBase64Screencap();

            webSocket.send(screencap);
        }
        else if(s.equals("keyevent")){
            webSocket.send("keyevent cmd.");
        }
        else if(s.contains("mouseevent")) {

            String[] cmds = s.split("#");

            Input input = new Input();
            int inputSource = InputDevice.SOURCE_TOUCHSCREEN;
            input.sendTap(inputSource, Float.parseFloat(cmds[2]),
                    Float.parseFloat(cmds[3]));
            webSocket.send("mouseevent success.");
        } else{
            webSocket.send("Unknown cmd: " + s);
        }
}

前端js关键代码:

var screen = document.getElementById('screen');
var websocket = '';

var x = 0;
var y = 0;

var downTimestamp = 0;
var upTimestamp = 0;

if (window.WebSocket) {
    websocket = new WebSocket(encodeURI('ws://localhost:8888'));
    websocket.onopen = function() {
        console.log('已连接');
    };
    websocket.onerror = function() {
        console.log('连接发生错误');
        alert("websocket连接失败")
    };
    websocket.onclose = function() {
        console.log('已经断开连接');
    };
    // 消息接收
    websocket.onmessage = function(message) {
        websocket.send("screencap");
        screen.src =  message.data; 
    };
} else {
    alert("该浏览器不支持websocket。<br/>建议使用高版本的浏览器,<br/>如 IE10、火狐 、谷歌  、搜狗等");
}

标识符主要用来给类、变量、方法指定名称。
标识符的命名规则:

  • 标识符以字母、美元符号$、或者下划线来开头,其后可以跟上字母、美元符号$、下划线、数字来组合。
  • 标识符不能包含关键字。
  • 标识符大小写敏感,大写A和小写a的变量不是同一标识符。

去年写过关于安卓运行dex的文章 https://testerhome.com/topics/9649
那时候觉得,哇,真是好东西。最近研究minicap(C++),vysor(反射调用screenshot)这些同屏工具,想造个轮子试试:dex+反射截屏。

0x00.截屏核心代码:

参考安卓源码:

/**
反射大法调用Surface|SurfaceControl的screenshot方法
参考安卓源码:
sdk >  17: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/SurfaceControl.java
sdk <= 17: https://android.googlesource.com/platform/frameworks/base/+/android-4.2.2_r1.2/core/java/android/view/Surface.java
*/
public static Bitmap screecap(int screenWidth, int screenHeight){

  String surfaceClassName = " ";

  if (Build.VERSION.SDK_INT <= 17) {
    surfaceClassName = "android.view.Surface";
  } else {
    surfaceClassName = "android.view.SurfaceControl";
  }

  // 关键在于此处反射调用获取bitmap
  Bitmap bitmap = (Bitmap) Class.forName(surfaceClassName).getDeclaredMethod("screenshot", new Class[]{Integer.TYPE, Integer.TYPE}).invoke(null, new Object[]{picWidth, picHeight});
  return bitmap;
}

0x01.bitmap转换为图片

压缩bitmap为jpg|png图片,写入sd卡或其他合适路径即可。

代码烂就不贴了,后面提供一个demo,可以尝试下

0x02.拓展:浏览器看图

java实现一个简单的HTTP server,把图片base64编码, 嵌在html中自动刷新,然后使用adb forward重定向绑定到电脑的某一端口,浏览器访问。

//截屏转base64字符串
public static String cap2base64(){
    int picWidth = 1080;
    int picHeight = 1920;
    String result = "";

    System.out.println("Starting screen capture...");

    long startTime = System.currentTimeMillis();

    String surfaceClassName = " ";
    if (Build.VERSION.SDK_INT <= 17) {
        surfaceClassName = "android.view.Surface";
    } else {
        surfaceClassName = "android.view.SurfaceControl";
    }

    try {
        Bitmap bitmap;
        bitmap = (Bitmap) Class.forName(surfaceClassName).getDeclaredMethod("screenshot", new Class[]{Integer.TYPE, Integer.TYPE}).invoke(null, new Object[]{picWidth, picHeight});

        System.out.println(bitmap.getWidth() + "x" + bitmap.getHeight());
        System.out.println(bitmap.toString());

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);

        baos.flush();
        baos.close();

        byte[] bitmapBytes = baos.toByteArray();
        result = Base64.encodeToString(bitmapBytes, Base64.DEFAULT);

        long endTime = System.currentTimeMillis();
        System.out.println("Cost: " + (endTime - startTime) + "ms");
        System.out.println("Screen capture finished.");

    } catch (IllegalAccessException e) {
        System.out.println("1 error");
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        System.out.println("2 error");
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        System.out.println("3 error");
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        System.out.println("4 error");
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

//http server
public void startServer(int port){
    try {
        try (ServerSocket ss = new ServerSocket(port)) {
            while (true) {
                Socket socket = ss.accept();
                
                PrintWriter pw = new PrintWriter(socket.getOutputStream());

                pw.println("HTTP/1.1 200 OK");
                pw.println("Content-type:text/html");
                pw.println();
                pw.println("<head>" +
                        "<meta charset=\"utf-8\"/>" +
                        "<meta http-equiv=\"refresh\" content=\"0.25\">" +
                        "<title>Android Screen Mirror</title>" +
                        "</head>");
                pw.println("<h2>A screen mirror tool for android by Wanyor.</h2>" );

                Utils u = new Utils();
                String imgBase64 = u.cap2base64();
                pw.println("<img src=\"data:image/png;base64," + imgBase64 + "\" width=\"480\" height=\"800\"/>");

                pw.flush();
                socket.close();
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

0x03.demo