智能合约自动检测工具『链必验』,如何带你解锁Web3.0世界

Posted 成都链安

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了智能合约自动检测工具『链必验』,如何带你解锁Web3.0世界相关的知识,希望对你有一定的参考价值。

在我们发布【链必验】新版本之后,目前已有大批开发者前来试用,今天,我们还需要详细介绍这款工具。

【链必验】智能合约自动检测工具,可用来检测区块链智能合约漏洞。平台针对每个用户模拟了一条单独的测试链,用户可以自主在测试链上对智能合约进行部署、测试和验证,是集智能合约开发、测试、验证于一体的综合平台。

在验证的过程中,平台采用形式化验证等技术,对执行环境进行建模,通过数学推理等方法对安全属性进行验证,发现合约在运行时可能出现的安全问题,协助合约开发者发现合约中的潜在安全隐患,定位漏洞产生的位置,增强合约的安全性。主要包含四大方面的检测:代码规范检测、标准规范检测、函数调用检测、业务逻辑安全检测。

Web3.0世界,最不可或缺的,便是智能合约。今天,跟着我们一起来学习这款智能合约自动检测工具,一起解锁Web3.0世界。

ONE 代码规范检测

1.内存ABIEncoderV2数组

等级:ERROR

描述:0.4.7-0.5.9版本solc编译器存在一个BUG,此BUG会导致abi.encode接口处理多维数组时产生错误结果。

样例

在编译器版本为0.5.9时,嵌套数组badArr的返回值是错误的,为 [[1, 2], [2, 3], [3, 4]]。在编译器版本为0.6.7时,嵌套数组badArr的返回值是正确的,为 [[1, 2], [3, 4], [5, 6]]。

修复建议:避免使用0.4.7-0.5.9编译器,或禁止使用0.4.7-0.5.9编译器中的abi.encode接口。

2.多重构造函数

等级:ERROR

描述:在0.4.22版本的编译器中,合约允许合约同时存在两个格式的构造函数(以constructor关键字声明构造函数,或以合约名声明函数)。因此构造函数中的变量存在相互覆盖的危险。

样例

constructor()中初始化x为1,而Test()初始化x为2,位置靠后定义的构造函数会失效,因此x最终会被初始化为1。

修复建议:只使用一种构造函数。

3.公开的mapping嵌套结构变量

等级:ERROR

描述:公开的变量有一个默认的只读getter函数,但公开的mapping嵌套引用结构将导致非法的getter函数。

样例

公开的mappingm嵌套了引用类型struct,将导致针对public变量的默认的读取方法m[1].a失败。

修复建议:避免使用public mapping嵌套引用类型,或者使用pragma experimental ABIEncoderV2。

4.从右到左读写控制字符

等级:ERROR

描述:Unicode [U+202E] 强制编译器从右到左读取,与正常顺序相反,可能误导使用者。

样例

_f函数期待输入值i,j,m依次传递给a,b,c由于有U+202E的存在,输入函数需要以j,i,m的顺序给出。

修复建议:避免使用U+202E字符。

5.状态变量覆盖

等级:ERROR

描述:合约的继承包括状态变量的继承,在子合约中重载基类合约的状态变量可能会造成变量的使用逻辑错误。

样例

合约Test是Base的子合约,Test中a的定义重载了Base中的状态变量a。调用f1()将返回Base中的a,调用f2()将返回Test中的a。

修复建议:避免重载基类合约状态变量。

6.未初始化的storage变量

等级:ERROR

描述:未初始化的storage状态变量的地址将指向第一个状态变量的地址,使用它可能造成数据覆盖或数据丢失。

样例

未初始化变量st的存储指针将指向状态变量a,st.b的赋值将覆盖变量a使a的值变为2。

修复建议:使用前先初始化storage局部变量,或改用memory局部变量。

7.常量函数改变状态

等级:WARNING

描述:在Solidity 0.5.0之前,其可变性被定义为constant/prue/view的函数,但更改了函数体中的语句。这样的函数可以编译,但是只报warning,进一步调用该函数会失败。这个问题在0.5.0以后的版本中已经修复,常量函数中实现的状态修改无法编译。

样例

变量a在f()函数中改变了它的值,但是a()函数被标记为view。所以调用 f() 不会改变 a。

修复建议:确保Solidity 0.5.0之前的合约的可变性是正确的。

8.删除包含mapping的结构

等级:WARNING

描述:使用delete重置包含mapping的struct时,struct中的mapping不会被重置,这可能导致后续逻辑错误。

样例

合约在初始化后实例化了一个struct a,并将a.i初始化为10以及a.j[10]初始化为100。在f1中使用delete a重置了结构a。f2()中可以读取数据,结果是变量a.i已经被重置为0,但是a.j[10]中的数据仍然是100。

修复建议:避免使用delete重置包含mapping的struct。

9.返回值失配

等级:WARNING

描述:在returns语句中声明了返回名称及类型,但实际返回值与声明中变量不符。

样例

函数f()定义了一个返回类型和名称为uint a,而return语句直接返回100,与return声明不匹配。

修复建议:确保return语句中的值与returns语句中的返回声明相匹配。

10.重用基类构造函数

等级:WARNING

描述:合约之间允许继承,子合约继承父合约的状态变量、函数、构造函数。当子合约继承了多个构造函数时,可能多次重用构造函数。

样例

Test1和Test2都是继承了合约test的子合约,并重用了构造函数将各自的状态变量a初始化为1和2,合约Test3继承了合约Test1和Test2,因此具有两个不同的构造函数,导致Test3中状态变量a被多次赋值,并最终赋值为2。

修复建议:确保子合约拥有唯一继承的构造函数。

TWO 标准规范检测

1.未检查转账操作

等级:ERROR

描述:当合约定义ERC20标准的transfer/transferFrom接口时,需要检查transfer/transferFrom接口的返回值,否则会导致对转账状态的判断错误。

样例

