第一次参加Android项目(BLE模块开发)

关于xUtils框架

导入xUtils包

在build.gradle(Module:app)下的"dependencies"加入以下代码:

implementation 'org.xutils:xutils:3.5.0'

添加权限

在AndroidManifest.xml中添加以下代码:

<!-- xutils3 需要的存储和联网权限 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

创建Application(MultiDexApplication)

项目中使用的MultiDexApplication而非原生的Application,原因是原生的Application支持最多65536个方法,大型项目引用了众多的第三方库之后几乎不可能避免这个问题。

引入MultiDexApplication,在build.gradle(Module:app)下的"dependencies"加入以下代码:

implementation 'com.android.support:multidex:1.0.3'

然后在MyApplication中的onCreate()中引用:

x.Ext.init(this);
x.Ext.setDebug(BuildConfig.DEBUG); 

值的注意的是,在setDebug中就算是输入的"false",在Logcat中也会打印Log,但是不会打印LogUtil。

在AndroidMainfest文件中注册MyApplication

在AndroidManifest.xml中的标签中加入以下代码:

android:name=".MyApplication"

这样就完成了MyApplication的注册。

xUtils注解的使用

在Activity中使用注解

首先需要定位布局文件,使用如下注解:

@ContentView(R.layout.xxx)

这样Activity就可以获取到指定的布局文件。

然后,在布局文件中获取到指定的部件,使用如下注解:

@ViewInject(R.id.xxx)
private xxx xx;

这样Activity就可以找到指定布局文件中的某一个部件。

最后,在Activity中的onCreate()方法中添加,引用,代码如下:

x.view().inject(this);

只得注意的是,这一句话在项目中也基本没有使用过,但是页面仍然可以正常显示。

在Fragment中使用注解(本次项目中未使用)

具体样例代码如下:

@ContentView(R.layout.fragment_http)
public class HttpFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return x.view().inject(this, inflater, container);
    }
    @Override
    public void onViewCreated(View v, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(v, savedInstanceState);
    }
}

具体的使用不详,在本项目中未使用Fragment。

给点击事件使用注解

在导入布局文件之后,使用如下代码,为指定的部件添加点击事件:

@Event(R.id.xxx)

代码意为,在当前的布局文件中找到id为"xxx"的部件,为其添加点击事件。

xUtils数据库模块的使用

初始化配置

在MyApplication中添加如下代码:

DbManager.DaoConfig daoConfig = new DbManager.DaoConfig()
        //设置数据库名,默认xutils.db
        .setDbName("myapp.db")
        //设置数据库路径,默认存储在app的私有目录
        .setDbDir(new File("/mnt/sdcard/"))
        //设置数据库的版本号
        .setDbVersion(2)
        //设置数据库打开的监听
        .setDbOpenListener(new DbManager.DbOpenListener() {
            @Override
            public void onDbOpened(DbManager db) {
                //开启数据库支持多线程操作,提升性能,对写入加速提升巨大
                db.getDatabase().enableWriteAheadLogging();
            }
        })
        //设置数据库更新的监听
        .setDbUpgradeListener(new DbManager.DbUpgradeListener() {
            @Override
            public void onUpgrade(DbManager db, int oldVersion, int newVersion) {
            }
        })
        //设置表创建的监听
        .setTableCreateListener(new DbManager.TableCreateListener() {
            @Override
            public void onTableCreated(DbManager db, TableEntity<?> table){
                Log.i("JAVA", "onTableCreated:" + table.getName());
            }
        });

然后,采用单例模式:

DbManager db = x.getDb(daoConfig);
public DbManager getDbManager(){
    return db!=null?db:x.getDb(daoConfig);
}

因为MyApplication是单例的,所以在使用的时候获得就能保证不会创建多个对象。

创建实体类

实体类样例代码如下:

@Table(name = "setting")
public class Setting {

    @Column(name = "id", isId = true)
    private int id;//主键

    @Column(name = "userName")
    private String userName;//用户名称

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    @Override
    public int hashCode() {

        return Objects.hash(id, userName);
    }

