一、问题来源
目前市面上多数APP都是基于互联网来读取数据,展示界面,但若在无网络环境,如展厅、政府机构中大多都不连接外网,那么APP的数据只能以静态或内置的方式存储,这些数据(图片、Excel等文档、音视频等)无法刷新和更改,非常不方便后期维护更新;
典型案例:某展厅有一台触摸屏(Android系统),提供该单位所管辖范围下的客户信息,一旦客户信息变更,那么就需要修改APP中的数据,问题来了,该数据是静态写入程序的,触摸屏是不联网的,只能重新提供APP安装包,非常不方便。
二、分析原因
数据来源:
1 内置数据,就是所谓的单机版,静态,不易修改,无法满足后期更新维护的要求,无法满足如上案例更新数据的要求;
2 服务器提供,通过接口获取服务器存储的数据,前提是需要一台服务器,在联网的情况下通过接口把数据提供给APP,达到更新数据的目的,这种也无法满足如上案例无网的情况;
案例分析:本案例在日常生活中经常遇到,如展厅的触摸屏,内容数据更新频率不高,设备不联网,只用来做简单的查询展示作用;
束手无策:怎么办,无法满足案例需求?还好,可采用第三种解决方案,通过中间件提供数据,即无需设备联网,也无需一台服务器提供数据,也可实现更新数据的目的,那就是通过OTG技术。
什么是OTG ?
OTG是一种USB传输技术,通过OTG转接线,可以让手机直接访问U盘或数码相机等设备中的文件,还可以连接到键盘、鼠标等外接设备;
三、解决方案
非常产品,就需非常手段,笔者已经解释了OTG技术,那么解决方案应该已经清楚了,简单描述就是支持OTG的设备(一般触摸屏和部分手机都支持OTG技术),通过U盘提供数据源给APP程序;
方案描述:首先将修改后的数据源按照一定格式或文件目录存入U盘,APP启动后从U盘读取数据并存入设备内置SD卡,拔掉U盘后,App从SD卡中读取对应数据展示;
数据流向:U盘——SD卡——App程序——用户界面。
附录一张笔者在项目技术方案中的流程图:
四、代码流程
基本思路已如上图很清晰了,下面就是通过代码来一步一步实现了,核心代码如下:
1 权限检测和开源库
涉及权限,注意6.0的动态权限
<!-- 开机启动 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- SD卡写权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- SD卡读权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 在SDCard中创建与删除文件权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
U盘插入拔出检测
private void registerReceiver() {
//监听otg插入 拔出
IntentFilter usbDeviceStateFilter = new IntentFilter();
usbDeviceStateFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
usbDeviceStateFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
registerReceiver(mUsbReceiver, usbDeviceStateFilter);
//注册监听自定义广播
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(mUsbReceiver, filter);
}
private BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case ACTION_USB_PERMISSION://接受到自定义广播
setMsg("接收到自定义广播");
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { //允许权限申请
if (usbDevice != null) { //Do something
setMsg("用户已授权,可以进行读取操作");
readDevice(getUsbMass(usbDevice));
} else {
setMsg("未获取到设备信息");
}
} else {
setMsg("用户未授权,读取失败");
}
break;
case UsbManager.ACTION_USB_DEVICE_ATTACHED://接收到存储设备插入广播
UsbDevice device_add = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (device_add != null) {
setMsg("接收到存储设备插入广播,尝试读取");
redDeviceList();
}
break;
case UsbManager.ACTION_USB_DEVICE_DETACHED://接收到存储设备拔出广播
UsbDevice device_remove = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (device_remove != null) {
setMsg("接收到存储设备拔出广播");
usbFiles.clear();//清除
cFolder = null;
}
break;
}
}
};
2 读取U盘数据
这里用到开源库:
//Excel解析库
implementation 'com.hynnet:jxl:2.6.12.1'
//U盘
implementation 'com.github.mjdev:libaums:0.5.5'
private void readDevice(UsbMassStorageDevice device) {
// before interacting with a device you need to call init()!
try {
device.init();//初始化
// Only uses the first partition on the device
Partition partition = device.getPartitions().get(0);
FileSystem currentFs = partition.getFileSystem();
// fileSystem.getVolumeLabel()可以获取到设备的标识
// 通过FileSystem可以获取当前U盘的一些存储信息,包括剩余空间大小,容量等等
// Log.d(TAG, "Capacity: " + currentFs.getCapacity());
// Log.d(TAG, "Occupied Space: " + currentFs.getOccupiedSpace());
// Log.d(TAG, "Free Space: " + currentFs.getFreeSpace());
// Log.d(TAG, "Chunk size: " + currentFs.getChunkSize());
UsbFile root = currentFs.getRootDirectory();//获取根目录
String deviceName = currentFs.getVolumeLabel();//获取设备标签
setMsg("正在读取U盘" + deviceName);
cFolder = root;//设置当前文件对象
addFile2SD();
} catch (Exception e) {
e.printStackTrace();
setMsg("读取失败,异常:" + e.getMessage());
}
}
3 写数据到SD卡
private void addFile2SD() {
try {
for (UsbFile file : cFolder.listFiles()) {
//找到文件夹
if (file.isDirectory() && file.getName().equals(SD_DIR_NAME)) {
for (UsbFile subfile : file.listFiles()) {
if (subfile.isDirectory()) {//image 文件夹
for (UsbFile imgfile : subfile.listFiles()) {
usbFiles_img_list.add(imgfile);
readFile(imgfile,FileUtil.getSavePath(SD_DIR_NAME+File.separator+SD_DIR_IMAGE_NAME));
}
} else { //em.xls info.png
usbFiles_xls_list.add(subfile);
readFile(subfile,FileUtil.getSavePath(SD_DIR_NAME));
}
}
break;
}
}
} catch (IOException e) {
e.printStackTrace();
setMsg("读取出错IO异常:" + e.getMessage());
}
}
4 App读取数据
根据路径从SD卡中取出所需文件,示例
private void loadExcelData() {
if (!SDUtils.checkSDcard()) {
Toast.makeText(getBaseContext(), "该设备没有SD卡,请检查!", Toast.LENGTH_LONG).show();
return;
}
String filePath = SDUtils.getSDPath() + File.separator + SDUtils.SD_DIR_NAME +
File.separator + SDUtils.SD_FILE_NAME;
Log.w("TAG", "filePath:" + filePath);
//解析xls
ExcelDataLoader excelDataLoader = new ExcelDataLoader();
excelDataLoader.execute(filePath);
}
//在异步方法中 调用
private class ExcelDataLoader extends AsyncTask<String, Integer, ArrayList<ExcelBean>> {
//运行子主线程,执行耗时操作,防止主线程阻塞,出现ANR
@Override
protected ArrayList<ExcelBean> doInBackground(String... strings) {
return ExcelUtils.parseXlsDataFromSD(strings[0], 0, getBaseContext());
}
//运行于主线程,更新进度
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
}
//运行于主线程,耗时操作结束后执行该方法
@Override
protected void onPostExecute(ArrayList<ExcelBean> excelBeans) {
super.onPostExecute(excelBeans);
initData(excelBeans);
}
}
private void initData(ArrayList<ExcelBean> list) {
Gson gson = new Gson();
String jsonStr = gson.toJson(list);
Log.i("TAG", "JSON_INFO:" + jsonStr);
}
可能有朋友需要看下Excel表格解析成数据模型的代码,代码如下:
public static ArrayList<ExcelBean> parseXlsDataFromSD(String xlsPath, int index, Context
context) {
ArrayList<ExcelBean> excelBeanArrayList = new ArrayList<>();
File file = new File(xlsPath);
try {
InputStream is = new FileInputStream(file);
Workbook workbook = Workbook.getWorkbook(is);
int sheetNum = workbook.getNumberOfSheets();
Log.d(TAG, "the num of sheets is " + sheetNum);
for (int i = 0; i < sheetNum; i++) {
Sheet sheet = workbook.getSheet(i);
int sheetRows = sheet.getRows();
int sheetColumns = sheet.getColumns();
Log.d(TAG, "the name of sheet is " + sheet.getName());
Log.d(TAG, "total rows is 行=" + sheetRows);
Log.d(TAG, "total cols is 列=" + sheetColumns);
for (int j = 1; j < sheetRows; j++) {
ExcelBean excelBean = new ExcelBean();
excelBean.setNum(Integer.valueOf(sheet.getCell(0, j).getContents()));
excelBean.setType(Integer.valueOf(sheet.getCell(1, j).getContents()));
excelBean.setName(sheet.getCell(2,j).getContents());
/*
* 如果是日期类型,可以使用DateCell对象提供的方法:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Cell cell = sheet.getCell(j, i);
DateCell dateCell = (DateCell) cell;
String date = sdf.format(dateCell.getDate());
如果是数值类型,可以使用NumberCell对象提供的方法:
Cell cell = sheet.getCell(j, i);
NumberCell numberCell = (NumberCell)cell;
Double numberValue = numberCell.getValue();
String number = numberValue.toString();
* */
Cell cell_lon = sheet.getCell(3, j);
NumberCell numberCell_lon = (NumberCell)cell_lon;
Double numberValue_lon = numberCell_lon.getValue();
excelBean.setLatitude(numberValue_lon);
Cell cell_lat = sheet.getCell(4, j);
NumberCell numberCell_lat = (NumberCell)cell_lat;
Double numberValue_lat = numberCell_lat.getValue();
excelBean.setLongitude(numberValue_lat);
excelBean.setContent(sheet.getCell(5,j).getContents());
excelBean.setImgPath(sheet.getCell(6,j).getContents());
Log.d(TAG, "setLongitude=" + sheet.getCell(3,j).getContents());
excelBeanArrayList.add(excelBean);
}
}
workbook.close();
} catch (Exception e) {
Log.e(TAG, "read error=" + e, e);
}
return excelBeanArrayList;
}
5 用户界面展示
将获取到的数据做进一步处理,展示到用户界面。
五、方案总结
至此案例需求完美解决,通过Android系统的OTG功能,采用U盘作为数据源提供者,实现了无网络、无服务器来定期更新维护App的特殊要求,目前该解决方案已通过真实项目测试,笔者可提供项目源码或技术协助,注意提供技术或源码均不免费,联系方式:mmw05@163.com,其他问题可直接留言,感谢大家的支持以及前辈同行们总结的宝贵经验。