token.transferFrom(msg.sender, address(this), amount);的返回值需要检验。

修复建议:对所有转账函数的结果进行校验。

2.错误的ERC20接口

等级:WARNING

描述:在定义标准的ERC20接口时与标准ERC20接口不完全一致。

样例

ERC20标准的Transfer事件是event Transfer(address indexed from, address indexed to, uint256 value);。

修复建议:完全参照ERC20标准设置ERC20事件和接口。

3.错误的ERC721接口

等级:WARNING

描述:定义的标准ERC721接口和标准ERC721接口并不完全相同。

样例

函数ownerOf(uint256 tokenId)是ERC721的接口,但缺少参数或返回值。

修复建议:对照ERC721

THREE 函数调用检测

1 受控的代理调用

等级:ERROR

描述:委托调用是调用合约的一种方式。委托调用的操作空间在调用发起这一方,因此没有任何权限控制或调用地址未知的调用是可被入侵的。

样例

addr可以被调用者任意操控。

修复建议:为执行delegatecall所在函数设置权限控制,指定调用者。

2.未检查的底层call

等级:ERROR

描述:智能合约的底层调用具有返回数据,调用合约执行失败不会导致调用发起合约执行失败,如果调用操作失败且没有检查返回值,可能导致预计逻辑与实际状态出现差异。

样例

address(f).call(abi.encodePacked(function_selector)); 实现了对合约Base中函数f()的调用。但是使用了call没有对调用的返回值做校验,将导致无法判断调用状态。

修复建议:对所有底层call方法返回值进行校验。

3.未检查的send方法

等级:ERROR

描述:智能合约的转账函数send具有返回值,如果转账失败代码会继续执行,调用不会回退状态。因此使用send进行转账时应检查返回值,并以此判定转账是否成功。

样例

函数f使用send进行转移ether,由于没有对send的返回值进行校验,将不能知晓转账是否真实成功。

修复建议:使用send进行转账时,对返回值进行校验。

4.底层call

等级:INFO

描述:使用低级调用是有风险的。低级调用不检查代码是否存在或调用成功。

修复建议:避免底层call

FOUR业务逻辑安全检测

1.转账地址未知

等级:ERROR

描述:转账函数没有添加任何权限限制,且转账接受者可设定,任何人都可以获取合约资金。

样例

函数f()没有任何权限控制,且转账接受者是msg.sender。调用f()即可获得合约所有资金。

修复建议:当合约存在对外转账功能时,对包含转账函数添加正确的权限控制。

2.动态数组长度的修改

等级:ERROR

描述:在solc 0.6.0版本以下,动态数组类型的长度信息可以被直接修改,长度信息的改变将直接影响存储的数组数据。

样例

合约部署后,动态数组a的第20位数据a[20]为1,若调用f(10)将a的长度修改为10,则a[20]指向的值将丢失。

修复建议:避免对动态数组的长度直接或间接修改。

3.谨慎使用枚举

等级:ERROR

描述:在0.4.5版本以前,枚举类型的调用不会进行溢出判断。

样例

E是长度为3的enum类型,即使尝试读取E的第10个,bug()函数也不会恢复。

修复建议:避免使用0.4.0-0.4.4版本的solc编译器,或对枚举值进行区间判断。

4.锁定ETH的合约

等级:ERROR

描述:在智能合约中存在收取以太币的函数,但不存在发出币的函数,将导致以太币被锁定在合约中。

样例

函数f()有一个payable符号,但合约没有能力花费/转移以太币。

修复建议:移除收钱函数的payable属性,或添加可消耗Ether/向外转Ether的函数。

5.错误的修饰器

等级:ERROR

描述:修饰器起到一个状态/权限控制的作用,在修饰器中如果无法到达_;代码段,将无法执行函数并引起逻辑错误。

样例

修饰符 bug1() 有一个 if 语句,当bool_test 为 false 时, _; 不会到达,那么函数 use() 将不会被使用。

修复建议:保证修饰器可以到达_;代码段,正确执行修饰器功能。

6.缺少返回值

等级:ERROR

描述:在函数返回声明有返回值,但没有相应的返回实现。

样例

函数f()声明返回一个uint类型的值,而合约在函数体中缺少return关键字,这将导致返回0(uint类型的最小值 )。

修复建议:添加对应的返回值或删除返回声明。

7.重入风险

等级:ERROR

描述:调用外部合约的主要危险之一是它们可以接管控制流。在重入攻击(又名递归调用攻击)中,恶意合约在函数的第一次调用完成之前回调调用合约。这可能会导致函数的不同调用以不希望的方式交互。在call调用后改变关键状态变量容易造成重入危险。

样例

在函数f()判断地址拥有的数额大小后,使用call发送以太币,最后storage变量book在转账操作后发生变化。因此,攻击者可以循环调用f()来提取以太币。

修复建议:使用检查-生效-交互模式避免重入攻击。

8.合约自毁函数

等级:ERROR

描述:合约中包含了自毁函数,且没有使用任何身份认证,将使合约处于不稳定状态。

样例

任何人都可以通过调用f()将合约销毁并提取合约中的资金。

修复建议:尽量避免使用自毁函数,或添加正确的权限控制。

9.构造函数中存在未初始化的函数指针

等级:ERROR

描述:在合约的构造函数中存在未初始化的函数指针,直接调用这些指针将出现错误。

样例

f是constructor中的函数指针,而在函数指针完全实现之前,它已被调用。这种行为会导致部署失败。

修复建议:在函数指针完全实现之前不要调用函数指针。

10.未初始化的状态变量

等级:ERROR

描述:使用未初始化的状态变量可能导致逻辑错误。

样例

状态变量a未初始化,将被默认为0地址。在执行转账操作时,将导致以太币丢失(转账去0x0地址)。

修复建议:尽可能在声明状态变量时便初始化该状态变量。

11.可入侵的升级合约

等级:ERROR