    @Override
    public String toString() {
        return "Setting{" +
                "id=" + id +
                ", userName='" + userName + '\'' +
                '}';
    }
}

值得注意的是,在网上查到的信息为@Table(name = “setting”,onCreate=“xxx”),意为在表创建的时候加入某些数据,而不是在MyApplication创建的时候自动建表然后有某些信息。

数据库操作(本次项目中常用的)

  1. 保存实体类或者实体类的List到数据库
    void save(Object entity) throws DbException;

    if (db.findAll(Setting.class) == null){
                setting.setUserName("xxxx");
                db.save(setting);
            }
    

    如果原数据库为空,则会在新数据入库的时候被创建。

  2. 按条件查找
    List res = dbManager.selector(Setting.class)
    .where(“username”,"=",aim).findAll();
    将符合条件的所有对象返回到一个List中。

  3. 更新数据库数据
    dbManager.update(res,“username”);
    将res更新到名为"username"的Column中。

xUtils中异步任务的执行

x.task().run(new Runnable() {

@Override

public void run() {
   //异步代码
 	}
   }
);

关于蓝牙模块的实现

监听来自Activity的命令

Activity可以通过Command类的对象向蓝牙Service(BtService)发送以下四种命令:

1. 启动扫描并连接蓝牙设备(“startBLE”)

2. 断开当前连接的蓝牙设备(“stopBLE”)

3. 暂停扫描周围蓝牙设备(“stopScan”)

即实现一种很简单的命令模式。

具体的BtService.Command使用方法

当Service被启动时,在Service#onCreate()中会创建一个Command对象,在外部可以通过Service#getcommander()来获取Service内部的Command对象,然后在Activity中使用Command#ControlService()来向Service中发送命令,命令为String类型,使用时注意String串不要错。

收到命令并且执行命令

收到“启动扫描并连接蓝牙设备”的命令

当Command收到此条命令后,首先会先判断当前的连接流程是否正在执行,如果没有执行则再判断当前是否处于正在连接的状态,如果都没有,则进行当前是否是第一次连接而非重连,如果是第一次连接则会重置相关变量准备连接,如果是重连则会跳过重置变量的操作直接执行扫描并链接的流程。发出连接成功的广播(全局广播),通知到所有需要连接成功广播的地方。

