Android: 移植Python-3.10.2 到Android平台
背景目的
- 公司内网实现移动设备自动化测试
- 个人瞎折腾、积累学习Android和Python以及编译知识
准备工作
- 一台Linux x64电脑或虚拟机(VirtualBox),推荐Deepin、Ubuntu、CentOS等
- 下载Android NDK r23:NDK下载
- 下载Python3.10.2源码:Python3.10.2源码下载
安装配置
开始编译
- 啊
- 啊
- 啊
原理简介:
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+反射截屏。
参考安卓源码:
/**
反射大法调用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;
}
压缩bitmap为jpg|png图片,写入sd卡或其他合适路径即可。
代码烂就不贴了,后面提供一个demo,可以尝试下
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();
}
}
APP的接口有两套环境,需要配置hosts文件才能生效。配置方法如下,特意记录下。
adb root
adb remount
adb push hosts /etc/hosts
hosts文件使用以下格式
ip 域名
ip 域名
ip 域名
ip 域名
127.0.0.1 www.baidu.com
# “#”符号可以注释掉一行,使此行不生效
注意:
最新测试配置测试服务器,需要用到切换hosts文件来切换服务器IP。有以下几种办法来清除手机的dns缓存:
//开启飞行模式
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
办法就这么多,各取所需。