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.jpg
接口说明:
- 串口:Android盒子和计算机的通讯接口;
- HDMI接口:连接显示器的通讯接口;
- RJ45网线口:连接互联网的通讯接口(也可通过该接口与计算机通信);
- 电源接口:Android盒子的供电接口。
视图2:
Android盒子接口2.jpg
说明:
- 电源指示灯:显示Android盒子供电情况,亮红灯为正常情况;
- 预留指示灯:预留指示灯,尚未使用;
- 状态灯:Android盒子状态灯,每分钟亮一次,若Android盒子异常(网络异常),则Android盒子会发出“滴滴滴”响声;
- 电源键:Android盒子开机键,设备每次加电时需长按按钮2-3秒,直到电源指示灯亮起;
- 预留按钮:预留按钮,尚未使用;
- 数据接口:Android盒子与外界设备交互接口;
- USB接口:连接扫码枪等设备的接口。
2. 软件介绍
Android盒子是里面装了Android系统的一个终端设备,不过该系统具有root权限。
1.1.2 扫码枪介绍
扫码枪:
USB接口扫码枪.jpg
说明:
usb扫码枪无特殊要求或配置,作为一个外设设备,只要通过usb接口连接上Android盒子就可以工作。
1.2 项目介绍
该项目要求做一个App,用于计算机程序和支付网关间的桥梁,通过该App实现获取付款码并向支付网关发起扣费请求,以及根据计算机程序的不同指令处理不同的业务逻辑。
该篇文章主要整理该项目用到以下几个知识点:
扫码程序开机启动
- 因为这个Android盒子正常使用的时候是不接屏幕的,所以扫码程序也没有界面,操作人员也不能点击扫码程序图标让扫码程序启动,所以扫码程序就要设置成开机自动启动。
日志上传程序
- 考虑到如果这个设备如果要用到一些其他地方(例如其他城市的超时等,反正不在身边)的时候,程序出现bug,需要获取那个盒子的日志来分析问题,所以就开发了一个功能比较简单的日志上传程序,日志上传程序的主功能是把扫码程序的日志上传到服务器,还有另一个功能是监听扫码程序,当扫码程序因内存回收被杀了或者因为有bug而崩溃等等情况下重启启动扫码程序,又或者当扫码程序不在前台的时候把扫码程序调回前台运行。
扫码枪开发:
- 获取扫码枪扫到的内容并解析出来。
Android串口编程:
- Android串口编程介绍。
- 开源项目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 实现:
-
添加权限
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
-
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); } }
-
静态注册广播接收器
<receiver android:name=".StartUpBroadcast" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </receiver>
3. 日志上传程序
3.1 功能
- 这个程序的主要业务逻辑都在一个服务(service)中,要做到时刻监听扫码程序,需要做到自己不被杀掉,所以这个服务是做了“杀不死”的处理。
- 上传扫码程序的本地日志到服务器。
- 监听扫码程序,当扫码程序不在前台运行/扫码程序被杀掉的时候把扫码程序调回前台运行/重新启动扫码程序。至于为什么扫码程序一定要在前台运行,后面的扫码枪开发章节再解释。
3.2 原理
3.2.1 保证service不被杀掉
在onStartCommand方法,返回START_STICKY。手动返回START_STICKY,当service因内存不足被杀掉,当内存又有的时候,service又会被重新创建,但是不能保证任何情况下都被重建,比如进程被干掉了的情况,具体原理请看下面的介绍。
StartCommond几个常量参数简介:
- START_STICKY
在运行onStartCommand后service进程被kill后,那将保留在开始状态,但是不保留那些传入的intent。不久后service就会再次尝试重新创建,因为保留在开始状态,在创建 service后将保证调用onstartCommand。如果没有传递任何开始命令给service,那将获取到null的intent。- START_NOT_STICKY
在运行onStartCommand后service进程被kill后,并且没有新的intent传递给它。Service将移出开始状态,并且直到新的明显的方法(startService)调用才重新创建。因为如果没有传递任何未决定的intent那么service是不会启动,也就是期间onstartCommand不会接收到任何null的intent。- 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不被杀掉
@Override
public 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 监听扫码程序
日志上传程序中的代码
@Override
public 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<ActivityManager.RunningAppProcessInfo> 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;
}
扫码程序的代码
-
添加权限
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
-
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<ActivityManager.RunningTaskInfo> 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重新启动!"); } } }
-
静态注册广播接收器
<receiver android:name=".RestartAppBroadcast" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="com.gorilla.scanningcodedemo.RestartAppBroadcast" /> </intent-filter> </receiver>
4. 扫码枪开发
一般的扫码枪设备都相当于一个普通的外接输入设备,像外接键盘一样。扫码枪扫到的内容就相当于用户在键盘输入内容一样,所以要获取扫码枪扫到的内容,就要对键盘事件做出解析。要解析键盘输入的内容,那就要重写Activity的dispatchKeyEvent(KeyEvent event),所以前面说的Activity必须要在前台运行就是这个原因,因为如果Activity不在前台运行,那么dispatchKeyEvent(KeyEvent event)方法就执行不了,就获取不了扫码内容。
扫码枪开发中的一些常见问题
-
怎么判断扫码枪是否已经连接?
每个品牌型号的扫码枪都有一个名字,你要判断扫码枪是否已经连接,就获取到Android盒子连接的所有外接设备,遍历,通过名字来匹配,如果匹配到这个扫码枪的名字,则认为是连接上了。 -
怎么区分扫描到的结果是一次扫描到的还是多次扫描的结果?
一般扫码枪扫描一次的内容都会以回车键作为结束符,所以可以判断是否有回车键来区分是否是一次扫描的结果。(可能不同扫码枪有不同的结束标识,我在其他博文中看到有些串口扫码枪,它们有特定的数据结束标识,但是我用过三个品牌的usb扫码枪,都是以回车键作为结束标识) -
当应用到各个城市的Android盒子的扫码枪坏了,要更换扫码枪,又找不到相同牌子的扫码枪怎么办?
前面说了,我们是通过扫码枪的名字来判断扫码枪是否已经连接的,所以我们可以将这个扫码枪通过配置文件的方式来写到配置文件中,当要更换不同品牌的扫码枪的时候,我们就可以在配置文件中把扫码枪名字配置上去,然后在扫码程序中读取配置文件来获取扫码枪名字,然后和Android盒子连接上的外接设备名字来对比。
4.1 原理
4.1.1 重写Activity的dispatchKeyEvent(KeyEvent event)方法
首先我们来了解一下当按键按下和弹起,activity中回调的一系列方法:
当键盘按下时
- 首先触发dispatchKeyEvent
- 然后触发onUserInteraction
- 再次onKeyDown
如果按下紧接着松开,则是俩步
- 紧跟着触发dispatchKeyEvent
- 然后触发onUserInteraction
- 再次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)方法代码实现
@Override
public 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);
}
}
未完待续...
网友评论