一、简介
截图(蓝牙查找界面,帮助界面,主控制界面1,主控制界面2,主控制界面3)
开发环境:Eclipse adt。
主要功能:
1.蓝牙连接、断开、接收数据、发送数据、自动连接。
2.指令发送按钮、拖动条、加减微调,定时发送。接收解析并设定参数到界面。
3.版本更新。含检查新版本,下载,安装。
4.帮助。使用说明、购买链接、版本等。
(一定要允许打开蓝牙、定位、读写存储、网络权限,APP提示时不要拒绝)
对应源码:
─src
├─com
│ ├─iswitch
│ │ └─iswitch
│ │ AboutActivity.java//帮助界面
│ │ DeviceListActivity.java//蓝牙查找界面
│ │ MainActivity.java //主控制界面(含蓝牙连接、断开、收发数据)
│ │
│ └─trinet
│ └─util
│ AppUtils.java //获得app通用信息,包名、版本等
│ DialogHelper.java //对话框通用范例,下载对话框等
│ NetHelper.java //网络通用处理
│
├─SilentInstall
│ SilentInstall.java //安装处理
│
└─update
└─test
UpdateManager.java //更新管理处理
Update_TestActivity.java //检查更新界面(启动界面)
二、开发的顺序
1.先通用蓝牙连接、断开、接收数据、发送数据稳定。基础没弄好就不能做后面的。
有些蓝牙模块不一样,2.0与4.0不是一个结构,不是改改就行。
2.界面确定。xml文件确定好。
3.修改源码。源码是根据界面来写,所以先要确定界面。指令发送按钮、拖动条、加减微调,定时发送。接收解析设定参数。
4.版本更新等其它功能。
阅读源码是根据界面xml来找源码。源码都有注释。
三、详解——蓝牙连接、断开、接收数据、发送数据
蓝牙需要的权限
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
ACCESS_COARSE_LOCATION没有,6.0及以上的手机会搜不到蓝牙模块。
自动连接逻辑:
连接成功后保存连接地址,再次打开APP时直接从已经配对的设备中找保存的连接地址,找到就直接返回地址去连接。没有找到就扫描。设备不在就删除保存的地址再扫描。扫描时能搜到的设备才显示出来(配对了不在线也不显示)。第一次需手工配对连接。自动连接连接的是上一次连接成功的设备。
连接逻辑:
放到线程中,UI更新放到UI线程。所以不死机。连接中再点连接会显示“请等待”。
new Thread(new Runnable() {
@Override
public void run() {
//线程
runOnUiThread(new Runnable() {
@Override
public void run() {
//线程结束后要更新的UI的线程
}
});
}
}).start();
连接成功后,注册异常断开接收器,启动接收。
发送处理
// 发送响应
public boolean sendString(String str) {
if (_socket == null) {
Toast.makeText(this, "未连接蓝牙", Toast.LENGTH_SHORT).show();
return false;
}
if (str == null) {
Toast.makeText(this, "发送内容为空", Toast.LENGTH_SHORT).show();
return false;
}
try {
OutputStream os = _socket.getOutputStream(); // 蓝牙连接输出流
if(hex)
{
byte[] bos_hex = hexStringToBytes(str); // 十六进制
os.write(bos_hex);
}
else
{
byte[] bos = str.getBytes("GBK"); //native的Socket发送字节流默认是GB2312的,所以在Java方面需要指定GB2312
os.write(bos);
Toast.makeText(this, "发送成功"+str, Toast.LENGTH_SHORT).show();
return true;
}
} catch (IOException e) {
return false;
}
return true;
}
接收处理
// 接收数据线程
Thread ReadThread = new Thread() {
public void run() {
int num = 0;
byte[] buffer = new byte[1024];
byte[] buffer_new = new byte[1024];
bRun = true;
// 接收线程
while (true) {
try {
while (is.available() == 0) { //无接收数据
while (bRun == false) { //线程阻塞
}
}
while (true) {
if (1 == is.read(buffer, 0, 1)) // 一个一个地接收,把需要的数据放在buffer_new中
{
if(hex)
{
smsg+=String.format("%02X ",buffer[0]);//转为十六进制格式 所有接收
}
else
{
if(127<(buffer[0]&0xff)) //解决汉字被截断
{
if (1 == is.read(buffer, 1, 1)) //昇954E 4E =78 少于128 所以只能判断第一个字节
{
smsg+=new String(buffer, 0, 2, "GBK"); //+String.format("(%02X %02X )",buffer_new[0],buffer_new[1]); //GBK GBK UTF-8
}
else
{
buffer_new[num++]=buffer[0];
if(num==2)
{
smsg+=new String(buffer_new, 0, 2, "GBK"); //+String.format("(%02X %02X )",buffer_new[0],buffer_new[1]); //GBK GBK UTF-8
num=0;
}
}
}
else
smsg+=new String(buffer, 0, 1, "GBK"); //GBK GBK UTF-8
}
}
/*
num = is.read(buffer); // 读入数据
smsg += new String(buffer, 0, num, "GBK"); //GBK GBK UTF-8
*/
if (is.available() == 0)
break; // 短时间没有数据才跳出进行显示
}
// 发送显示消息,进行显示刷新
handler.sendMessage(handler.obtainMessage());
} catch (IOException e) {
}
}
}
};
四、详解——指令发送按钮、拖动条、加减微调,定时发送。接收解析设定参数。
主要是批量处理(控件绑定,控件响应,控件指令,接收解析设定参数到界面)
控件批量处理范例
private String[][] toggleCommandArray={
{"ty","tz"},
{"tg","tk"}, //关闭,打开
{"at","mt"},
{"wx","yx"},
};
private int[] toggleIdArray={
.ToggleButton11,
.ToggleButton12,
.ToggleButton01,
.ToggleButton02,
};
private ToggleButton[] toggleArray=new ToggleButton[toggleIdArray.length];
private void setElse() //设置其它控件
{
for(int i=0;i<toggleIdArray.length;i++)
{
toggleArray[i] = (ToggleButton) findViewById(toggleIdArray[i]);
toggleArray[i].setChecked(false); // 显示原状态 退刀关闭水钻机
toggleArray[i].setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
// @Override
public void onCheckedChanged(CompoundButton v,
boolean isChecked) {
//判断是不是点击触发的,否则当我setChecked()时会触发此listener
if(isToggle)
return;
for(int i=0;i<toggleIdArray.length;i++){
if(v.getId()==toggleIdArray[i])
{
if (isChecked)
{
sendString(toggleCommandArray[i][1]);
}
else
{
sendString(toggleCommandArray[i][0]);
}
}
}
}
});
}
}
定时1秒发送
private void setElse() //设置其它控件
{
toggleButton_switch3 = (ToggleButton) findViewById(.ToggleButton03);
toggleButton_switch3.setChecked(false); // 显示原状态
toggleButton_switch3.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
// @Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
if (isChecked)
{
if (_socket != null)
{
Toast.makeText(MainActivity.this, "启动定时发送", Toast.LENGTH_SHORT).show();
handler2.postDelayed(runnable2,TIME);
}
else
{
Toast.makeText(MainActivity.this, "请先连接设备", Toast.LENGTH_SHORT).show();
toggleButton_switch3.setChecked(false); // 显示原状态
}
}
else
Toast.makeText(MainActivity.this, "关闭定时发送", Toast.LENGTH_SHORT).show();
}
});
}
Handler handler2 = new Handler();
Runnable runnable2 = new Runnable() {
@Override
public void run() {
// handler自带方法实现定时器
try {
if(toggleButton_switch3.isChecked())
{
smsg="";
dis.setText(smsg);//先清空接收区
sendString(btCommandArray[0]);
handler2.postDelayed(runnable2,TIME); //定时到后,重新定时
}
else
{
}
} catch (Exception e) {
//
e.printStackTrace();
// System.out.println("exception...");
}
}
};
接收解析
接收测试数据示例:
sz:00.3
tf:00.2
rs:023
zf:02.2A
qs:024S
tz
tk
mt
yx
接收解析源码
private void receive(String smsg)
{
try {
//Toast.makeText(this, "接收:"+smsg, Toast.LENGTH_SHORT).show();
if (smsg.contains(":") && smsg.endsWith("\r\n") &&smsg.length()>5) {
for(int i=0;i<seekBarIdArray.length;i++)
{
String s = valueCommandPreArray[i]+":"; //前缀,格式sz:00.0\r\n
if (smsg.contains(s)) {
int beginIndex = smsg.lastIndexOf(s);
int endIndex = smsg.indexOf("\r\n",beginIndex + s.length()); //后缀
String strData=smsg.substring(beginIndex + s.length(), endIndex).replace("A", "").replace("S", "");//取得的数据
if(i<4 || i==6 || i==9)//这些有一位小数
{
valueArray[i]=(int)(10*Float.parseFloat(strData));
}
else
{
valueArray[i]=Integer.parseInt(strData);
}
tvsbSetText(i);
sbArray[i].setProgress(valueArray[i] );
}
}
}
} catch (Exception e) {
}
try {
//toggle开关
if (smsg.length()>15 && (smsg.endsWith("yx\r\n") || smsg.endsWith("wx\r\n"))) {
for(int i=0;i<toggleIdArray.length;i++)
{
isToggle=true;
if (smsg.contains(toggleCommandArray[i][0]+"\r\n"))//ty off
{
toggleArray[i].setChecked(false);
// toggleArray[i].invalidate();
}
else if(smsg.contains(toggleCommandArray[i][1]+"\r\n"))//tz on
{
toggleArray[i].setChecked(true);
//toggleArray[i].validate();
}
isToggle=false;
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
五、详解——版本更新
版本更新需要的权限
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
逻辑顺序:
1.先检查更新。读这个版本说明文件Update_Test_version.txt内容 ,里面的版本与APP版本不一致就提示更新。所以版本说明文件Update_Test_version.txt内容 版本号要与更新的MainActivitySZJ.apk版本一致。verCode一致就行。android手机不允许更新的版本小于当前安装版本。无网络、网络处理失败和用户取消更新,就跳过。
[{"verCode":"4","verName":"1.4"}]
2. 下载apk文件。下载完后当前是放在手机根目录下。
3.安装。先静默安装(全自动,需root权限) ,不具备或失败就使用提示安装。
4.启动 新安装的apk(静默安装才有效)。
版本更新配置修改
检查更新
private void getCurVersion() { //得到当前版本号
try {
PackageInfo pInfo = ctx.getPackageManager().getPackageInfo(
ctx.getPackageName(), 0);
curVersion = pInfo.versionName;
curVersionCode = pInfo.versionCode;
} catch (NameNotFoundException e) {
Log.e("update", e.getMessage());
curVersion = "1.1.1000";
curVersionCode = 111000;
}
}
public void checkUpdate() { //检查是否有更新
hasNewVersion = false;
new Thread(){
// ***************************************************************
/**
* @by wainiwann add
*
*/
@Override
public void run() {
Log.i("@@@@@", ">>>>>>>>>>>>>>>>>>>>>>>>>>>getServerVerCode() ");
try {
String verjson = NetHelper.httpStringGet(UPDATE_CHECKURL);
Log.i("@@@@", verjson
+ "**************************************************");
JSONArray array = new JSONArray(verjson);
if (array.length() > 0) {
JSONObject obj = array.getJSONObject(0);
try {
newVersionCode = Integer.parseInt(obj.getString("verCode"));
newVersion = obj.getString("verName");
updateInfo = "";
Log.i("newVerCode", newVersionCode
+ "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
Log.i("newVerName", newVersion
+ "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
if (newVersionCode > curVersionCode) {
hasNewVersion = true; //有新版本,需更新
}
} catch (Exception e) {
newVersionCode = -1;
newVersion = "";
updateInfo = "";
}
}
} catch (Exception e) {
Log.e("update", e.getMessage());
}
updateHandler.sendEmptyMessage(UPDATE_CHECKCOMPLETED);
};
// ***************************************************************
}.start();
}
下载apk
public void downloadPackage()
{
new Thread() {
@Override
public void run() {
try {
URL url = new URL(UPDATE_DOWNURL);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.connect();
int length = conn.getContentLength();
InputStream is = conn.getInputStream();
File ApkFile = new File(savefolder,UPDATE_SAVENAME);
if(ApkFile.exists())
{
ApkFile.delete();
}
FileOutputStream fos = new FileOutputStream(ApkFile);
int count = 0;
byte buf[] = new byte[512];
do{
int numread = is.read(buf);
count += numread;
progress =(int)(((float)count / length) * 100);
updateHandler.sendMessage(updateHandler.obtainMessage(UPDATE_DOWNLOADING));
if(numread <= 0){
updateHandler.sendEmptyMessage(UPDATE_DOWNLOAD_COMPLETED);
break;
}
fos.write(buf,0,numread);
}while(!canceled);
if(canceled)
{
updateHandler.sendEmptyMessage(UPDATE_DOWNLOAD_CANCELED);
}
fos.close();
is.close();
} catch (MalformedURLException e) {
e.printStackTrace();
updateHandler.sendMessage(updateHandler.obtainMessage(UPDATE_DOWNLOAD_ERROR,e.getMessage()));
} catch(IOException e){
e.printStackTrace();
updateHandler.sendMessage(updateHandler.obtainMessage(UPDATE_DOWNLOAD_ERROR,e.getMessage()));
}
}
}.start();
}
提示安装
private void installSelf(String fileName ) {//提示安装
try{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(fileName)), "application/vnd.android.package-archive");
startActivity(intent);
}catch(Exception e)
{
}
}
启动
private void doStartApplicationWithPackageName(String packagename) {
// 通过包名获取此APP详细信息,包括Activities、services、versioncode、name等等
PackageInfo packageinfo = null;
try {
packageinfo = getPackageManager().getPackageInfo(packagename, 0);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
if (packageinfo == null) {
return;
}
// 创建一个类别为CATEGORY_LAUNCHER的该包名的Intent
Intent resolveIntent = new Intent(Intent.ACTION_MAIN, null);
resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER);
resolveIntent.setPackage(packageinfo.packageName);
// 通过getPackageManager()的queryIntentActivities方法遍历
List<ResolveInfo> resolveinfoList = getPackageManager()
.queryIntentActivities(resolveIntent, 0);
ResolveInfo resolveinfo = resolveinfoList.iterator().next();
if (resolveinfo != null) {
// packagename = 参数packname
String packageName = resolveinfo.activityInfo.packageName;
// 这个就是我们要找的该APP的LAUNCHER的Activity[组织形式:packagename.mainActivityname]
String className = ;
// LAUNCHER Intent
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
// 设置ComponentName参数1:packagename参数2:MainActivity路径
ComponentName cn = new ComponentName(packageName, className);
intent.setComponent(cn);
startActivity(intent);
}
}