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;
Set devices;
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 ArrayAdapter adapter;
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 List mList = 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();

就先写到这里,后续有进展再更新~