Android双屏驱动Service架构实现

来源:互联网 发布:caxa机械画图软件 编辑:程序博客网 时间:2024/05/23 16:59

做程序员苦逼的地方就在于,当公司决定做什么的时候,是不会跟你商量的,只会跟你说,xxx,这个可能需要你来实现一下。fuck,想好实现思路了吗?(这是我司的程序员提出,我们来做整理完善的)

Android双屏显示,可能会和别的双屏机制不同,大多数情况下是一个android系统,分主副屏而已。我司的硬件是两个android系统,两个屏幕,内部通过一根usb直连(这根usb连接线很稳定,代工厂和我们讲的,坑~)。双屏运行两个独立的android系统,相互通过一条底层的通道传输数据,主屏通常可以运行业务软件。副屏可以显示宣传图片,视频,购物清单等信息,但不限于这些,实际上副屏也是个可以触摸交互的系统。

在这样的硬件前提下,我们开发需要实现这两个屏幕的通信,涉及到usb的驱动开发(由代工厂搞定),我们只需要调用jni的一些方法即可。上层应用之间的通信,类似于广播,主副屏可以相互发送接收,提供公共的api,可供其他的app调用,使之能实现自己的业务逻辑。

这里写图片描述

本片文章主要讲底层的Service的实现(也就是驱动的上一层)

思路

双屏通信,主屏会要求副屏显示一些文字(命令),图片(发送文件+命令),图文混合,甚至会发送一些音频,视频等大文件,几百M到几G不等。因为是双向通信,主屏发送指令过后,需要等待副屏的回调。通常如果是命令或者是小文件,毫秒级别内就能被处理掉。但如果是几个G的大文件呢?时间就被延时了,如果这个时候再有命令发送过来了,就会等待(需要维护一个任务列表),这样肯定是不好的。所以我们切割文件,将文件分包,一个一个的发送,最后拼装还原,这样即使中途了命令或者小文件,也能立马被处理掉。

我们有一个任务队列,service不断的去任务队列去轮询,取到任务,根据Task信息,区分是任务类别,做相应的处理。如果是文件的话,我们进行分包处理,这里,我暂定义的最大单个文件包为512kb,然后发送。副屏接收,拼装,还原(每个包有相应的头信息),在给主屏反馈结果,主屏做相应处理。

大致的一个流程图:

这里写图片描述

协议与机制

其实在整个流程中,我们主要要区分任务的来源,以及之后要反馈的源头,分包与还原包不能错乱,不然会产生脏数据。

那我们定义一下任务的类型:FileTask(文件任务),MemoryTask(内存任务,字符串之类的),ControlTask(控制任务)。对不同的任务类型处理是不同的,有的直接是内存传输,有的是写本地文件。

之前在usb通道还没有连通的时候,我们是用UDP协议来写demo实现的,在usb通来以后,直接改用就可以来。所以说,即使没有这个usb通道,你也可以用udp连接来测试两个手机直接的传输,只是这个速度就依赖于网络了。

具体实现

我们有一个底层service,CoreService,所有的发送、接收,回调都是靠它来实现的。那我们就具体围绕它来展开。

    @Override    public void onCreate() {        mSerialPort = new SerialPort();        connectRunable = new Connect();        new Thread(connectRunable).start();    }

SerialPort类里面是一些native的方法,通过jni来调用底层的usb驱动的,这个就略过来,各家的都是不一样的。我们只要知道它有读写的方法即可。

    public native int read(int fd, byte[] data, int offset, int len);    public native int write(int fd, byte[] data, int offset, int len);

Connect类是连接操作,副屏向主屏发起连接的请求。