描述:合约包含自毁函数,且初始化函数任何人可调用。

样例

函数initialize()会初始化合约,但任何人都可以调用,如果攻击者在所有者之前调用initialize(),攻击者可以随时调用kill()来使代理合约功能失效。

修复建议:在合约中将初始化函数功能在构造函数中执行,保证owner不能任意修改。

12.断言错误

等级:ERROR

描述:assert的限制条件是必须满足的。

修复建议:请查看代码逻辑寻找问题并修复。

13.整型上溢出

等级:ERROR

描述:上溢出是指运算的结果超过了结果类型所能表示的上限。

修复建议:请加上溢出判断或使用SafeMath库进行运算。

14.整型下溢出

等级:ERROR

描述:下溢出表示计算操作中的结果超过了结果类型可以表示的下限。

修复建议:请加下溢出判断或使用SafeMath

以上只选取一些案例

查阅更多安全检测项

可复制下面网站阅读

https://beosinofficial.gitbook.io/vaas-zhong-wen/

成都链安最具实力“单品”之一

链必验 v3.1 强势进阶

目前已开启第一批适用

加wx:qiuqiupapa520

让他给你发试用链接

带你解锁蓝牙skill

转载请注明出处,本文出自fanfan带你解锁蓝牙skill系列
蓝牙这块儿算是系统中的一个大块儿,刚开始分析确实很容易没有头绪,所以在进入庞大的源码之前先确定一个分析顺序,也好避免越学越乱。
对于源码的分析不外乎whw(what—how—why)

对于蓝牙各协议的功能以及如何演示都已经分析完了,具体可以参考
带你解锁蓝牙skill(一)以及带你解锁蓝牙skill(二)。转载请注明出处,本文出自fanfan

本文以Android7.0为例进行源码分析。开始分析源码之前,先来看看蓝牙相关的都有什么东西


4,如何开始

在对一个新事物进行研究之前,我们已经了解了他是什么,那么接下来就是庖丁解牛了。但是目前还做不到目无全牛哈哈。 蓝牙代码实现不外乎包括以下三个方面
  1. 界面UI
    • 设置应用中蓝牙的ui
    • 蓝牙本身这个系统应用中的ui
  2. 蓝牙开关默认值
  3. 协议配置开关:手机是否要支持各种协议
在学之前我们也先要明确目的是什么,即学完蓝牙后我们想要掌握什么样的技能?? 大致如下:
  1. 对于系统开发工作者
    • 掌握基础界面修改及相关逻辑
    • 掌握手机蓝牙开关的系统默认值
    • 掌握蓝牙各协议开关(即禁用/启用蓝牙协议)
    • 掌握如何新增蓝牙协议(更高要求)
  2. 对于应用开发工作者
    • 掌握蓝牙的基本用法:包括开关,扫描,配对,连接等等(毕竟系统应用你并不能决定,你只能是调用各接口)
    • 掌握各蓝牙协议的基本用法(比如实现一个读取联系人的操作)
在确定了研究思路和研究目的之后,我们就可以开始对源码的研究了。

5,蓝牙源码研究

按照第四部分确定的大致方向来进行接下来的研究。不论是蓝牙开关默认值还是协议的开关的值,对这些值还好说,三下五除二就分析好了,但是蓝牙界面仍旧有一个大工程在。从驱动往应用层方向,蓝牙相关的代码位置如下
  1. 第一部分,系统应用设置Settings中的蓝牙相关,显示从设置进入蓝牙的一些界面,代码位置为:packages/apps/Settings/src/com/android/settings/bluetooth/目录结构如下:包括蓝牙开关,蓝牙扫描,蓝牙配对框,蓝牙重命名框,蓝牙选择框等等




  2. 第二部分,系统中有个蓝牙应用Bluetooth,显示界面诸如蓝牙文件传入传出历史记录,蓝牙配对框,蓝牙文件传输框等等,代码位置为:/packages/apps/Bluetooth,Java代码目录结构如下:


    可以看出Bluetooth应用中的代码是按照各协议模块进行区分目录的,但是在各协议模块中并不包含对profile的具体定义,以A2DP为例


    在packages/apps/Bluetooth/src/com/anddroid/bluetooth/a2dp文件夹中只有两个文件A2dpService和A2dpStateMachine,至于这两个文件是干什么,后续会介绍,暂时先了解一个大致的目录结构


  3. 第三部分,就是蓝牙协议的具体实现,代码所在路径:/frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/

    在该目录下就是一些蓝牙profile的相关的配置了。



  4. 第四部分,集成的一些蓝牙接口:frameworks/base/core/java/android/bluetooth/,目录结构如下:



  5. 可以看到很多在开发过程中常见的类:BluetoothAdapter,BluetoothDevice,BluetoothSocket等等,蓝牙的核心代码和接口的具体实现都在这里!!!


  6. 第五部分,手机开机后启动的蓝牙服务是BluetoothManagerService,代码目录结构为:/frameworks/base/services/core/java/com/android/server/



对蓝牙的应用层的代码接口有了一个大致了解之后,我们开始进行分析

1>,蓝牙界面相关

估计有着急的人会说,看什么界面啊,我就想知道功能是怎么实现的
但我想说,如果没有界面,你如何知道他到底实现了什么功能??
如果没有界面,你该如何下手??

