分类 Android 下的文章

原理简介:
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、火狐 、谷歌  、搜狗等");
}

在使用 WebView 当加载网页时,默认会调用系统的默认外部浏览器来加载页面,原因是因为 WebViewClient 中的 shouldOverrideUrlLoading 方法默认返回为false。
要使用内部的 WebView 加网页就要重写 shouldOverrideUrlLoading 方法,使其返回 true。

webView = (WebView) findViewById(R.id.webView);
webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    view.loadUrl(url);
    return true;
    }
});
webView.loadUrl("http://www.baidu.com");

去年写过关于安卓运行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

APP的接口有两套环境,需要配置hosts文件才能生效。配置方法如下,特意记录下。

准备工作:

  • 已root的手机
  • hosts文件
  • 电脑已配置好adb

配置步骤:

  1. 手机连接电脑,使用以下命令挂载安卓根文件系统为root模式;

adb root
adb remount

  1. 使用以下命令推送hosts文件到指定目录;

adb push hosts /etc/hosts

  1. 打开然后关闭飞行模式即可及时生效hosts配置。

知识补充:

hosts文件使用以下格式

ip 域名
ip 域名
ip 域名
ip 域名
127.0.0.1 www.baidu.com
# “#”符号可以注释掉一行,使此行不生效

注意:

  • hosts文件后面一定要留空行,不然会不生效
  • hosts文件编码一定要使用utf-8编码

最新测试配置测试服务器,需要用到切换hosts文件来切换服务器IP。有以下几种办法来清除手机的dns缓存:

  • 经典:重启手机
  • 简单:开关一下飞行模式
  • 砖家:(手机需root权限,使用以下命令清除

//开启飞行模式
adb shell settings put global airplane_mode_on 1
adb shell am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true
//关闭飞行模式
adb shell settings put global airplane_mode_on 0
adb shell am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false


办法就这么多,各取所需。