最近在用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();
        }
    }

}