界面大致包括两部分,设置中的蓝牙界面和蓝牙应用中的蓝牙界面

  • 设置中蓝牙相关的界面

    蓝牙界面相关的分析在去年做过,虽然是4.4.2但是原理还是一样的,也没必要再做那么多无用功,贴上博客链接

    蓝牙界面实现分析(一) 蓝牙界面实现分析(二) 蓝牙界面实现分析(三)

    有需要的可以看一下

    在这里需要说明一下:关于已配对设备的各种配置协议显示界面

    蓝牙设备间的连接其实就是各协议之间的连接,蓝牙设备间有可能有多个协议连接,但每个协议仅支持一个连接

    在设备进行配对时,发送设备配对状态发生改变的广播,在监听到广播后设备会去获取到remote设备所支持的协议,并尝试进行连接,比如如果remote设备支持手机音频或者媒体音频,那么本机设备就会尝试该协议的连接。但如果本机设备已经与另一台B远程设备进行了手机音频的连接,那么本机设备会先断开与B设备的连接,去尝试和remote设备进行手机音频的连接。

    也就是说某一个时刻设备某个协议连接只能支持一个远程设备,但是可以支持多个远程设备进行多个协议连接。 设备A在某时刻可以和远程设备B进行媒体音频连接同时保持和远程设备C之间的手机音频连接,如果设备A需要播放媒体音频(比如音乐,视频等)音频会在设备B中进行播放,如果设备A需要进行播放手机音频(即手机通话)会在设备C中进行播放。
  • 蓝牙应用相关界面

    蓝牙应用是为了配合各蓝牙协议而存在的,所以,针对蓝牙应用相关界面会穿插在蓝牙协议的分析中

    蓝牙配对弹出框过程分析讲述了蓝牙配对弹出框的原理。
  • 蓝牙协议开关

  • 蓝牙协议开关这篇文章中讲述了作为系统开发人员如何禁止掉蓝牙某个协议(包括上层和底层)

2>,蓝牙协议实现

对于蓝牙协议我只能是分析常见且我的测试机可以实现的,计划要分析的协议如下

  • opp文件传输协议
  • 互联网协议
  • Hsp手机音频协议
  • A2dp媒体音频协议

也许后续计划会有所改变,先暂时确定这样。


i>,opp协议

以蓝牙传输图片为例,opp文件传输包括文件的传入和传出两方面,分别来考虑

  • 传出文件

  • 传入文件

传出文件




我们就沿着分享图片这一条线去分析,在分享蓝牙图片时,选择蓝牙分享,当然如果蓝牙未开启的话会询问先要开启蓝牙。至于系统分享属于另一个系统的功能,在以后的文章中会介绍。
先介绍一个目前用到的,在选择蓝牙分享后,会启动BluetoothOppLauncherActivity,在该类中的launchDevicePicker()启动DevicePickerActivity,方法如下:


    /**
    如果蓝牙未开启就开启蓝牙,如果蓝牙已开启就启动选择蓝牙设备界面
     * Turns on Bluetooth if not already on, or launches device picker if Bluetooth is on
     * @return
     */
private final void launchDevicePicker() 
        // TODO: In the future, we may send intent to DevicePickerActivity
        // directly,
        // and let DevicePickerActivity to handle Bluetooth Enable.
        if (!BluetoothOppManager.getInstance(this).isEnabled()) 
            if (V) Log.v(TAG, "Prepare Enable BT!! ");
            //如果蓝牙未开启,就去开启蓝牙
            Intent in = new Intent(this, BluetoothOppBtEnableActivity.class);
            in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            startActivity(in);
         else 
        //如果蓝牙已开启就启动DevicePickerActiivty,会传入一些参数,这个在以后会用到
            if (V) Log.v(TAG, "BT already enabled!! ");
            Intent in1 = new Intent(BluetoothDevicePicker.ACTION_LAUNCH);
            in1.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
            in1.putExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false);
            in1.putExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE,
                    BluetoothDevicePicker.FILTER_TYPE_TRANSFER);
            in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE,
                    Constants.THIS_PACKAGE_NAME);
            in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS,
                    BluetoothOppReceiver.class.getName());
            if (V) Log.d(TAG,"Launching "+BluetoothDevicePicker.ACTION_LAUNCH);
            startActivity(in1);
        
    
开启蓝牙之后,弹出选择蓝牙设备界面

那么该界面显示的蓝牙设备都包括什么呢?以及点击蓝牙设备后又会去做什么事儿呢?带着这些问题来继续接下来的分析

首先我们要知道该界面所加载的activity的名字,这个可以借助sdk的工具很明显的看出。在这里说明一下,sdk中有很多工具可以方便我们的分析,就在sdk\\tools目录下,大家可以自己去尝试。



可以看到该在选择蓝牙分享后弹出的activity的界面为DevicePickerActiviy.java。借助谷歌源码网址AndroidXRef可以快速找到该java文件,进行分析。

该activity的所在目录如下:位于settings应用中(代码来自Android7.0.0_r1分支)

出乎意料。代码简直少的不能再少了


/**
*该activity是蓝牙设备选择时的dialog(这里说是dialog的意思
是该activity的主题是dialog形式的),设备选择的
逻辑实现在BluetoothSettings的fragment中
 * Activity for Bluetooth device picker dialog. The device picker logic
 * is implemented in the @link BluetoothSettings fragment.
 */
public final class DevicePickerActivity extends Activity 

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.bluetooth_device_picker);
    

看到这里你可能也许还会困惑,那这个界面怎么加载出来的?逻辑实现呢?人家说的很清楚了,设备选择的逻辑代码在fragment中,而且还给你指明了和BluetoothSettings相关,也就是说具体的你去BluetoothSettings中找去吧。
但是我们先不着急看BluetoothSettings,先看一下DevicePickerActivity中的代码。该类中就有一个需要分析的,那就是他的布局文件bluetooth_device_picker.xml,该文件内容也是很少

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/bluetooth_device_picker_fragment"
        android:name="com.android.settings.bluetooth.DevicePickerFragment"
        android:layout_width="match_parent"
        android:layout_height="0dip"
        android:layout_weight="1" />
</LinearLayout>

看到这里,差不多就明白了,原来该activity是加载了一个fragment:DevicePickerFragment。是不是差一点就错过了呢。好了,接下来就是去分析该fragment了。
每次去分析一个文件时,首先要明白你想从该文件中明白什么?然后在分析结束后再看看你原先的疑问有没有解决,以及你有什么新的疑问。
所以,明确分析目的:

  • 会加载什么样的设备
  • 设备是如何加载的
  • 设备的点击事件的处理