class Connect implements Runnable {        @Override        public void run() {            id = 0;            // 在建立连接之前,断开之前所有的任务            if (mapSend != null) {                for (Integer i : mapSend.keySet()) {                    for (SendTask sendTask : mapSend.get(i).tasksFile) {                        try {                            sendTask.setTaskState(TaskState.error_sendData);                            sendTask.getCallback().sendError(sendTask.getID(), sendTask.getTaskState().get_code(),                                    sendTask.getTaskState().get_message());                        } catch (Exception e) {                        }                    }                    for (SendTask sendTask : mapSend.get(i).tasksMemory) {                        try {                            sendTask.setTaskState(TaskState.error_sendData);                            sendTask.getCallback().sendError(sendTask.getID(), sendTask.getTaskState().get_code(),                                    sendTask.getTaskState().get_message());                        } catch (Exception e) {                        }                    }                }            }            // 重新初始化参数            mapSend = new ConcurrentHashMap<Integer, SendProcess>();            mapRecv = new ConcurrentHashMap<Integer, RecvTask>();            controlQueue = new ConcurrentLinkedQueue<ControlTask>();            finshAndWait = new HashMap<Integer, Task>();            errorSendTaskID = new HashSet<Integer>();            errorRecvTaskID = new HashSet<Integer>();            // 对象锁            lock = new Object();            // 线程控制锁            controlThreadLock = new Object();            controlProcess = new SendProcess(errorSendTaskID, controlThread, finshAndWait);            mapSend.put(-1, controlProcess);            controlThread = new ControlThread(controlQueue, controlProcess, lock, controlThreadLock, finshAndWait,                    errorSendTaskID);            controlProcess.setControlThread(controlThread);            controlThread.start();            while (true) {                // usb端口打开                if (mSerialPort.tryOpen(isMain)) {                    // isMain true:表示主屏 false:表示副屏                    if (isMain) {                        byte[] temp = new byte[1];                        boolean flag = false;                        do {                            // 读取建立请求连接的数据 -1                            if (mSerialPort.read(mSerialPort.mFd, temp, 0, 1) <= 0) {                                flag = true;                                break;                            } else if (temp[0] != -1 && temp[0] != -2) {                                flag = true;                                break;                            } else if (temp[0] == -2) {                                flag = false;                                break;                            }                            // 向副屏发送建立请求连接的数据 -1                            if (mSerialPort.write(mSerialPort.mFd, temp, 0, 1) <= 0) {                                flag = true;                                break;                            }                        } while (temp[0] == -1);                        if (flag) {                            continue;                        }                    } else {                        // 向主屏发送建立请求连接的数据 -1                        sendsyn = new sendSYN();                        sendsynThread = new Thread(sendsyn);                        sendsynThread.start();                        try {                            Thread.sleep(1000);                        } catch (InterruptedException e) {                            e.printStackTrace();                        }                        byte[] temp = new byte[1];                        mSerialPort.clear(mSerialPort.mFd);                        if (mSerialPort.read(mSerialPort.mFd, temp, 0, 1) <= 0) {                            sendsyn.gh = false;                            continue;                        } else if (temp[0] != -1) {                            sendsyn.gh = false;                            continue;                        }                        sendsyn.gh = false;                        try {                            sendsynThread.join();                        } catch (InterruptedException e) {                            e.printStackTrace();                        }                        temp[0] = -2;                        // 读取建立请求连接的数据 -1                        if (mSerialPort.write(mSerialPort.mFd, temp, 0, 1) <= 0) {                            sendsyn.gh = false;                            continue;                        }                    }                    // 发送处理                    transferSend = new TransferSend(lock, mapSend, mSerialPort, handler);                    // 接收处理                    transferRecv = new TransferRecv(mapRecv, mSerialPort, controlThread, errorRecvTaskID, handler);                    transferRecv.start();                    transferSend.start();                    // 发送handler表示连接成功                    handler.sendMessage(handler.obtainMessage(-2));                    break;                } else {                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }            isConnecting = false;        }    }

代码有点多,来分析一下初始化的整个连接过程。

    // 发送task的map列表    private ConcurrentHashMap<Integer, SendProcess> mapSend;    // 接收task的map列表    private ConcurrentHashMap<Integer, RecvTask> mapRecv;

1.首先,在建立连接之前,断开之前所有的任务,Task的TaskState.error_sendData就是表示usb数据通道断开,取消所有发送接收的任务。

2.初始化参数。包括发送,接收的列表,锁对象,任务id等,ControlThread类是一个线程控制类,来处理task的。SendProcess类是发送task的包装类,把它丢到ControlThread类去处理,然后开启Thread,让它不停的去轮询任务队列。

3.开始建立发送与接收的连接。isMain这个字段,true表示主屏,false表示副屏。先来看副屏的代码,初始化一个sendSYN类。它向主屏发送一个为-1字节的数据,然后如果主屏收到一个为-1的数据,就表示这是副屏发起的连接请求(正常的数据请求是不可能为-1 的)。主屏收到以后,也向副屏发送一个-1字节的数据,如果副屏也收到来这个数据,表示双屏建立连接成功。

连接通信成功,就可以相互发送数据了。来看下定义的Task这个类。

这里写图片描述

这里面是一些数据信息,hasSendedLength,hasRecvLength,fileLength用来分包,还原包的,sender表示发送者,成功或失败要反馈发送者…

TaskType是一个枚举

这里写图片描述

分为文件任务,内存任务,控制任务三种。

那我们是如何向副屏发送任务的呢?是通过服务的aidl来发送的和接收回调信息的。

// SendService.aidlinterface SendService {    int sendFileToFile(in String recvPackageName,in String path,boolean isReport, long userFlag,in SendServiceCallback callback);    int sendByteToMemory(in String recvPackageName,in byte [] data,in SendServiceCallback callback);}

在CoreService的onBind()方法中,返回了此对象。现在来看下SendService的具体实现。

