在Java项目中使用ADB操作安卓手机
2021-03-17
1,425 views
14 min read
最近在用java写一个手游脚本框架,采用识图的方式处理脚本逻辑。与安卓手机的交互的话最方便的可能就是ADB了。
可以用命令行的方式操作ADB,但这显然太麻烦且编码困难,尝试过自己封装但是封装的不太好,经过搜索,发现有Google自己做的一个包ddmlib
,专门用来操作ADB,从建立连接到各种命令都非常齐全。
我在Maven仓库中找到了这个包,但是需要注意的是,Central
仓库只更新到了25.3.0
,如果你的手机是安卓7以及以上的版本的话,那么使用这个包截图(takeScreenshot()
)会抛出错误:Unsupported protocol: 2
。要使用更新版本的话需要切换到Google
的仓库。
Maven依赖
...
<repositories>
<!-- 添加Google仓库 -->
<repository>
<id>Google</id>
<url>https://maven.google.com/</url>
</repository>
</repositories>
...
<dependencies>
...
<!-- https://mvnrepository.com/artifact/com.android.tools.ddms/ddmlib -->
<dependency>
<groupId>com.android.tools.ddms</groupId>
<artifactId>ddmlib</artifactId>
<version>26.6.4</version>
</dependency>
...
</dependencies>
...
简单使用
这段代码涵盖了做手游脚本的基本操作,识图触摸以及输入。
package com.sakuradon.mahoutsukai.android;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.NullOutputReceiver;
import com.google.common.base.Stopwatch;
import com.sakuradon.mahoutsukai.config.Config;
import com.sakuradon.mahoutsukai.exception.Exceptions;
import com.sakuradon.mahoutsukai.log.Logger;
import com.sakuradon.mahoutsukai.log.LoggerFactory;
import com.sakuradon.mahoutsukai.utils.ImageUtil;
import com.sakuradon.mahoutsukai.utils.StringUtil;
import java.awt.image.BufferedImage;
import java.util.concurrent.TimeUnit;
/**
* @author SakuraDon
*/
public class Adb {
private static final Long ADB_CREATE_TIMEOUT = 60000L;
private static final Logger LOGGER = LoggerFactory.createLogger(Adb.class);
private static final IShellOutputReceiver NULL_RECEIVER = new NullOutputReceiver();
private final String deviceName;
private final IDevice device;
private final Config config;
private Adb(String deviceName, IDevice device, Config config) {
this.deviceName = deviceName;
this.device = device;
this.config = config;
}
public String getDeviceName() {
return deviceName;
}
public void tap(int x, int y) {
shell("input tap " + x + " " + y);
}
public void swipe(int x1, int y1, int x2, int y2, int ms) {
shell("input swipe " + x1 + " " + y1 + " " + x2 + " " + y2 + " " + ms);
}
public void text(String text) {
shell("input text " + text);
}
public void key(int k) {
shell("input keyevent " + k);
}
public void shell(String script) {
Stopwatch stopwatch = Stopwatch.createStarted();
try {
device.executeShellCommand(script, NULL_RECEIVER);
LOGGER.trace("adb exec <%s> cost <%d> ms", script, stopwatch.elapsed(TimeUnit.MILLISECONDS));
} catch (Exception e) {
e.printStackTrace();
throw Exceptions.ADB_EXEC_FAILED;
}
}
public BufferedImage takeScreenshot() {
Stopwatch stopwatch = Stopwatch.createStarted();
try {
BufferedImage image = ImageUtil.convertImage(device.getScreenshot());
if (image == null) {
throw Exceptions.ADB_TAKE_SCREENSHOT_FAILED;
}
LOGGER.trace("adb take screenshot cost <%d> ms", stopwatch.elapsed(TimeUnit.MILLISECONDS));
return image;
} catch (Exception e) {
throw Exceptions.ADB_TAKE_SCREENSHOT_FAILED;
}
}
public static Adb createAdb(String deviceName, Config config) {
LOGGER.info("creating adb...");
AndroidDebugBridge.init(false);
AndroidDebugBridge bridge = AndroidDebugBridge.createBridge(config.getAdbPath(), false);
Stopwatch stopwatch = Stopwatch.createStarted();
while (!bridge.hasInitialDeviceList() && stopwatch.elapsed(TimeUnit.MILLISECONDS) < ADB_CREATE_TIMEOUT) {
try {
Thread.sleep(50L);
} catch (InterruptedException var5) {
throw Exceptions.ADB_CREATE_FAILED;
}
}
if (!bridge.hasInitialDeviceList()) {
throw Exceptions.ADB_CREATE_FAILED;
}
LOGGER.debug("adb created, finding devices...");
IDevice[] devices = bridge.getDevices();
if (devices.length == 0) {
throw Exceptions.ADB_COULD_NOT_FIND_DEVICE;
}
IDevice device = null;
StringBuilder devicesSb = new StringBuilder();
if (StringUtil.isBlank(deviceName)) {
if (devices.length > 1) {
throw Exceptions.ABD_ONE_MORE_DEVICES;
}
device = devices[0];
} else {
for (IDevice iDevice : devices) {
devicesSb.append(iDevice.getName());
devicesSb.append(";");
if (iDevice.getName().equals(deviceName)) {
device = iDevice;
break;
}
}
}
if (device == null) {
LOGGER.warn("find devices: <%s>", devicesSb.toString());
throw Exceptions.ADB_COULD_NOT_FIND_DEVICE;
}
Adb adb = new Adb(deviceName, device, config);
LOGGER.trace("create adb cost <%d> ms", stopwatch.elapsed(TimeUnit.MILLISECONDS));
return adb;
}
public static void disconnectAdb() {
LOGGER.debug("terminate adb");
AndroidDebugBridge.terminate();
}
}
代码75行使用到了ImageUtil.convertImage()
方法,ddmlib
截图方法返回的是自己的RawImage
类,这里将其进行转换成Java里常用的BufferedImage
类。
package com.sakuradon.mahoutsukai.utils;
import com.android.ddmlib.RawImage;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.util.Hashtable;
/**
* @author SakuraDon
*/
public class ImageUtil {
private static final Hashtable<?, ?> EMPTY_HASH = new Hashtable<>();
private static final int[] BAND_OFFSETS_32 = {0, 1, 2, 3};
private static final int[] BAND_OFFSETS_16 = {0, 1};
public static BufferedImage convertImage(RawImage rawImage) {
if (rawImage.bpp == 16) {
return rawImage16toARGB(rawImage);
}
if (rawImage.bpp == 32) {
return rawImage32toARGB(rawImage);
}
return null;
}
static int getMask(int length) {
int res = 0;
for (int i = 0; i < length; i++) {
res = (res << 1) + 1;
}
return res;
}
private static BufferedImage rawImage32toARGB(RawImage rawImage) {
DataBufferByte dataBuffer = new DataBufferByte(rawImage.data,
rawImage.size);
PixelInterleavedSampleModel sampleModel = new PixelInterleavedSampleModel(
0, rawImage.width, rawImage.height, 4, rawImage.width * 4,
BAND_OFFSETS_32);
WritableRaster raster = Raster.createWritableRaster(sampleModel,
dataBuffer, new Point(0, 0));
return new BufferedImage(new ThirtyTwoBitColorModel(rawImage), raster,
false, EMPTY_HASH);
}
private static BufferedImage rawImage16toARGB(RawImage rawImage) {
DataBufferByte dataBuffer = new DataBufferByte(rawImage.data,
rawImage.size);
PixelInterleavedSampleModel sampleModel = new PixelInterleavedSampleModel(
0, rawImage.width, rawImage.height, 2, rawImage.width * 2,
BAND_OFFSETS_16);
WritableRaster raster = Raster.createWritableRaster(sampleModel,
dataBuffer, new Point(0, 0));
return new BufferedImage(new SixteenBitColorModel(), raster,
false, EMPTY_HASH);
}
private static class SixteenBitColorModel extends ColorModel {
private static final int[] BITS = { 8, 8, 8, 8 };
public SixteenBitColorModel() {
super(32, BITS, ColorSpace.getInstance(1000), true, false, 3, 0);
}
@Override
public boolean isCompatibleRaster(Raster raster) {
return true;
}
private int getPixel(Object inData) {
byte[] data = (byte[]) inData;
int value = data[0] & 0xFF;
value |= data[1] << 8 & 0xFF00;
return value;
}
@Override
public int getAlpha(Object inData) {
return 255;
}
@Override
public int getBlue(Object inData) {
int pixel = getPixel(inData);
return (pixel & 0x1F) << 3;
}
@Override
public int getGreen(Object inData) {
int pixel = getPixel(inData);
return (pixel >> 5 & 0x3F) << 2;
}
@Override
public int getRed(Object inData) {
int pixel = getPixel(inData);
return (pixel >> 11 & 0x1F) << 3;
}
@Override
public int getAlpha(int pixel) {
throw new UnsupportedOperationException();
}
@Override
public int getBlue(int pixel) {
throw new UnsupportedOperationException();
}
@Override
public int getGreen(int pixel) {
throw new UnsupportedOperationException();
}
@Override
public int getRed(int pixel) {
throw new UnsupportedOperationException();
}
}
private static class ThirtyTwoBitColorModel extends ColorModel {
private static final int[] BITS = { 8, 8, 8, 8 };
private final int alphaLength;
private final int alphaMask;
private final int alphaOffset;
private final int blueMask;
private final int blueLength;
private final int blueOffset;
private final int greenMask;
private final int greenLength;
private final int greenOffset;
private final int redMask;
private final int redLength;
private final int redOffset;
public ThirtyTwoBitColorModel(RawImage rawImage) {
super(32, BITS, ColorSpace.getInstance(1000), true, false, 3, 0);
this.redOffset = rawImage.red_offset;
this.redLength = rawImage.red_length;
this.redMask = getMask(this.redLength);
this.greenOffset = rawImage.green_offset;
this.greenLength = rawImage.green_length;
this.greenMask = getMask(this.greenLength);
this.blueOffset = rawImage.blue_offset;
this.blueLength = rawImage.blue_length;
this.blueMask = getMask(this.blueLength);
this.alphaLength = rawImage.alpha_length;
this.alphaOffset = rawImage.alpha_offset;
this.alphaMask = getMask(this.alphaLength);
}
@Override
public boolean isCompatibleRaster(Raster raster) {
return true;
}
private int getPixel(Object inData) {
byte[] data = (byte[]) inData;
int value = data[0] & 0xFF;
value |= (data[1] & 0xFF) << 8;
value |= (data[2] & 0xFF) << 16;
value |= (data[3] & 0xFF) << 24;
return value;
}
@Override
public int getAlpha(Object inData) {
int pixel = getPixel(inData);
if (this.alphaLength == 0) {
return 255;
}
return (pixel >>> this.alphaOffset & this.alphaMask) << 8 - this.alphaLength;
}
@Override
public int getBlue(Object inData) {
int pixel = getPixel(inData);
return (pixel >>> this.blueOffset & this.blueMask) << 8 - this.blueLength;
}
@Override
public int getGreen(Object inData) {
int pixel = getPixel(inData);
return (pixel >>> this.greenOffset & this.greenMask) << 8 - this.greenLength;
}
@Override
public int getRed(Object inData) {
int pixel = getPixel(inData);
return (pixel >>> this.redOffset & this.redMask) << 8 - this.redLength;
}
@Override
public int getAlpha(int pixel) {
throw new UnsupportedOperationException();
}
@Override
public int getBlue(int pixel) {
throw new UnsupportedOperationException();
}
@Override
public int getGreen(int pixel) {
throw new UnsupportedOperationException();
}
@Override
public int getRed(int pixel) {
throw new UnsupportedOperationException();
}
}
}
Previous Post
React mobx无法触发重新渲染
Next Post
MybatisPlus分页插件无效,total和pages始终为0
Or you can contact me by Email