在继续接下来的阅读时我假设你是对settings源码已经有了一定的了解,如果没有建议你先看看我对源码的settings或者蓝牙的一些基础界面的分析。不论是哪个源码版本,大致都是相通的。
话不多说,进入正题,先来看看DevicePickerFragment类

/**
这句不用翻译了吧各位,,BluetoothSettings是在设置应用中蓝牙的配置和连接管理的界面
 * BluetoothSettings is the Settings screen for Bluetooth configuration and
 * connection management.
 */
public final class DevicePickerFragment extends DeviceListPreferenceFragment 

    public DevicePickerFragment() 
        super(null /* Not tied to any user restrictions. */);
    

    private boolean mNeedAuth;
    private String mLaunchPackage;
    private String mLaunchClass;
    private boolean mStartScanOnResume;
    private ListView mListView;//zhaohaiyun add
     private TextView mEmptyView;//zhaohaiyun add
    @Override
    void addPreferencesForActivity() 
        addPreferencesFromResource(R.xml.device_picker);

        Intent intent = getActivity().getIntent();
        mNeedAuth = intent.getBooleanExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false);

      //调用父类方法设置过滤器,过滤蓝牙设备  setFilter(intent.getIntExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE,
                BluetoothDevicePicker.FILTER_TYPE_ALL));
                //mLaunchPackage 的取值为Constants.THIS_PACKAGE_NAME
                //即取值为com.android.bluetooth
        mLaunchPackage = intent.getStringExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE);
        //mLaunchClass 取值为BluetoothOppReceiver.class.getName()
        //即要启动的class为BluetoothOppReceiver
        mLaunchClass = intent.getStringExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS);
    

    @Override
    public void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        //设置界面的标题
        getActivity().setTitle(getString(R.string.device_picker));
        UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
        mStartScanOnResume = !um.hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH)
                && (savedInstanceState == null);  // don't start scan after rotation,在进行横竖屏切换时数据会保存,不要重新扫描
    

    @Override
    public void onResume() 
        super.onResume();
        //添加扫描到的蓝牙设备
        addCachedDevices();
        if (mStartScanOnResume) 
        //如果满足扫描条件,则进行蓝牙扫描
            mLocalAdapter.startScanning(true);
            mStartScanOnResume = false;
        
    

    @Override
    void onDevicePreferenceClick(BluetoothDevicePreference btPreference) 
    //首先停止扫描
        mLocalAdapter.stopScanning();
        //保存所点击的设备信息
        LocalBluetoothPreferences.persistSelectedDeviceInPicker(
                getActivity(), mSelectedDevice.getAddress());
                //判断是否已经配对或者是远程设备不需要配对授权就可以发送文件
        if ((btPreference.getCachedDevice().getBondState() ==
                BluetoothDevice.BOND_BONDED) || !mNeedAuth) 
                //确定被选择的设备,发送设备信息
            sendDevicePickedIntent(mSelectedDevice);
            finish();
         else 
        //否则就执行父类的方法
            super.onDevicePreferenceClick(btPreference);
        
    

    public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice,
            int bondState) 
        if (bondState == BluetoothDevice.BOND_BONDED) 
            BluetoothDevice device = cachedDevice.getDevice();
            if (device.equals(mSelectedDevice)) 
                sendDevicePickedIntent(device);
                finish();
            
        
    

    @Override
    public void onBluetoothStateChanged(int bluetoothState) 
        super.onBluetoothStateChanged(bluetoothState);

//如果蓝牙状态发生改变,且目前属于开启状态时也会进行扫描
        if (bluetoothState == BluetoothAdapter.STATE_ON) 
            mLocalAdapter.startScanning(false);
        
    

    private void sendDevicePickedIntent(BluetoothDevice device) 
        Intent intent = new Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
        if (mLaunchPackage != null && mLaunchClass != null) 
            intent.setClassName(mLaunchPackage, mLaunchClass);
        
        getActivity().sendBroadcast(intent);
    

  • 首先我们看到了两个问题,包括什么设备,以及如何添加的?
    • 在onResume方法中调用addCachedDevices方法添加蓝牙设备。因为所设备的过滤器是FILTER_TYPE_ALL,所以所添加的设备包括已配对的,已连接的,以及附近可用设备即所有蓝牙设备

      针对选择蓝牙设备界面的filter共有以下几种取值(为什么我这么说,因为对于其他界面比如BLuetoothSettings时filter又会有别的取值,在加载已配对设备和可用设备时区分是靠BluetoothDeviceFilter中的取值)

      以上是BluetoothDevicePicker中的一个代码片段,可以看出filter的类型包括
      • FILTER_TYPE_ALL:没有什么限制条件,显示所有蓝牙设备
      • FILTER_TYPE_AUDIO:只显示支持audio协议的蓝牙设备
      • FILTER_TYPE_TRANSFER:只显示支持文件传输的蓝牙设备
      • FILTER_TYPE_PANU:只显示支持个人局域网用户即可以使用个人局域网的蓝牙设备
      • FILTER_TYPE_NAP:只显示支持开启个人局域网的蓝牙设备

      所以我们说该界面是加载说有类型的蓝牙设备。

    • 说明一下,在蓝牙扫描到设备后会缓存起来,通过addCachedDevices方法,就算不经过扫描也可以获取到曾经缓存起来的蓝牙设备。
    • 在开启该activity时,如果满足扫描条件的话,也会进行蓝牙扫描。
    • 在蓝牙状态发生改变并且当前蓝牙状态为开启时也会进行扫描

  • 设备的点击事件?
    设备点击事件onDevicePreferenceClick中处理,针对设备点击事件有两个分支
    • 第一种情况,远程设备已经与本机设备配对或者是远程设备在接收文件时不需要授权即不需要配对的
    • 这种情况下,会调用sendDevicePickedIntent确认已经选择成功,并结束当前界面

    • 第二种情况,远程设备未与本机设备配对,并且远程设备在接收文件时需要授权的。
    • 在点击时,当前界面不会结束,会先去调用父类的方法进行配对,配对成功后发送广播,触发该类中的onDeviceBondStateChanged方法,在该方法中检测到所配对的设备就是所选择的设备后重复第一种情况的行为

    所以总结下就是,在点击选择蓝牙设备时,如果设备已和本机设备完成配对,则finish掉该activity并且调用sendDevicePickerIntent方法。如果设备不需要授权即不需要配对就可以发送文件则也是调用sendDevicePickerIntent。如果设备需要授权且未配对的情况下就会去调用父类的方法进行配对操作,配对成功后依旧是调用sendDevicePickerIntent方法。所以就是在保证设备可以接受文件后就调用sendDevicePickerIntent方法。接下来看该方法的具体实现

 private void sendDevicePickedIntent(BluetoothDevice device) 
        Intent intent = new Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
        if (mLaunchPackage != null && mLaunchClass != null) 
            intent.setClassName(mLaunchPackage, mLaunchClass);
        
        getActivity().sendBroadcast(intent);
    

