PS:本文为本人学习的一个过程,大神直接绕道,若发现有错误之处,请评论留言,小编会及时更正,不喜勿喷,谢谢。

今年年初接到一个项目,是通过串口来和计算机通信,然后实现相关业务功能,所以现在整理一下相关知识点,当作笔记,方便日后回顾。

1. 简介

因为这个项目,有幸接触到一个Android设备,我管它叫Android盒子吧,该项目需求是这样的:通过该Android盒子连接计算机和usb扫码枪,当Android盒子收到对应的指令的时候,做对应的事情,例如收到计算机发过来的扣款指令时就调起扫码枪扫描付款码,然后请求支付网关进行扣款。

说明:  1. Android盒子是通过usb转串口线和计算机相连的,通信也是通过串口来进行通信;  2. Android盒子通过usb接口外接了一个usb扫码枪,用于扫码。

1.1 设备介绍

1.1.1 Android盒子介绍

1. 硬件介绍:
该设备自带有3个USB接口,1个Micro USB接口(Android手机充电、数据接口,非TypeC接口),1个预留按钮,1个电源键,3个指示灯,1个电源接口,1个RJ45网口,1个HDMI接口,1个串口公头接口。

视图1:

Android串口盒子+扫码枪开发_第1张图片 Android盒子接口1.jpg
接口说明:

  1. 串口:Android盒子和计算机的通讯接口;
  2. HDMI接口:连接显示器的通讯接口;
  3. RJ45网线口:连接互联网的通讯接口(也可通过该接口与计算机通信);
  4. 电源接口:Android盒子的供电接口。

视图2:

Android盒子接口2.jpg
说明:

  1. 电源指示灯:显示Android盒子供电情况,亮红灯为正常情况;
  2. 预留指示灯:预留指示灯,尚未使用;
  3. 状态灯:Android盒子状态灯,每分钟亮一次,若Android盒子异常(网络异常),则Android盒子会发出“滴滴滴”响声;
  4. 电源键:Android盒子开机键,设备每次加电时需长按按钮2-3秒,直到电源指示灯亮起;
  5. 预留按钮:预留按钮,尚未使用;
  6. 数据接口:Android盒子与外界设备交互接口;
  7. USB接口:连接扫码枪等设备的接口。

2. 软件介绍
Android盒子是里面装了Android系统的一个终端设备,不过该系统具有root权限。

1.1.2 扫码枪介绍

扫码枪:

Android串口盒子+扫码枪开发_第2张图片 USB接口扫码枪.jpg
说明:
usb扫码枪无特殊要求或配置,作为一个外设设备,只要通过usb接口连接上Android盒子就可以工作。

1.2 项目介绍

该项目要求做一个App,用于计算机程序和支付网关间的桥梁,通过该App实现获取付款码并向支付网关发起扣费请求,以及根据计算机程序的不同指令处理不同的业务逻辑。
该篇文章主要整理该项目用到以下几个知识点:

扫码程序开机启动

  1. 因为这个Android盒子正常使用的时候是不接屏幕的,所以扫码程序也没有界面,操作人员也不能点击扫码程序图标让扫码程序启动,所以扫码程序就要设置成开机自动启动。

日志上传程序

  1. 考虑到如果这个设备如果要用到一些其他地方(例如其他城市的超时等,反正不在身边)的时候,程序出现bug,需要获取那个盒子的日志来分析问题,所以就开发了一个功能比较简单的日志上传程序,日志上传程序的主功能是把扫码程序的日志上传到服务器,还有另一个功能是监听扫码程序,当扫码程序因内存回收被杀了或者因为有bug而崩溃等等情况下重启启动扫码程序,又或者当扫码程序不在前台的时候把扫码程序调回前台运行。

扫码枪开发:

  1. 获取扫码枪扫到的内容并解析出来。

Android串口编程:

  1. Android串口编程介绍。
  2. 开源项目android-serialport-api的原理和实现。

2. 扫码程序开机启动

2.1 原理:

在Android系统启动时,会发出一个系统广播 ACTION_BOOT_COMPLETED,它的字符串常量表示为 “android.intent.action.BOOT_COMPLETED”。我们要做的就是写一个广播接收器BroadcastReceiver,广播接收器中写你启动的Activity或者Service等的代码,然后静态注册这个广播接收器,添加BOOT_COMPLETED这个action,当然,别忘记添加相应的权限。完成上述步骤之后,当系统开机发出系统广播 ACTION_BOOT_COMPLETED的时候,我们的程序就会“捕捉”到这个广播,然后执行我们广播中的代码,启动我们的Activity或者service等。

