android studio蓝牙学习记录
做一个蓝牙的app弄了快一周了,虽然离项目完成还很远,但确实也是遇到了不少问题,特此总结一下。(再不写博客我就要忘了我做过啥了)
一、蓝牙权限
蓝牙权限由于android的版本问题有着不同的申请方式:在android 6.0之前只需要再manifests里添加几行权限代码就能实现对蓝牙权限的调用,而android 6.0以上则规定:所有需要访问硬件唯一标识符的权限,都必须对位置信息进行申请,即便我们这个行为本身与位置无关。而位置信息是Dangerous级权限,所以需要在代码中进行动态申请。但在android 12之后,又设计了三个不需要位置申请就能调用蓝牙信息的新蓝牙权限。
由于我的手机版本是Android 10,所以肯定要进行动态权限申请。其实这个学习一下自己也可以写,但问题就在于我写的由于版本还有一些原因会考虑不全,有很多问题……于是在网上找到了一个大佬博客里写好的代码,直接复制到android studio里就能用。
来源:Android 6.0以上动态申请权限的问题(以蓝牙为例) - 简书 (jianshu.com)
首先在manifests里加上这些:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.bluetooth"> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <uses-permission android:name="android.permission.LOCAL_MAC_ADDRESS" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:configChanges="keyboard|keyboardHidden|navigation" android:theme="@style/Theme.BlueTooth"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> intent-filter> activity> application> manifest>
这里我也加上了android12的三个新权限,因为不加的话写到蓝牙相关的函数会报错(实际加了后还会有新的报错……)。加了前是没有权限,加了后大致的意思是要检查权限,但问题是我手机不是android12,所以检查权限肯定是没有的……但实际上这几个报错都不会影响程序的正常运行,如果觉得顶着十几个error运行不舒服的话就到右上角设置-settings-editor-inspections里调一下警告等级吧。除此之外
然后就是添加两个类,PermissionsActivity和PermissionsChecker,最后直接在自己主界面里继承一下就行。
PermissionsActivity.java:
package com.example.bluetooth; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.provider.Settings; public abstract class PermissionsActivity extends AppCompatActivity { private static final int PERMISSION_REQUEST_CODE = 200; // 系统权限管理页面的参数 private static final String PACKAGE_URL_SCHEME = "package:"; // 方案 private PermissionsChecker mPermissionsChecker; // 权限检测器 private boolean isRequireCheck; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPermissionsChecker = new PermissionsChecker(this); } protected void requestPermission(){ isRequireCheck = true; if (isRequireCheck) { String[] permissions = getPermission(); if (mPermissionsChecker.lacksPermissions(permissions)) { requestPermissions(permissions); // 请求权限 } else { // 全部权限都已获取 onPermissionRequestSuccess(); } } } /** * 用户权限处理, * 如果全部获取, 则直接过. * 如果权限缺失, 则提示Dialog. * * @param requestCode 请求码 * @param permissions 权限 * @param grantResults 结果 */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE && hasAllPermissionsGranted(grantResults)) { isRequireCheck = true; // 全部权限都已获取 onPermissionRequestSuccess(); } else { isRequireCheck = false; onPermissionRequestFail(); } } // 请求权限兼容低版本 private void requestPermissions(String... permissions) { ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE); } // 含有全部的权限 private boolean hasAllPermissionsGranted(@NonNull int[] grantResults) { for (int grantResult : grantResults) { if (grantResult == PackageManager.PERMISSION_DENIED) { return false; } } return true; } // 显示缺失权限提示 protected void showMissingPermissionDialog(String tip) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("提示"); builder.setMessage(tip); builder.setCancelable(false); // 拒绝, 退出应用 builder.setNegativeButton("退出", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { finish(); } }); builder.setPositiveButton("去开启", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { startAppSettings(); finish(); } }); builder.show(); } // 启动应用的设置 public void startAppSettings() { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.parse(PACKAGE_URL_SCHEME + getPackageName())); startActivity(intent); } public abstract String[] getPermission(); public abstract void onPermissionRequestSuccess(); public abstract void onPermissionRequestFail(); }
PermissionsChecker.java:
package com.example.bluetooth; import android.content.Context; import android.content.pm.PackageManager; import androidx.core.content.ContextCompat; /** * 检查权限的工具类 * */ public class PermissionsChecker { private final Context mContext; public PermissionsChecker(Context context) { mContext = context.getApplicationContext(); } // 判断权限集合 public boolean lacksPermissions(String... permissions) { for (String permission : permissions) { if (lacksPermission(permission)) { return true; } } return false; } // 判断是否缺少权限 private boolean lacksPermission(String permission) { return ContextCompat.checkSelfPermission(mContext, permission) == PackageManager.PERMISSION_DENIED; } }
主界面里重写:
@Override public String[] getPermission() { return new String[]{Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.BLUETOOTH,Manifest.permission.BLUETOOTH_ADMIN}; } @Override public void onPermissionRequestSuccess() { } @Override protected void onResume() { super.onResume(); requestPermission(); } @Override public void onPermissionRequestFail() { showMissingPermissionDialog("缺失权限!"); }
二、打开蓝牙并获取配对列表和连接信息
String information,name, address,blu; BluetoothAdapter mBluetoothAdapter; Setdevices; BluetoothManager bluetoothManager; @SuppressLint("HardwareIds") private boolean checkBleDevice() { bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if (bluetoothManager != null) { mBluetoothAdapter = bluetoothManager.getAdapter(); if (mBluetoothAdapter != null) { if (!mBluetoothAdapter.isEnabled()) { if (!mBluetoothAdapter.enable()) { Log.i("tag", "蓝牙打开失败"); } else { Log.i("tag", "蓝牙已打开"); return true; } }else { Log.i("tag","同意申请"); return true; } } } return false; } public void blueToothConnectGet() { name = mBluetoothAdapter.getName(); address = mBluetoothAdapter.getAddress(); devices = mBluetoothAdapter.getBondedDevices(); for (BluetoothDevice bluetoothDevice : devices) { try { connect = (boolean) bluetoothDevice.getClass(). getMethod("isConnected").invoke(bluetoothDevice); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { e.printStackTrace(); } if (connect) { Log.d("markTest","连接对象:"+bluetoothDevice.getName()); information = bluetoothDevice.getName(); runOnUiThread(() -> Toast.makeText(getApplicationContext(), information, Toast.LENGTH_SHORT).show()); } Log.d("markTest", "配对列表:" + bluetoothDevice.getName()); } }
三、蓝牙状态改变的监听
接收函数:
BlueToothValueReceiver blueToothValueReceiver;public class BlueToothValueReceiver extends BroadcastReceiver { public static final int DEFAULT_VALUE_BLUETOOTH = 1000; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, DEFAULT_VALUE_BLUETOOTH); Log.e("tag", String.valueOf(state)); switch (state) { case BluetoothAdapter.STATE_OFF: Log.e("tag", "蓝牙已关闭"); runOnUiThread(() -> Toast.makeText(getApplicationContext(), "蓝牙已关闭", Toast.LENGTH_SHORT).show()); break; case BluetoothAdapter.STATE_ON: Log.e("tag", "蓝牙已打开"); runOnUiThread(() -> Toast.makeText(getApplicationContext(), "蓝牙已打开", Toast.LENGTH_SHORT).show()); break; case BluetoothAdapter.STATE_TURNING_ON: Log.e("tag", "正在打开蓝牙"); runOnUiThread(() -> Toast.makeText(getApplicationContext(), "正在打开蓝牙", Toast.LENGTH_SHORT).show()); break; case BluetoothAdapter.STATE_TURNING_OFF: Log.e("tag", "正在关闭蓝牙"); runOnUiThread(() -> Toast.makeText(getApplicationContext(), "正在关闭蓝牙", Toast.LENGTH_SHORT).show()); break; default: Log.e("tag", "未知状态"); runOnUiThread(() -> Toast.makeText(getApplicationContext(), "未知状态", Toast.LENGTH_SHORT).show()); } } } }
然后在oncreate中开启:
blueToothValueReceiver = new BlueToothValueReceiver(); IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); registerReceiver(blueToothValueReceiver, filter);
四、搜索状态的监听(.startDiscovery())
实现接收的函数:
BlueToothFoundReceiver blueToothFoundReceiver; public class BlueToothFoundReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { //获取广播Action String action = intent.getAction(); //判断广播是搜索到设备还是搜索完成 if (BluetoothDevice.ACTION_FOUND.equals(action)) { // 找到设备后获取其设备 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest. permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 200); Log.d("tag", "扫描到了:" + device.getName() + ":" + device.getAddress() + "\n"); Log.d("tag", String.valueOf(device)); } } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_STARTED)) { Log.e("tag","搜索开启"); } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) { Log.e("tag","搜索完成"); } } }
然后在oncreate中开启:
blueToothFoundReceiver= new BlueToothFoundReceiver(); filter_found= new IntentFilter(BluetoothDevice.ACTION_FOUND); filter_found.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); filter_found.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); filter_found.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); filter_found.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); filter_found.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); registerReceiver(blueToothFoundReceiver, filter_found);
之后就在需要的时候使用就行,比如:
if (mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); } mBluetoothAdapter.startDiscovery();
五、将搜索结果呈现到一个spinner里面
private Spinner spinnertext; private ArrayAdapteradapter; private List list = new ArrayList (); spinnertext = (Spinner) findViewById(R.id.spinner); adapter = new ArrayAdapter (this, android.R.layout.simple_spinner_item, list); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinnertext.setAdapter(adapter);
然后在上面的onReceive里对应修改list的值就行,比如说:
String blu=device.getName() + ":" + device.getAddress(); for(String bluetooth:list) { if(blu.equals(bluetooth)) { return; } } list.add(device.getName() + ":" + device.getAddress());
不要忘了加上清空列表的按键,例如:
runOnUiThread(() ->list.clear());
六、找到具体蓝牙设备,配对或取消配对
还没有做依据选择而改变的效果,目前就是在代码中指定设备名称:
protected BluetoothDevice mSelectedDevice; private void getTargetBLEDevice() { Log.i("tag", "a"); if (devices != null && devices.size() > 0) { for (BluetoothDevice bluetoothDevice : devices) { String name = bluetoothDevice.getName(); Log.i("tag", "bluetoothDevice name " + name); if (bluetoothDevice != null && name.equalsIgnoreCase("需要找的设备名称")) { Log.i("tag", "已找到指定蓝牙设备,该设备MAC=" + bluetoothDevice.getAddress()); mSelectedDevice = bluetoothDevice; break; } } } } private void createOrRemoveBond(int type, BluetoothDevice device) { Method method = null; try { switch (type) { case 1://开始匹配 method = BluetoothDevice.class.getMethod("createBond"); method.invoke(device); break; case 2://取消匹配 method = BluetoothDevice.class.getMethod("removeBond"); method.invoke(device); //list.remove(device);//清除列表中已经取消了配对的设备 break; } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } }
使用示例:
getTargetBLEDevice(); createOrRemoveBond(1,mSelectedDevice); createOrRemoveBond(2,mSelectedDevice);
七、低功耗蓝牙的搜索
摘自博客:(25条消息) Android 低功耗蓝牙开发(扫描、连接)_初学者-Study的博客-CSDN博客_android 低功耗蓝牙扫描
在settings.gradle中加上第三方库:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io"}
}
}
添加两个类:BleDevice和BleDeviceAdapter。
BleDevice.java:
package com.example.bluetooth; import android.bluetooth.BluetoothDevice; /** * @author llw * @description BleDevice * @date 2021/7/21 19:20 */ public class BleDevice { private BluetoothDevice device; private int rssi; private String realName;//真实名称 /** * 构造Device * @param device 蓝牙设备 * @param rssi 信号强度 * @param realName 真实名称 */ public BleDevice(BluetoothDevice device, int rssi, String realName) { this.device = device; this.rssi = rssi; this.realName = realName; } public BluetoothDevice getDevice(){ return device; } public int getRssi(){ return rssi; } public void setRssi(int rssi) { this.rssi = rssi; } public String getRealName(){ return realName; } public void setRealName(String realName) { this.realName = realName; } @Override public boolean equals(Object object) { if(object instanceof BleDevice){ final BleDevice that =(BleDevice) object; return device.getAddress().equals(that.device.getAddress()); } return super.equals(object); } }
BleDeviceAdapter.java:
package com.example.bluetooth; import com.chad.library.adapter.base.BaseQuickAdapter; import com.chad.library.adapter.base.viewholder.BaseViewHolder; import java.util.List; /** * @author llw * @description BleDeviceAdapter * @date 2021/7/21 19:34 */ public class BleDeviceAdapter extends BaseQuickAdapter{ public BleDeviceAdapter(int layoutResId, List data) { super(layoutResId, data); } @Override protected void convert(BaseViewHolder holder, BleDevice bleDevice) { holder.setText(R.id.tv_device_name, bleDevice.getRealName()) .setText(R.id.tv_mac_address, bleDevice.getDevice().getAddress()) .setText(R.id.tv_rssi, bleDevice.getRssi() + " dBm"); } }
activity_main.xml中增加:
<androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_device" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_above="@+id/btn2" android:layout_marginBottom="320dp" android:overScrollMode="never" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" />
新建item_device_rv.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" android:foreground="?attr/selectableItemBackground" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:padding="16dp"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" /> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:paddingStart="12dp"> <TextView android:id="@+id/tv_device_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="end" android:singleLine="true" android:text="设备名称" android:textColor="@color/black" android:textSize="16sp" /> <TextView android:id="@+id/tv_mac_address" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:ellipsize="end" android:singleLine="true" android:text="Mac地址" /> LinearLayout> <TextView android:id="@+id/tv_rssi" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="信号强度" /> LinearLayout> <View android:layout_width="match_parent" android:layout_height="0.5dp" android:background="#EEE" /> LinearLayout>
搜索功能实现:
private ScanCallback scanCallback; private ListmList = new ArrayList<>(); private BleDeviceAdapter deviceAdapter; private void initView() { RecyclerView rvDevice = findViewById(R.id.rv_device); //列表配置 deviceAdapter = new BleDeviceAdapter(R.layout.item_device_rv, mList); rvDevice.setLayoutManager(new LinearLayoutManager(this)); //启用动画 deviceAdapter.setAnimationEnable(true); //设置动画方式 deviceAdapter.setAnimationWithDefault(BaseQuickAdapter.AnimationType.SlideInRight); rvDevice.setAdapter(deviceAdapter); //扫描结果回调 scanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, @NonNull ScanResult result) { addDeviceList(new BleDevice(result.getDevice(),result.getRssi(),result.getDevice().getName())); Log.d(TAG, "name:" + result.getDevice().getName() + ",rssi:" + result.getRssi()); } @Override public void onScanFailed(int errorCode) { throw new RuntimeException("Scan error"); } }; } /** * 开始扫描设备 */ public void startScanDevice() { mList.clear(); deviceAdapter.notifyDataSetChanged(); BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); scanner.startScan(scanCallback); } /** * 停止扫描设备 */ public void stopScanDevice() { BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); scanner.stopScan(scanCallback); } /** * 添加到设备列表 * * @param bleDevice 蓝牙设备 */ private void addDeviceList(BleDevice bleDevice) { if (!mList.contains(bleDevice)) { bleDevice.setRealName(bleDevice.getRealName() == null ? "UNKNOWN" : bleDevice.getRealName()); mList.add(bleDevice); } else { //更新设备信号强度值 for (int i = 0; i < mList.size(); i++) { if (mList.get(i).getDevice().getAddress().equals(bleDevice.getDevice().getAddress())) { mList.get(i).setRssi(bleDevice.getRssi()); break; } } } //刷新列表适配器 deviceAdapter.notifyDataSetChanged(); }
使用示例:
initView();
checkBleDevice();
startScanDevice();
就先写到这里,后续有进展再更新~