在该方法中会发送一个广播。所发送的广播的action为ACTION_DEVICE_SELECTED,携带的字段有EXTRA_DEVICE。LaunchPackage和launchclass是在创建activity时所携带过来的信息,具体参考DevicePickerActivity代码分析注释。归根结底,该方法就是去启动BlueoothOppReceiver。

好了,现在DevicePickerActivity这个界面和文件都分析完了,接下来要进去下一个界面文件BluetoothOppReceive分析了。

在选择蓝牙设备后发送的广播为BluetoothDevicePicker.ACTION_DEVICE_SELECTED,所以看receiver中个对于该广播的处理

/**
用于处理蓝牙文件传输:包括系统广播,来自其他应用的intents,来自OppService的Intents,来自Opp应用层其他模块的Intents
 * Receives and handles: system broadcasts; Intents from other applications;
 * Intents from OppService; Intents from modules in Opp application layer.
 */
public class BluetoothOppReceiver extends BroadcastReceiver 
.....
//接受到向其他蓝牙设备发送文件的广播
 if (action.equals(BluetoothDevicePicker.ACTION_DEVICE_SELECTED)) 
            BluetoothOppManager mOppManager = BluetoothOppManager.getInstance(context);
            //获取到远程蓝牙设备信息,即获取到文件接收者
            BluetoothDevice remoteDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

            if (V) Log.v(TAG, "Received BT device selected intent, bt device: " + remoteDevice);

            // Insert transfer session record to database
            //开始传输文件。将要分享的文件插入到db中
            mOppManager.startTransfer(remoteDevice);

            // Display toast message
            String deviceName = remoteDevice.getName();
            String toastMsg;
            //传输的文件数量
            int batchSize = mOppManager.getBatchSize();
            if (mOppManager.mMultipleFlag) 
            //如果是发送多个文件,获取对应toast信息
                toastMsg = context.getString(R.string.bt_toast_5, Integer.toString(batchSize),
                        deviceName);
             else 
            //如果是发送单个文件,获取对应toast信息
                toastMsg = context.getString(R.string.bt_toast_4, deviceName);
            
            Toast.makeText(context, toastMsg, Toast.LENGTH_SHORT).show();
         
        ....
 

可以看到BlueoothOppReceiver其实是做了两件事儿

  1. 一是把文件传输这件事儿插入到传输队列中去传输
  2. 二是toast一条信息告诉用户,正在向谁传输文件,以及传输文件的数量

BlueoothOppReceier到这里结束了,紧接着去看mOppManager的startTransfer方法。方法定义在BluetoothOppManager

 /**
     * Fork a thread to insert share info to db.
     */
    public void startTransfer(BluetoothDevice device) 
        if (V) Log.v(TAG, "Active InsertShareThread number is : " + mInsertShareThreadNum);
        InsertShareInfoThread insertThread;
        synchronized (BluetoothOppManager.this) 
        //ALLOWED_INSERT_SHARE_THREAD_NUMBER的取值为3,mInsertShareThreadNum 是在每次创建文件传输线程时值会++,在线程结束后值会--文件传输线程数量如果大于3,则报错
            if (mInsertShareThreadNum > ALLOWED_INSERT_SHARE_THREAD_NUMBER) 
                Log.e(TAG, "Too many shares user triggered concurrently!");

                // Notice user
                Intent in = new Intent(mContext, BluetoothOppBtErrorActivity.class);
                in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                in.putExtra("title", mContext.getString(R.string.enabling_progress_title));
                in.putExtra("content", mContext.getString(R.string.ErrorTooManyRequests));
                mContext.startActivity(in);

                return;
            
            //创建文件传输线程
            insertThread = new InsertShareInfoThread(device, mMultipleFlag, mMimeTypeOfSendingFile,
                    mUriOfSendingFile, mNameOfSendingFile, mMimeTypeOfSendingFiles, mUrisOfSendingFiles,
                    mIsHandoverInitiated);
            if (mMultipleFlag) 
            //如果是多文件传输,把要传输的文件数量存在mfileNumInBatch字段中
                mfileNumInBatch = mUrisOfSendingFiles.size();
            
        
         //开启文件传输线程
        insertThread.start();
    

可以看到,在startTransfer方法中,首先会去判断文件传输线程是否超过上限(最大值为3),注意,这里所说的不是说文件传输数量,而是文件传输线程,由上述分析可知每当选择一个蓝牙设备进行分享时就会去创建一个文件传输线程。所以这里的上限是说在同一时刻最多可以向3个设备发送文件
判断之后当然会有两个结果,如果超过了最大值则会报错,并且结束本次传输。如果没有超过文件传输线程所限定的最大值,则会继续创建文件分享线程去分享文件。所以,接下来就是要分析文件分享线程,线程代码依旧位于BluetoothOppManager类中。