2.2 实现:

  1. 添加权限

     
  2. StartUpBroadcast

     public class StartUpBroadcast extends BroadcastReceiver {     @Override     public void onReceive(Context context, Intent intent) {         Intent intent1 = new Intent(context, MainActivity.class);         intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);         context.startActivity(intent1);       } }
  3. 静态注册广播接收器

                                  

3. 日志上传程序

3.1 功能

  1. 这个程序的主要业务逻辑都在一个服务(service)中,要做到时刻监听扫码程序,需要做到自己不被杀掉,所以这个服务是做了“杀不死”的处理。
  2. 上传扫码程序的本地日志到服务器。
  3. 监听扫码程序,当扫码程序不在前台运行/扫码程序被杀掉的时候把扫码程序调回前台运行/重新启动扫码程序。至于为什么扫码程序一定要在前台运行,后面的扫码枪开发章节再解释。

3.2 原理

3.2.1 保证service不被杀掉

在onStartCommand方法,返回START_STICKY。手动返回START_STICKY,当service因内存不足被杀掉,当内存又有的时候,service又会被重新创建,但是不能保证任何情况下都被重建,比如进程被干掉了的情况,具体原理请看下面的介绍。

StartCommond几个常量参数简介:

  1. START_STICKY
    在运行onStartCommand后service进程被kill后,那将保留在开始状态,但是不保留那些传入的intent。不久后service就会再次尝试重新创建,因为保留在开始状态,在创建 service后将保证调用onstartCommand。如果没有传递任何开始命令给service,那将获取到null的intent。
  2. START_NOT_STICKY
    在运行onStartCommand后service进程被kill后,并且没有新的intent传递给它。Service将移出开始状态,并且直到新的明显的方法(startService)调用才重新创建。因为如果没有传递任何未决定的intent那么service是不会启动,也就是期间onstartCommand不会接收到任何null的intent。
  3. START_REDELIVER_INTENT
    在运行onStartCommand后service进程被kill后,系统将会再次启动service,并传入最后一个intent给onstartCommand。直到调用stopSelf(int)才停止传递intent。如果在被kill后还有未处理好的intent,那被kill后服务还是会自动启动。因此onstartCommand不会接收到任何null的intent。

ps: 保证service不被杀掉的方法有很多,但是在现在各种的定制系统以及杀毒软件手机管家中,不可能保证service不被杀死。因为我的这个项目比较特殊,没有界面,用户操作不了,所以不存在用户主动杀死进程或服务的情况,也不会有系统被定制或安装杀毒软件的情况,Android盒子系统里面也不会安装其他太多的程序,所以也很少会出现内存不足的情况,所以我这里只用到这一个方法就够用了,就是在onStartCommand方法,返回START_STICKY。如果读者想要service不被杀死的更好的方案,可以自行百度,这里也推荐一篇文章:http://blog.csdn.net/mad1989/article/details/22492519

3.2.2 上传扫码程序的本地日志到服务器的功能原理

其实这个也没什么好说的,就是后台提供一个上传文件的接口,日志上传程序这边去拿特定文件夹下的日志文件,然后通过后台接口上传到服务器,我这里用的网络框架是OkHttp。

3.2.3 监听扫码程序原理

在日志上传程序中的onStartCommand方法中开一个定时任务,每隔60秒则检测一下扫码程序是否在栈顶,如果不在栈顶,则发送一个广播,在扫码程序中,当接收到这个广播之后,获得当前运行的task,找到当前应用的task,并启动task的栈顶activity,达到程序切换到前台,若没有找到运行的task,用户结束了task或被系统释放,则重新启动mainactivity

3.3 实现

3.3.1 保证service不被杀掉

@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {    Log.i("onStartCommand","......");    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);    Notification noti = new Notification.Builder(this)            .setContentTitle("Title")            .setContentText("Message")            .setContentIntent(pendingIntent)            .build();    startForeground(12346, noti);    // 这里执行自己的业务逻辑    if (intent != null) {        final String action = intent.getAction();        if (ACTION_BAZ.equals(action)) {            Thread thread = new Thread(new Runnable() {                @Override                public void run() {                    handleActionBaz();                }            });            thread.start();        }    }    //延迟0秒后,每隔60秒执行一次该任务    singleThreadScheduledPool.scheduleWithFixedDelay(new Runnable() {        @Override        public void run() {            String topPackageName = getForegroundApp(getBaseContext());            Log.d("packageName", "topPackageName:"+topPackageName);            // com.gorilla.scanningcodedemo为扫码程序包名            if (!"com.gorilla.scanningcodedemo".equals(topPackageName)) {                Intent intent = new Intent("com.gorilla.scanningcodedemo.RestartAppBroadcast");                sendBroadcast(intent);            }        }    }, 60, 60, TimeUnit.SECONDS);    return START_STICKY;// 重点在这}