    @Override    public IBinder onBind(Intent intent) {        if (callback == null) {            callback = new CallBack();        }        return callback;    }    private class CallBack extends SendService.Stub {        @Override        public int sendFileToFile(String recvPackageName, String path, boolean isReport, long userFlag,                SendServiceCallback callback) throws RemoteException {            SendProcess process = mapSend.get(Binder.getCallingUid());            if (process == null) {                process = new SendProcess(errorSendTaskID, controlThread, finshAndWait);                mapSend.put(Binder.getCallingUid(), process);            }            int tem_id = getID();            synchronized (lock) {                process.addTask(new FileTask(tem_id,                        getApplicationContext().getPackageManager().getNameForUid(Binder.getCallingUid()),                        recvPackageName, path, isReport, userFlag, callback));                lock.notify();            }            return tem_id;        }        @Override        public int sendByteToMemory(String recvPackageName, byte[] data, SendServiceCallback callback)                throws RemoteException {            SendProcess process = mapSend.get(Binder.getCallingUid());            if (process == null) {                process = new SendProcess(errorSendTaskID, controlThread, finshAndWait);                mapSend.put(Binder.getCallingUid(), process);            }            int tem_id = getID();            synchronized (lock) {                process.addTask(new MemoryTask(tem_id,                        getApplicationContext().getPackageManager().getNameForUid(Binder.getCallingUid()),                        recvPackageName, data, callback));                lock.notify();            }            return tem_id;        }    }

主要是从map队列中取出客户端任务,放进任务列表,唤醒处理线程,执行任务。主要发送任务是SendProcess类,其中我们定义了sendM()和sendF()方法,来发送字符串命令和文件。

这里写图片描述

这里写图片描述

最终都通过task的send()方法来发送,其实也就是前面所说的SerialPort中的write()这个jni方法。

这里写图片描述

其中fillData()方法,也就是前面所有的给Task填充数据,包括请求头,大小,描述等。

下面我们再来看一下接收的方法,也是前面所说的SerialPort的read()来读取发送过来的数据,通过aidl回调主屏。其中的回调callback在任务创建的时候传入的,在控制线程和发送线程中对其作出相应的回调处理。

这里写图片描述

// SendServiceCallback.aidlinterface SendServiceCallback {    oneway void sendSuccess(int id);    oneway void sendError(int id,int  errorId, String errorInfo);    oneway void sendProcess(int id, long totle, long sended);}

获取到数据,进行拼装还原,取出任务的携带信息,进行分类处理。

            byte flag = data[4];            int taskId = ByteUtil.bytes2Int(data, 5);            fileTaskRecv = (RecvTask) mapRecv.get(taskId);            int sendPackNameLength = ByteUtil.bytes2Int(data, 9);            int recvPackNameLength = ByteUtil.bytes2Int(data, 13 + sendPackNameLength);            if (fileTaskRecv == null) {                if (errorTaskID.contains(taskId)) {                    return;                }                String sendPackName = new String(data, 13, sendPackNameLength, "utf-8");                String recvPackName = new String(data, 17 + sendPackNameLength, recvPackNameLength, "utf-8");                int timeOut = ByteUtil.bytes2Int(data, 17 + recvPackNameLength + sendPackNameLength);                long fileLength = ByteUtil.bytes2long(data, 21 + recvPackNameLength + sendPackNameLength);                long userFlag = ByteUtil.bytes2long(data, 29 + recvPackNameLength + sendPackNameLength);

最后通过Service中的Handler来回调处理,其实是通过广播发送出去的,所以需要接收双屏通信信息的app都要注册该广播。

这里写图片描述

存在问题与改进空间

这样,双屏通信的大致流程就已经说完了,其中有一些需要补充和完善的地方,因为项目紧急,所以第一版上线的也比较粗糙,后续会陆续改进的。但我想,不管是怎样改,分包,发送,接收,回调,原包这样逻辑应该是通用的。

问题:1.接收到的文件没有处理,因为sdcard的内存是有限的。
2.如果连接断开(像强行关机之类),失败的任务是不能复原的。在初始化的时候,我们把所有以前的任务都当作失败任务放弃掉了。这里其实可以做一些缓存处理,在拿出未完成的任务,在重连的时候继续处理。

还有一些东西可能还没有考虑到,在后面无尽的需求中,也许会增加上去。

Beating Heart !
0 0