/**线程用于将传输的文件插入到db中,因为当传输多个文件时(以传输100个文件为例)会是一个耗时操作,所以需要开启线程来处理。可以创建多个线程来实现对多个设备的文件传输。
     * Thread to insert share info to db. In multiple files (say 100 files)
     * share case, the inserting share info to db operation would be a time
     * consuming operation, so need a thread to handle it. This thread allows
     * multiple instances to support below case: User select multiple files to
     * share to one device (say device 1), and then right away share to second
     * device (device 2), we need insert all these share info to db.
     */
    private class InsertShareInfoThread extends Thread 
        private final BluetoothDevice mRemoteDevice;

        private final String mTypeOfSingleFile;

        private final String mUri;

        private final String mNameOfSingleFile;

        private final String mTypeOfMultipleFiles;

        private final ArrayList<Uri> mUris;

        private final boolean mIsMultiple;

        private final boolean mIsHandoverInitiated;

        public InsertShareInfoThread(BluetoothDevice device, boolean multiple,
                String typeOfSingleFile, String uri, String nameOfSingleFile,
                String typeOfMultipleFiles, ArrayList<Uri> uris,
                boolean handoverInitiated) 
            super("Insert ShareInfo Thread");
            //远程设备信息
            this.mRemoteDevice = device;
            //是否是要传输多个文件
            this.mIsMultiple = multiple;
            //传输的单个文件类型
            this.mTypeOfSingleFile = typeOfSingleFile;
            //传输单个文件的uri
            this.mUri = uri;
           //传输的单个文件的name
            this.mNameOfSingleFile = nameOfSingleFile;
            //传输多个文件的文件类型
            this.mTypeOfMultipleFiles = typeOfMultipleFiles;
            //传输多个文件的uris
            this.mUris = uris;
            //传输是否已经通过WiFi ,nfc等被初始化了
            this.mIsHandoverInitiated = handoverInitiated;

            synchronized (BluetoothOppManager.this) 
            //同步锁,对线程数量进行一个增量计算
                mInsertShareThreadNum++;
            

            if (V) Log.v(TAG, "Thread id is: " + this.getId());
        

        @Override
        public void run() 
    //设置线程优先级为后台Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            if (mRemoteDevice == null) 
                Log.e(TAG, "Target bt device is null!");
                return;
            
            if (mIsMultiple) 
                //传输多个文件
                insertMultipleShare();
             else 
                //传输单个文件
                insertSingleShare();
            
            synchronized (BluetoothOppManager.this) 
            //在线程完成文件插入到db的操作后,对线程数量进行减量计算
                mInsertShareThreadNum--;
            
        

        /**
        插入多个文件到db,只能被OPP应用程序调用
         * Insert multiple sending sessions to db, only used by Opp application.
         */
        private void insertMultipleShare() 
            int count = mUris.size();
            Long ts = System.currentTimeMillis();
            for (int i = 0; i < count; i++) 
                Uri fileUri = mUris.get(i);

                BluetoothOppSendFileInfo fileInfo = BluetoothOppUtility.getSendFileInfo(fileUri);
                ContentValues values = new ContentValues();
                values.put(BluetoothShare.URI, fileUri.toString());

                ContentResolver contentResolver = mContext.getContentResolver();
                fileUri = BluetoothOppUtility.originalUri(fileUri);
                String contentType = contentResolver.getType(fileUri);
                if (V) Log.v(TAG, "Got mimetype: " + contentType + "  Got uri: " + fileUri);
                if (TextUtils.isEmpty(contentType)) 
                    contentType = mTypeOfMultipleFiles;
                
                values.put(BluetoothShare.MIMETYPE, contentType);
                values.put(BluetoothShare.DESTINATION, mRemoteDevice.getAddress());
                values.put(BluetoothShare.TIMESTAMP, ts);
                values.put(BluetoothShare.FILENAME_HINT, fileInfo.mFileName);
                if (mIsHandoverInitiated) 
                    values.put(BluetoothShare.USER_CONFIRMATION,
                            BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED);
                
                final Uri contentUri = mContext.getContentResolver().insert(
                        BluetoothShare.CONTENT_URI, values);
                if (V) Log.v(TAG, "Insert contentUri: " + contentUri + "  to device: "
                            + getDeviceName(mRemoteDevice));
            
        

         /**
         插入单个文件到db,只能被Opp应用程序调用
         * Insert single sending session to db, only used by Opp application.
         */
        private void insertSingleShare() 
            ContentValues values = new ContentValues();
            //问价uri
            values.put(BluetoothShare.URI, mUri);
            //文件名
            values.put(BluetoothShare.FILENAME_HINT, mNameOfSingleFile);
            //文件类型
            values.put(BluetoothShare.MIMETYPE, mTypeOfSingleFile);
            //文件接收者的设备地址
            values.put(BluetoothShare.DESTINATION, mRemoteDevice.getAddress());
            //是否已经被用户确认
            if (mIsHandoverInitiated) 
                values.put(BluetoothShare.USER_CONFIRMATION,
                        BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED);
            
            final Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI,
                    values);
            if (V) Log.v(TAG, "Insert contentUri: " + contentUri + "  to device: "
                                + getDeviceName(mRemoteDevice));
        
    

第一,传入线程所需参数
第二,设置线程优先级为后台,这样可以减少对cpu的占用,当多个分享线程并发时减少对主线程的影响。
第三,记录文件分享线程数量值mInsertShareThreadNum
第四,将要传输的文件插入到db中,如果是多个文件就调用insertMultipleShare插入,如果是单个文件就调用insertSingleShare插入。
该线程也就这么点儿事儿,也许到现在你该奇怪了,文件传输到底在哪儿?怎么就把文件插入到db就结束了??
难道你以为insert就只是简简单单的插入db中吗??那你就大错特错了。到现在为止,你将你要传输的文件交给了db,接下来就是ContentProvider的处理了。在介绍provider中的处理之前,先总结下从开始分享到交给provider的流程。

