系列文章目录
【AndroidStudio开发】(一):新建页面切换项目
【AndroidStudio开发】(二):加入摇杆控制
【AndroidStudio开发】(三):经典蓝牙+BLE蓝牙搜索
【AndroidStudio开发】(四):蓝牙BLE发送消息
【AndroidStudio开发】(五):横竖屏切换及APP打包安装
目录
(2)新建一个bt的package,以及BtBase、BtClient类
前言
第一篇文章讲解了如何新建一个页面切换项目并且学会使用模拟手机进行调试功能,第二篇则是讲解如何设计自己的页面并添加摇杆模块,第三篇接着讲解添加蓝牙模块,实现经典和BLE蓝牙的搜索设备功能,前面三篇都是为了第四篇准备的。最后一篇文章要实现的效果是一个通过蓝牙遥控小车的APP。目前还差的功能是APP与小车的蓝牙模块建立连接,APP再通过蓝牙发送消息给小车的蓝牙模块。
一、建立蓝牙连接
前三篇文章的APP已经基本实现了可以搜索到小车的蓝牙模块,现在只需要加入蓝牙的连接过程,就可以建立APP与小车的蓝牙连接。
(1)FirstFragment文件
a. onItemClick函数
对每一个设备的onItemClick函数进行处理,点击表示连接此蓝牙设备,需要增加连接处理。
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
firstGatt = null;
conbletype = mfbletype.get(position);
condev = mLeDeviceListAdapter.getItem(position);
if (btAdapter.isDiscovering()) {
btAdapter.cancelDiscovery();
}
if (conbletype.equals("old")) {
//todo 目前放在secondfragment,需要剥离成类
} else {
connectToDevice(mLeDeviceListAdapter.getItem(position),position);
}
NavHostFragment.findNavController(FirstFragment.this)
.navigate(R.id.action_FirstFragment_to_SecondFragment);
}
public void connectToDevice(BluetoothDevice device, int pos) {
if (firstGatt == null) {
scanLeDevice(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
firstGatt = device.connectGatt(mActivity.getApplicationContext(),
true, gattCallback, TRANSPORT_LE);
} else {
firstGatt = device.connectGatt(mActivity.getApplicationContext(),
true, gattCallback);
}
if (firstGatt == null) {
Log.d(TAG, "firstGatt is null!" );
}
}
}
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
Log.d(TAG, "onConnectionStateChange,Status: " + status);
if (status==BluetoothGatt.GATT_SUCCESS){
switch (newState) {
case BluetoothProfile.STATE_CONNECTED:
Log.i(TAG, "gattCallback,STATE_CONNECTED");
gatt.discoverServices();
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.i(TAG, "gattCallback,STATE_DISCONNECTED");
break;
default:
Log.e(TAG, "gattCallback,STATE_OTHER");
}
}else{
//连接失败
Log.e(TAG,"onConnectionStateChange fail,Status:" + status);
gatt.close();
}
}
/**
* 发现设备(真正建立连接)
* */
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
List<BluetoothGattService> services = firstGatt.getServices();
Log.d(TAG, "onServicesDiscovered:" + services.toString());
//gatt.readCharacteristic(services.get(0).getCharacteristics().get(0));
for (BluetoothGattService service : services){
List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
Log.d(TAG, "扫描到Service:uuid " + service.getUuid());
for (BluetoothGattCharacteristic characteristic : characteristics) {
Log.d(TAG, "扫描到Service:characteristic " + characteristic.getUuid());
}
}
}
@Override
public void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, int status) {
Log.d(TAG, "onCharacteristicRead:" + characteristic.toString());
}
};
这个地方后面需要优化,应该单独抽象成一个蓝牙类,统一入口,现在这种处理方式有点丑陋,BLE的蓝牙连接放在first里面,经典蓝牙的连接放在了second里面。
b. onCreateView函数
scan按钮的右侧增加一个断连按钮,所以onCreateView函数里面需要增加一个对断连按钮的处理。
Button dc = view.findViewById(R.id.dc);
dc.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if (conbletype.equals("old")) {
//todo 目前放在secondfragment,需要剥离成类
} else {
if (firstGatt != null) {
firstGatt.disconnect();
firstGatt = null;
APP.toast("Disconnected From Device", Toast.LENGTH_SHORT);
} else {
APP.toast("No Device Is Connected", Toast.LENGTH_SHORT);
}
firstGatt = null;
}
}
});
firstGatt设置成静态变量的原因是,first页面负责搜索,点击对应的蓝牙设备会跳转到second页面,同时会销毁掉第一个页面,导致点击蓝牙建立的连接firstGatt同时也会被销毁,所以建立成静态,second页面获取的时候就不会是空值了。
增加Gatt的获取函数,第二个页面可以通过这个函数获取Gatt连接。
public static BluetoothGatt getBluetoothGatt() {
return firstGatt;
}
经典蓝牙获取类型和蓝牙设备(注意经典蓝牙发送信息这部分没有设备暂未测试过,请自行测试)
public static String getConbletype() {
return conbletype;
}
public static BluetoothDevice getCondev() {
return condev;
}
(2)fragment_first.xml文件
增加断连(disconnect,dc)按钮资源
<Button
android:id="@+id/dc"
android:text="Disconnect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="30dp"/>
同时增加对button_first按钮的位置限制。
<Button
android:id="@+id/button_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
android:layout_toStartOf="@+id/scan"
android:layout_toEndOf="@+id/dc"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/scan"
app:layout_constraintRight_toLeftOf="@+id/dc"
android:layout_marginBottom="30dp"/>
二、蓝牙发送消息
(1)SecondFragment.java文件
(a) 新建静态变量
private static final String TAG = "SecondFragment";
MainActivity sActivity;
private BluetoothGatt secondGatt = null;
private String secondbletype = "";
private BluetoothDevice secondcondev = null;
private final BtClient secondClient = new BtClient(this);
private ScheduledExecutorService scheduledExecutor;
(b) onAttach函数
public void onAttach(@NonNull Activity activity) {
super.onAttach(activity);
sActivity = (MainActivity) activity;
secondGatt = FirstFragment.getBluetoothGatt();
secondbletype = FirstFragment.getConbletype();
secondcondev = FirstFragment.getCondev();
if (secondbletype.equals("old")) {
Log.d(TAG, "ble type is old!" + secondcondev.getName());
if (secondClient.isConnected(secondcondev)) {
APP.toast("已经连接了", 0);
} else {
secondClient.connect(secondcondev);
APP.toast("正在连接...", 0);
}
}
}
获取first页面BLE蓝牙的Gatt连接,就是这部分经典蓝牙发送信息没有测试过。
(c)类SecondFragment声明增加implements BtBase.Listener
public class SecondFragment extends Fragment implements BtBase.Listener {
(d)增加BtBase类必须同时增加一个消息通知函数
public void socketNotify(int state, Object obj) {
String msg = null;
Toast toast = makeText(sActivity.getApplicationContext(), "", Toast.LENGTH_SHORT);
switch (state) {
case BtBase.Listener.CONNECTED:
BluetoothDevice dev = (BluetoothDevice) obj;
msg = String.format("与%s(%s)连接成功", dev.getName(), dev.getAddress());
//mTips.setText(msg);
toast.setText(msg);
toast.setDuration(Toast.LENGTH_SHORT);
toast.show();
break;
case BtBase.Listener.DISCONNECTED:
msg = "连接断开";
//mTips.setText(msg);
toast.setText(msg);
toast.setDuration(Toast.LENGTH_SHORT);
toast.show();
break;
case BtBase.Listener.MSG:
// msg = String.format("\n%s", obj);
Log.d(TAG, String.format("\n%s", obj));
break;
}
}
(e)onMove增加发送消息操作
@Override
public void onMove(int angle, int strength) {
mTextViewAngleRight.setText(angle + "°");
mTextViewStrengthRight.setText(strength + "%");
mTextViewCoordinateRight.setText(
String.format("x%03d:y%03d",
joystickRight.getNormalizedX(),
joystickRight.getNormalizedY())
);
String msgj = "jx" + joystickRight.getNormalizedX() + "y" + joystickRight.getNormalizedY() + "#";
bluetoothsendmsg(msgj);
}
(f)增加bluetoothsendmsg函数
private void bluetoothsendmsg(String msg)
{
if (secondbletype.equals("old")) {
if (secondClient.isConnected(secondcondev)) {
Log.d(TAG, "old Button " + msg);
secondClient.sendMsg(msg);
} else {
Log.i(TAG, "secondClient not connect!");
}
} else {
if (secondGatt != null) {
//BluetoothGattService service = secondGatt.getService(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"));
BluetoothGattService service = secondGatt.getService(UUID.fromString("00001801-0000-1000-8000-00805f9b34fb"));
if (service != null) {
//BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb"));
BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString("00002a05-0000-1000-8000-00805f9b34fb"));
characteristic.setValue(msg.getBytes());
secondGatt.writeCharacteristic(characteristic);
Log.d(TAG, "Button " + msg);
} else {
Log.i(TAG, "service is null!");
}
} else {
Log.i(TAG, "BluetoothGatt is null!");
}
}
}
(2)新建一个bt的package,以及BtBase、BtClient类
(a)文档目录如下
(b)BtBase类
package com.example.myapplication.bt;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Environment;
import android.util.Log;
import com.example.myapplication.APP;
import com.example.myapplication.util.Util;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.UUID;
/**
* 客户端和服务端的基类,用于管理socket长连接
*/
public class BtBase {
static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
// static final UUID SPP_UUID = UUID.fromString("00001105-0000-1000-8000-00805f9b34fb");
// static final UUID SPP_UUID = UUID.fromString("00001800-0000-1000-8000-00805f9b34fb");
// static final UUID SPP_UUID = UUID.fromString("00001801-0000-1000-8000-00805f9b34fb");
private static final String FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/bluetooth/";
private static final int FLAG_MSG = 0; //消息标记
private static final int FLAG_FILE = 1; //文件标记
private BluetoothSocket mSocket;
private DataOutputStream mOut;
private Listener mListener;
private boolean isRead;
private boolean isSending;
BtBase(Listener listener) {
mListener = listener;
}
/**
* 循环读取对方数据(若没有数据,则阻塞等待)
*/
void loopRead(BluetoothSocket socket,BluetoothDevice mdev) {
mSocket = socket;
try {
//防止横竖屏页面导致的in.readInt()返回read failed的read ret: -1错误。
Thread.sleep(300);
if (!mSocket.isConnected()) {
mSocket.connect();
}
notifyUI(Listener.CONNECTED, mSocket.getRemoteDevice());
mOut = new DataOutputStream(mSocket.getOutputStream());
DataInputStream in = new DataInputStream(mSocket.getInputStream());
isRead = true;
Log.d("BaseFragment","loopRead!"+isRead);
while (isRead) { //死循环读取
int msgflag = in.readInt();
Log.d("BaseFragment","read:"+msgflag);
switch (msgflag) {
case FLAG_MSG: //读取短消息
String msg = in.readUTF();
notifyUI(Listener.MSG, "接收短消息:" + msg);
break;
case FLAG_FILE: //读取文件
Util.mkdirs(FILE_PATH);
String fileName = in.readUTF(); //文件名
long fileLen = in.readLong(); //文件长度
// 读取文件内容
long len = 0;
int r;
byte[] b = new byte[4 * 1024];
FileOutputStream out = new FileOutputStream(FILE_PATH + fileName);
notifyUI(Listener.MSG, "正在接收文件(" + fileName + "),请稍后...");
while ((r = in.read(b)) != -1) {
out.write(b, 0, r);
len += r;
if (len >= fileLen)
break;
}
notifyUI(Listener.MSG, "文件接收完成(存放在:" + FILE_PATH + ")");
break;
}
}
} catch (Throwable e) {
Log.d("BaseFragment","error!"+e.getMessage());
e.printStackTrace();
close();
}
}
/**
* 发送短消息
*/
public void sendMsg(String msg) {
if (checkSend()) return;
isSending = true;
try {
mOut.writeInt(FLAG_MSG); //消息标记
mOut.writeChars(msg);
// mOut.write(msg);
mOut.flush();
notifyUI(Listener.MSG, "发送短消息:" + msg);
} catch (Throwable e) {
Log.d("BaseFragment","sendMsg error!"+e.getMessage());
// e.printStackTrace();
close();
}
isSending = false;
}
/**
* 发送文件
*/
public void sendFile(final String filePath) {
if (checkSend()) return;
isSending = true;
Util.EXECUTOR.execute(new Runnable() {
@Override
public void run() {
try {
FileInputStream in = new FileInputStream(filePath);
File file = new File(filePath);
mOut.writeInt(FLAG_FILE); //文件标记
mOut.writeUTF(file.getName()); //文件名
mOut.writeLong(file.length()); //文件长度
int r;
byte[] b = new byte[4 * 1024];
notifyUI(Listener.MSG, "正在发送文件(" + filePath + "),请稍后...");
while ((r = in.read(b)) != -1)
mOut.write(b, 0, r);
mOut.flush();
notifyUI(Listener.MSG, "文件发送完成.");
} catch (Throwable e) {
close();
}
isSending = false;
}
});
}
/**
* 释放监听引用(例如释放对Activity引用,避免内存泄漏)
*/
public void unListener() {
mListener = null;
}
/**
* 关闭Socket连接
*/
public void close() {
try {
isRead = false;
if (mSocket != null)
{
mSocket.close();
notifyUI(Listener.DISCONNECTED, null);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* 当前设备与指定设备是否连接
*/
public boolean isConnected(BluetoothDevice dev) {
boolean connected = (mSocket != null && mSocket.isConnected());
if (dev == null)
return connected;
return connected && mSocket.getRemoteDevice().equals(dev);
}
// ============================================通知UI===========================================================
private boolean checkSend() {
if (isSending) {
APP.toast("正在发送其它数据,请稍后再发...", 0);
return true;
}
return false;
}
private void notifyUI(final int state, final Object obj) {
APP.runUi(new Runnable() {
@Override
public void run() {
try {
if (mListener != null)
mListener.socketNotify(state, obj);
} catch (Throwable e) {
e.printStackTrace();
}
}
});
}
public interface Listener {
int DISCONNECTED = 0;
int CONNECTED = 1;
int MSG = 2;
void socketNotify(int state, Object obj);
}
}
(c)BtClent类
package com.example.myapplication.bt;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import com.example.myapplication.util.Util;
/**
*
* 客户端,与服务端建立长连接
*/
public class BtClient extends BtBase {
public BtClient(Listener listener) {
super(listener);
}
/**
* 与远端设备建立长连接
*
* @param dev 远端设备
*/
public void connect(BluetoothDevice dev) {
close();
try {
// final BluetoothSocket socket = dev.createRfcommSocketToServiceRecord(SPP_UUID); //加密传输,Android系统强制配对,弹窗显示配对码
// final BluetoothSocket socket =(BluetoothSocket) dev.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(dev,1);
final BluetoothSocket socket = dev.createInsecureRfcommSocketToServiceRecord(SPP_UUID); //明文传输(不安全),无需配对
// 开启子线程
Util.EXECUTOR.execute(new Runnable() {
@Override
public void run() {
loopRead(socket,dev); //循环读取
}
});
} catch (Throwable e) {
Log.d("BtFragment","connect error!");
close();
}
}
}
(d)util包里面增加类Util
package com.example.myapplication.util;
import android.util.Log;
import java.io.File;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class Util {
private static final String TAG = com.example.myapplication.util.Util.class.getSimpleName();
public static final Executor EXECUTOR = Executors.newCachedThreadPool();
public static void mkdirs(String filePath) {
boolean mk = new File(filePath).mkdirs();
Log.d(TAG, "mkdirs: " + mk);
}
}
总结
这部分讲解了如何利用起前面APP搜索到的蓝牙设备,即通过点击与之建立蓝牙连接,再进入Second页面进行发送具体的信息,这里面只加入了摇杆发送信息的处理,后面会加入四个按键,来模仿手柄。还有就是经典蓝牙发送信息这部分因为没有设备所以暂未测试过,如果测试过程遇到问题欢迎留言。