3.3.2 上传扫码程序的本地日志到服务器

略。可自行百度OkHttp文件上传功能。

3.3.3 监听扫码程序

日志上传程序中的代码

@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {    Log.i("onStartCommand","......");    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);    Notification noti = new Notification.Builder(this)            .setContentTitle("Title")            .setContentText("Message")            .setContentIntent(pendingIntent)            .build();    startForeground(12346, noti);    // 这里执行自己的业务逻辑    if (intent != null) {        final String action = intent.getAction();        if (ACTION_BAZ.equals(action)) {            Thread thread = new Thread(new Runnable() {                @Override                public void run() {                    handleActionBaz();                }            });            thread.start();        }    }    //延迟0秒后,每隔60秒执行一次该任务    singleThreadScheduledPool.scheduleWithFixedDelay(new Runnable() {        @Override        public void run() {            String topPackageName = getForegroundApp(getBaseContext());            Log.d("packageName", "topPackageName:"+topPackageName);             // com.gorilla.scanningcodedemo为扫码程序包名            if (!"com.gorilla.scanningcodedemo".equals(topPackageName)) {                Intent intent = new Intent("com.gorilla.scanningcodedemo.RestartAppBroadcast");                sendBroadcast(intent);            }        }    }, 60, 60, TimeUnit.SECONDS);    return START_STICKY;// 重点在这}public String getForegroundApp(Context context) {    ActivityManager am =            (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);    List lr = am.getRunningAppProcesses();    if (lr == null) {        return null;    }    for (ActivityManager.RunningAppProcessInfo ra : lr) {        if (ra.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE                || ra.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {            return ra.processName;        }    }    return null;}