大致流程如下:
第一步,BluetoothOppLauncherActivity文件,选择蓝牙分享后会启动该activity(但是该activity主题为透明的,所以相当于瞒着用户启动了一个界面),在启动之后会进行判断是否开启蓝牙,如果没有开启就去开启蓝牙,如果蓝牙已经开启就打开蓝牙选择界面
第二步,DevicePickerActiviy文件:蓝牙选择界面。首先会负责加载蓝牙设备,在点击选择蓝牙设备后会先去判断是否可以发送文件(本机设备是否和远程蓝牙设备已完成配对,或者是远程蓝牙设备在接受文件时是否要授权)。如果可以就发送广播触发BluetoothOppReceiver,如果不可以就去营造条件
第三步,BluetoothOppReceive文件:一是通知系统要发送文件二是通知用户
第四步,BluetoothOppManager文件:启动线程将要发送的文件插入到db中。

在插入db时,uri为:

/**
     * The content:// URI for the data table in the provider
     */
    public static final Uri CONTENT_URI = Uri.parse("content://com.android.bluetooth.opp/btopp");

根据uri的域名com.android.bluetooth.opp找到所对应的provider为BluetoothOppProvider。进入到该文件中看insert方法

sURIMatcher.addURI("com.android.bluetooth.opp", "btopp", SHARES);
        sURIMatcher.addURI("com.android.bluetooth.opp", "btopp/#", SHARES_ID);

匹配BluetoothShare.CONTENT_URI的id为SHARES



BluetoothOppProvider中的insert方法起到两个作用

  1. 将要发送的文件插入到db中
  2. 启动BluetoothOppService去传输文件

代码分析:

  • 代码236-242行,因为在这里还需要对key所对应的value进行判断或者赋值,所以先将values中的一部分key对应的值复制到filteredValues
    1. BluetoothShare.URI:所发送或者接受的文件的uri
    2. BluetoothShare.FILENAME_HINT:文件名
    3. BluetoothShare.MIMETYPE:文件类型
    4. BluetoothShare.DESTINATION:目标设备
  • 第246行,BluetoothShare.DIRECTION:获取到文件传输方向。这是因为文件传入和传出的处理在一个类中,只是标志不同而已。如果是传出文件,DIRECTION对应的value为null,此时系统会将传输方向赋值为BluetoothShare.DIRECTION_OUTBOUND即传出文件(代码250行-251行)
  • 第247行,BluetoothShare.USER_CONFIRMATION:是否需要用户确认。即在传出文件时是否需要用户确认文件传出?有6个取值
    • BluetoothShare.USER_CONFIRMATION_PENDING:This transfer is waiting for user confirmation,等待用户确认传输
    • BluetoothShare.USER_CONFIRMATION_CONFIRMED:This transfer is confirmed by user用户已经确认传输
    • BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED: This transfer is auto-confirmed per previous user confirmation通过先前的用户信息自动确认
    • BluetoothShare.USER_CONFIRMATION_DENIED:This transfer is denied by user文件传输被拒绝
    • BluetoothShare.USER_CONFIRMATION_TIMEOUT:This transfer is timeout before user action,文件传输超时
    • BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED:This transfer was initiated by a connection handover(for example WIFI, NFC) and has been auto-confirmed ,传输是由连接的切换引起的(比如WiFi或者nfc),并且已经自动确认

    如果是传出文件,则用户无需手动确认,也正如平常所见,在使用蓝牙分享文件时不会去询问用户是否分享。在253-254行代码对con进行赋值

  • 第256-257行代码,如果是传入文件,则需要用户确认,即在蓝牙传入文件时会需要用户选择是否接受文件,如果用户经过一定时间未处理,则会出现文件传输超时的问题,对于文件传输超时的时间的定义的字段为BluetoothOppObexSession.SESSION_TIMEOUT=5000,即如果文件接受方在5秒之内没有处理文件发送请求,那么文件就会传输超时停止传输。

    注,题外话,对于文件超时的判断机理如下,在开始发送一个文件时延时SESSION_TIMEOUT向一个handler发送message,在对方开始接受文件后就移除该messge。一般源码上的一些判断超时的操作的机制大抵如此:即在事件开始之时延时启动线程或者是handler之类,所延时的时间即为判断超时的时间,待事件开始处理后就移除刚才的所要延时触发的动作。这种设计方式运用到应用开发中也是极好的。所以在研究源码的过程中不仅要明白这段代码是什么意思,更要看这段代码的实现原理有什么值得学习的地方
  • 271-272和279-280行代码均是启动service,一直没明白为什么要这样做。明明启动了,为什么还要再启动一遍?暂且留个悬念吧
  • 第274行代码:将要分享的文件插入插入数据库中
  • 第281行代码:用于触发该provider所注册的observer。只有显示的调用notifyChange方法,才会回调用户自定义的观察者的observe的onChange

ok,到现在为止,也差不多了,本以为传出文件代码会很好分析,没想到断断续续分析了这么多天,而且篇幅这么长,依旧没有完…….我也是很无语,不知不觉就罗嗦了一大堆,紧接着就该BluetoothOppService来分析了,也该进入下一篇了。太长的篇幅知道你们也没耐心看哈哈哈
转载请注明出处,本文出自fanfan带你解锁蓝牙skill系列

以上是关于智能合约自动检测工具『链必验』,如何带你解锁Web3.0世界的主要内容,如果未能解决你的问题,请参考以下文章

带你解锁蓝牙skill

带你解锁蓝牙skill

学习Salesforce | 带你解锁Superbadge的真正作用

带你解锁蓝牙skill

带你解锁蓝牙skill

只会用 WiFi 连接 Blynk 物联网?带你解锁三种新姿势!