检查阶段(BtService#checkBt())

  1. 会先询问当时是否处于正在执行流程的操作,如果是在执行流程,则不会执行本次操作。
  2. 如果之前没有在执行流程,则本次就是在执行流程,则会置running = true,然后通过BluetoothManager类对象来引用向系统请求来的蓝牙服务。
  3. 再通过上一步的BluetoothManager获取一个蓝牙适配器。
  4. 如果蓝牙适配器不为空,则证明当前设备存在蓝牙模块,即可以使用蓝牙。
  5. 再询问当前的蓝牙功能是否可用(BluetoothAdapter#isEnabled()),如果不可用则会通过BluetoothAdapter#enable()来开启蓝牙功能。
  6. 如果当前设备不可用,则会开启后退出流程,再重新走一次流程。
  7. 当蓝牙功能可用时,则会进入扫描前的准备阶段。

开始扫描蓝牙设备(BtService#scanLeDevice())

  1. 在开始此方法第一步,会先将当前状态置为正在扫描的状态(mScanning = true)。
  2. 判断蓝牙适配器是否为空,如果为空则会终止整个流程,重置相关变量,然后等待下一次被调用。
  3. 通过BluetoothAdapter#startLeScan()来扫描周围的蓝牙设备,将扫描到的结果会返回到BluetoothAdapter.LeScanCallback#onLeScan()中。
  4. 判断由BluetoothAdapter.LeScanCallback#onLeScan()中获得的蓝牙设备的RSSi信号值和设备名称来判断是否是目标设备(BtService#isOurBLE()),如果不是则重置相关变量,终止流程,等待下一次的调用。
  5. 进入连接设备阶段。
  6. 将当前状态置为停止扫描(mScanning = false)。

连接蓝牙设备(BtService#startConn())

  1. 先询问是否通过BluetoothAdapter.LeScanCallback#onLeScan()获得了蓝牙设备对象,如果获得了,则继续流程,如果没有获得,则会重置变量,停止流程,等待下一次连接。
  2. 查看当前的BluetoothGatt对象是否为空,如果为空则继续执行,如果不是空,则断开连接,为连接新的蓝牙设备做准备。
  3. 通过BluetoothDevice#connectGatt()来给BluetoothGatt对象赋值,其中BluetoothDevice#connectGatt()需要调用一个回调对象。
  4. 如果获得到了目标设备的BluetoothGatt服务,则继续执行流程,并将状态置为正在连接(conning = true)且没有在运行流程(running = false)。
  5. 再判断当前的设备是不是目标设备,当前的设备中的Service是否可以被扫描到,如果不是或者不行,再将状态置为不在连接(conning = false),然后断开连接。
  6. 然后再获得一次最新的设备名称。
  7. 如果不是出于重连连接成功的状态(reConntoBLE = false),则向教练机发送请求数据的请求。
  8. 将重连状态置为非重连状态(reConntoBLE = false)。
  9. 在获取到心率信息(或其他关于特征值的数据)之后,停止蓝牙适配器的扫描,如果提前停止会导致无法获取特征值信息。

具体的断开当前连接的过程

先询问BluetoothGatt对象是否为空,如果为空则继续执行其它逻辑,如果不为空,则先断开连接。

发出断开连接的全局广播。

收到“暂停扫描周围蓝牙设备”的命令

当Command收到此条命令后,会直接修改当前是否在执行连接流程的变量,使变量置为“true”,然后整个流程会被终止。

在蓝牙Service被创建时启动的定时任务

在定时任务中主要执行两个功能:

  1. 一个是在连接成功时,每隔一段时间(UPDATE_RSSI_PERIOD)通过BluetoothGatt#readRemoteRssi()更新一下RSSi值,来判定当前是否超出训练区域。
  2. 一个是在掉线时,计算掉线时间,如果超过判定时间(UPDATE_RSSI_PERIOD * OFF_LINE_LIMIT),则会发出掉线广播后重置相关变量。如果在超时之前成功重连,则会将状态置为成功重连(isReConn = true),然后在回调对象的方法中发出重连广播。
  3. 每一次判定是否超时都是在执行完一次搜索连接流程之后判定的。

在BtService中用到的几个回调对象的方法重写详解

BluetoothAdapter.LeScanCallback#onLeScan()

BluetoothAdapter#startLeScan()方法需要一个BluetoothAdapter.LeScanCallback的参数,该方法也就是在此时被调用的,这个方法被重写为,当扫描到的设备的名称不为空时,会更新BtService中的BluetoothDevice对象和换算的RSSi距离,然后用于判定。

BluetoothGattCallback#onConnectionStateChange()

在BluetoothDevice#connectGatt()中需要一个BluetoothGattCallback回调对象,在连接时会调用此方法,在BtService中主要对无法获取到目标设备的Gatt、成功连接到设备和与设备的连接断开这三种情况进行判断。

  1. 无法获取到目标设备的Gatt(status == BluetoothGatt.GATT_SUCCESS)
    当无法获取到目标设备的Gatt服务时,会将状态置为不在进行流程和断开连接(conning = false,running = false),然后断开当前的连接。
  2. 成功连接到设备(newState == BluetoothProfile.STATE_CONNECTED)
    会首先扫描目标设备上的Services。然后判断当前的连接是否是因为被动断开连接引起的,如果是则会发出设备重连的广播,并将状态置为不在处于尝试重新获得连接和不再处于掉线状态(reConntoBLE = false,isReConn = false)。
  3. 与设备的连接断开(newState == BluetoothProfile.STATE_DISCONNECTED)
    如果收到的状态为断开,如果当前的BluetoothGatt对象不为空,会首先断开连接之后再置空,让后将状态置为尝试重新获得连接(reConntoBLE = true),重置连接过程中的相关变量然后发出设备掉线的广播,但是不会清空当前设备的信息。

BluetoothGattCallback#onServicesDiscovered()

该方法是BluetoothGatt#discoverServices()的回调方法。首先根据BluetoothGatt#getService()来获取目标UUID的Service,如果能获得则通过BluetoothGatt.getService#getCharacteristic()来获得目标UUID的特征值,然后如果特征值获得成功,则会开始订阅特征值(BluetoothGatt#setCharacteristicNotification()),如果返回值是true则说明可以订阅,然后再通过Characteristic#getDescriptor(BleDefinedUUIDs.Descriptor.CHAR_CLIENT_CONFIG)来获得描述器来描述收到的特征值,如果可以获得特征值,则会通过BluetoothGattDescriptor#setValue()来激活描述器,然后通过BluetoothGatt#writeDescriptor()来启动描述器。

BluetoothGattCallback#onCharacteristicChanged()

当订阅成功后,每当蓝牙设备上的特征值改变时,此方法会被调用,然后需要判断特征值,然后根据特征值的不同解析的方式也不同,解析完成之后会将解析出来的信息以广播的方式发送。

BluetoothGattCallback#onReadRemoteRssi()

该方法会被BluetoothGatt#readRemoteRssi()回调,在该方法中会首先判断当前是否还处于连接状态,如果处于则会首先换算RSSi信号值,如果换算后的距离处于合法的运动半径(DETERMINE_THE_RADIUS_SPORT)内,则会归零超限次数(outCount),如果当前处于正在连接的状态(conning = true)且当前的距离大于合法运动半径则会让超限次数+1,如果超限次数大于限制次数(OUT_SITE_TIMES_LIMIT),则会将当前的状态置为正在尝试重新连接的状态(reConntoBLE = true),断开连接然后重置相关变量,最后发出用户掉线广播。

BtService的生命周期

BtService#onCreate()

在BtService被创建时,会创建Command对象、注册监听来自发卡器广播的接收器和启动更新RSSi值或尝试重连的定时任务。

BtService#onDestroy()

当BtService被销毁时,如果当前的BluetoothGatt不为空,则会直接关闭BluetoothGatt与蓝牙设备的连接,不会发出广播或者将用户对象置空。

BtService内的工具方法

BtService#getMeters(int newRSSi)

这个方法,主要用于将获取到的RSSi信号强度,换算为米,其中的变量A为蓝牙设备距离接收端1m时RSSi信号强度的绝对值,如果出现换算距离不准确时,可以重新校准变量A。

值得注意的是,由于RSSi信号的强弱收到各种因素干扰而且由于RSSi信号会因为硬件信息产生波动(波动范围为1~10倍正常值),所以不可用于精确测量!

BtService#isOurBLE(BluetoothDevice device)

这个方法,主要是通过设备的名称来判断是否是目标设备。

补充

蓝牙适配器(BluetoothAdapter)

一经启动会自动开始扫描周围设备,而不是调用一次启动方法扫描一次!

项目心得

  1. 需要考虑到程序以后的扩展性,被师父点名批评。
  2. 需要考虑代码的可读性,毕竟是多人协作。
  3. 注意公共功能进行抽取,减少代码的冗余。
  4. 多线程编程时,注意用volatile修饰目标变量,这样此变量就变为线程之间可见的。
  5. 多线程编程时,注意如果多个线程同时访问某一个变量或者方法时,注意用synchronized关键字修饰,这样可以达到互斥的效果。
  6. 类注释要用/** xxx */,注意写清楚该类、类成员变量或者类方法的用途和执行思路。
  7. Java高级和java基础知识还是欠缺。
发布了24 篇原创文章 · 获赞 8 · 访问量 1879

猜你喜欢

转载自blog.csdn.net/qq_40462579/article/details/89932090
今日推荐