扫码程序的代码

  1. 添加权限

     
  2. RestartAppBroadcast

     public class RestartAppBroadcast extends BroadcastReceiver {   @Override   public void onReceive(Context context, Intent intent) {       if ("com.gorilla.scanningcodedemo.RestartAppBroadcast".equals(intent.getAction())) {           //获取ActivityManager           ActivityManager mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);           //获得当前运行的task           List taskList = mAm.getRunningTasks(100);           for (ActivityManager.RunningTaskInfo rti : taskList) {               //找到当前应用的task,并启动task的栈顶activity,达到程序切换到前台               if(rti.topActivity.getPackageName().equals(context.getPackageName())) {                   try {                       Intent  resultIntent = new Intent(context, Class.forName(rti.topActivity.getClassName()));                       resultIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);                       context.startActivity(resultIntent);                       Logger.i("后台->前台");                       LogUtil.startActionFoo("后台->前台");                       return;                   }catch (ClassNotFoundException e) {                       e.printStackTrace();                   }               }           }           //若没有找到运行的task,用户结束了task或被系统释放,则重新启动mainactivity           Intent resultIntent = new Intent(context, MainActivity.class);           resultIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);           context.startActivity(resultIntent);           Logger.i("重新启动");           getConfig();           LogUtil.startActionFoo("终端app重新启动!");       }    } }
  3. 静态注册广播接收器

                         

4. 扫码枪开发

一般的扫码枪设备都相当于一个普通的外接输入设备,像外接键盘一样。扫码枪扫到的内容就相当于用户在键盘输入内容一样,所以要获取扫码枪扫到的内容,就要对键盘事件做出解析。要解析键盘输入的内容,那就要重写Activity的dispatchKeyEvent(KeyEvent event),所以前面说的Activity必须要在前台运行就是这个原因,因为如果Activity不在前台运行,那么dispatchKeyEvent(KeyEvent event)方法就执行不了,就获取不了扫码内容
扫码枪开发中的一些常见问题

  1. 怎么判断扫码枪是否已经连接?
    每个品牌型号的扫码枪都有一个名字,你要判断扫码枪是否已经连接,就获取到Android盒子连接的所有外接设备,遍历,通过名字来匹配,如果匹配到这个扫码枪的名字,则认为是连接上了。
  2. 怎么区分扫描到的结果是一次扫描到的还是多次扫描的结果?
    一般扫码枪扫描一次的内容都会以回车键作为结束符,所以可以判断是否有回车键来区分是否是一次扫描的结果。(可能不同扫码枪有不同的结束标识,我在其他博文中看到有些串口扫码枪,它们有特定的数据结束标识,但是我用过三个品牌的usb扫码枪,都是以回车键作为结束标识)
  3. 当应用到各个城市的Android盒子的扫码枪坏了,要更换扫码枪,又找不到相同牌子的扫码枪怎么办?
    前面说了,我们是通过扫码枪的名字来判断扫码枪是否已经连接的,所以我们可以将这个扫码枪通过配置文件的方式来写到配置文件中,当要更换不同品牌的扫码枪的时候,我们就可以在配置文件中把扫码枪名字配置上去,然后在扫码程序中读取配置文件来获取扫码枪名字,然后和Android盒子连接上的外接设备名字来对比。

4.1 原理

4.1.1 重写Activity的dispatchKeyEvent(KeyEvent event)方法

首先我们来了解一下当按键按下和弹起,activity中回调的一系列方法:
当键盘按下时

  1. 首先触发dispatchKeyEvent
  2. 然后触发onUserInteraction
  3. 再次onKeyDown

如果按下紧接着松开,则是俩步

  1. 紧跟着触发dispatchKeyEvent
  2. 然后触发onUserInteraction
  3. 再次onKeyUp

总结:如果按下接着松开的方法调用顺序是:dispatchKeyEvent-->onUserInteraction-->onKeyDown-->dispatchKeyEvent-->onUserInteraction-->onKeyUp

问题1:为什么不重写onKeyDown或onKeyUp来实现呢?
答:因为onKeyDown和onKeyUp貌似只有activity中可以触发,activityGroup,listActivity,tabActivity好像不好用,所以在dispatchKeyEvent监控按键不管是activity还是activitygroup都会触发

问题2:要在dispatchKeyEvent方法里做什么事?
答:首先,为了避免Android盒子同时接上了usb扫码枪和键盘,导致出现扫码错误的情况,我们要判断一下监听到的事件是否是扫码枪事件,如果不是扫码枪事件的话,就交给super.dispatchKeyEvent(event)来处理,如果是扫码枪事件就解析出扫到的内容

4.1.2 判断是否是扫码枪事件

判断方法:通过dispatchKeyEvent(KeyEvent event)的KeyEvent对象获取到设备的名字,然后和自己的扫码枪设备的名字做比较,是扫码枪事件,则解析扫码内容。

4.1.3 解析扫码枪扫码内容。

解析原理:通过KeyEvent的KeyCode来解析出来,然后通过一个StringBuffer来转换成一串字符串,然后通过判断KeyCode是否为回车键来判断是否扫码结束,扫码结束则把扫到的字符串保存起来使用。

4.2 实现

4.2.1 监听Activity的dispatchKeyEvent(KeyEvent event)方法代码实现

@Overridepublic boolean dispatchKeyEvent(KeyEvent event) {    // mScanGunKeyEventHelper是用于对按键事件做处理的对象,isScanGunEvent(event)是判断是否是扫码枪事件    if (mScanGunKeyEventHelper.isScanGunEvent(event)) {        // analysisKeyEvent(event)解析扫码枪扫到的内容        mScanGunKeyEventHelper.analysisKeyEvent(event);        return true;// 最后消费掉,不要继续传递事件下去    }    return super.dispatchKeyEvent(event);}

4.2.2 判断是否是扫码枪事件代码实现

/** * 是否为扫码枪事件 * * @param event KeyEvent * @return */public boolean isScanGunEvent(KeyEvent event) {    boolean isDeviceExist = false; // 是否是扫码枪设备    for (String deviceName : mDeviceName) {// mDeviceName是一个字符串数组,存的自己的扫码枪设备名字        // event.getDevice().getName()获取当前输入设备的设备名字        if (event.getDevice().getName().equals(deviceName)) {            isDeviceExist = true;        }    }    if (isDeviceExist) {        return true;    }    return false;}

4.1.3 解析扫码枪扫码内容代码实现

/** * 扫码枪事件解析 * * @param event KeyEvent */public void analysisKeyEvent(KeyEvent event) {    int keyCode = event.getKeyCode();    if (event.getAction() == KeyEvent.ACTION_DOWN) {        char aChar = getInputCode(event);        if (aChar != 0) {            mStringBufferResult.append(aChar);        }        if (keyCode == KeyEvent.KEYCODE_ENTER) {            //若为回车键,直接返回            mHandler.removeCallbacks(mScanningFishedRunnable);            mHandler.post(mScanningFishedRunnable);        } else {            // 延迟post,若500ms内,有其他事件            mHandler.removeCallbacks(mScanningFishedRunnable);            mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY);        }    }}//获取扫描内容private char getInputCode(KeyEvent event) {    int keyCode = event.getKeyCode();    char aChar = 0;    // 这里根据自己业务需求决定是否需要英文字母什么的,我这里只需要数字    if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {        //数字        aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0);    }    return aChar;}

4.1.4 最后附上扫码枪帮助类代码实现

public class ScanGunKeyEventHelper {private final static long MESSAGE_DELAY = 500;             //延迟500ms,判断扫码是否完成。private final Handler mHandler;private final Runnable mScanningFishedRunnable;private StringBuffer mStringBufferResult;                  //扫码内容private OnScanSuccessListener mOnScanSuccessListener;private String[] mDeviceName = null;public ScanGunKeyEventHelper(OnScanSuccessListener onScanSuccessListener) {    mOnScanSuccessListener = onScanSuccessListener;    mStringBufferResult = new StringBuffer();    mHandler = new Handler();    // 通过配置文件获取扫码枪名字    String deviceNames = GetConfig.getScanDeviceName();    // 若没配置扫码枪名字,则使用默认的    if (deviceNames == null || deviceNames.length() <= 0) {        mDeviceName = new String[]{"deviceName1", "deviceName2"};    } else {        mDeviceName = deviceNames.split(",");    }    mScanningFishedRunnable = new Runnable() {        @Override        public void run() {            performScanSuccess();        }    };}/** * 返回扫码成功后的结果 */private void performScanSuccess() {    String barcode = mStringBufferResult.toString();    if (mOnScanSuccessListener != null)        mOnScanSuccessListener.onScanSuccess(barcode);    mStringBufferResult.setLength(0);}/** * 扫码枪事件解析 * * @param event KeyEvent */public void analysisKeyEvent(KeyEvent event) {    int keyCode = event.getKeyCode();    if (event.getAction() == KeyEvent.ACTION_DOWN) {        char aChar = getInputCode(event);        if (aChar != 0) {            mStringBufferResult.append(aChar);        }        if (keyCode == KeyEvent.KEYCODE_ENTER) {            //若为回车键,直接返回            mHandler.removeCallbacks(mScanningFishedRunnable);            mHandler.post(mScanningFishedRunnable);        } else {            //延迟post,若500ms内,有其他事件            mHandler.removeCallbacks(mScanningFishedRunnable);            mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY);        }    }}//获取扫描内容private char getInputCode(KeyEvent event) {    int keyCode = event.getKeyCode();    char aChar = 0;    if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {        //数字        aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0);    }    return aChar;}public void onDestroy() {    mHandler.removeCallbacks(mScanningFishedRunnable);    mOnScanSuccessListener = null;}/** * 输入设备是否存在 * * @return 设备是否存在 */public boolean isInputDeviceExist() {    int[] deviceIds = InputDevice.getDeviceIds();    for (int id : deviceIds) {        boolean isDeviceExist = false;        for (String deviceName : mDeviceName) {            if (InputDevice.getDevice(id).getName().equals(deviceName)) {                isDeviceExist = true;            }        }        if (isDeviceExist) {            return true;        }    }    return false;}/** * 是否为扫码枪事件 * * @param event KeyEvent * @return */public boolean isScanGunEvent(KeyEvent event) {    boolean isDeviceExist = false;    for (String deviceName : mDeviceName) {        if (event.getDevice().getName().equals(deviceName)) {            isDeviceExist = true;        }    }    if (isDeviceExist) {        return true;    }    return false;}// 扫码成功后回调public interface OnScanSuccessListener {    void onScanSuccess(String barcode);}}


未完待续...
注:因个人职业转型问题,该文章不会再更新,如有在等待更新的小伙伴,本人深感抱歉,望理解!



更多相关文章

  1. 第三部分:Android 应用程序接口指南---第二节:UI---第一章 用户界
  2. Android应用程序窗口View的measure过程
  3. Mac 真机调试android程序
  4. android设置默认程序&清除默认设置
  5. 修改Android应用程序的默认最大内存值
  6. Android应用程序键盘(Keyboard)消息处理机制分析(16)
  7. android恶意程序分析 (三)
  8. Android获取应用程序的名称,包名,版本号

随机推荐

  1. Mars之android的Handler(2)
  2. Android(安卓)好的源码依赖包 收集
  3. Cordova开发环境搭建
  4. android 开发技巧(10)--为背景添加圆角边
  5. Android解析JSON方式(一)服务器端生成JSO
  6. Android开发SQLite基本用法
  7. Android(安卓)当子控件设置 focusable=tr
  8. APP完全退出
  9. Android(安卓)ConstraintLayout 两控件部
  10. android 9.0 Intent卸载应用无反应问题