Android 物联网应用开发实例
Android
可以采用 Kotlin、Java、C++ 语言编写应用程序,Android SDK
会将这些代码连同相应的数据和资源文件编译为 Android
软件包,即一个带有.apk
后缀的归档文件,也就是 Android
应用程序的安装文件。本质上 Android 系统是一种多用户的 Linux
系统,每个应用程序都运行在独立的 Linux 用户 ID
和进程之下,从而为每个 Android
应用都提供了独立的安全沙盒,体现了最小权限的设计原则。
鉴于 Google 官方提供了完善的文档,本文并不过多过深的涉及 Android SDK 开发的具体知识细节,仅会在简单介绍 Android 开发当中的一些基本概念之后,着重分析经典/低功耗蓝牙、NFC、WIFI、指纹识别、5G 等硬件外设的通信协议概念以及相应的实现步骤,并且展示一些比较典型的应用场景与示例代码,从而为读者在进行物联网相关项目的开发时,在移动设备应用控制端提供即有的现成经验。
Hello Android
打开 Android Studio,鼠标点击【+ Start a new Android Studio project】选项:
在 Create New Project 窗口当中,选择【Empty Activity】,然后点击【Next】:
在 Configure your project 窗口当中,填写如下信息:
经过上述步骤新建的 Hello Android 项目下面有如下 4 个比较重要的源文件:
MainActivity.java
app > java > com.example.myfirstapp > MainActivity
,主
Activity 是应用程序的入口点,程序运行时会首先启动该 Activity
的实例并且加载其对应的布局。
1 | package com.example.helloandroid; |
activity_main.xml
app > res > layout > activity_main.xml
,用于定义主
Activity 的 XML 界面布局文件。
1 |
|
AndroidManifest.xml
工程清单文件app > manifests > AndroidManifest.xml
,用于描述应用程序的基本特性,定义应用程序所使用到的组件。
1 |
|
build.gradle
Gradle Scripts > build.gradle
存在 2
个同名文件,分别针对项目Project: Hello_Android
和模块Module: app
,Android
应用的每个模块都拥有各自的build.gradle
文件,本项目当前仅拥有app
一个模块。
build.gradle (Project: Hello_Android)
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. |
build.gradle (Module: app)
1 | apply plugin: 'com.android.application' |
核心概念
基本组件
Android
应用组件拥有活动Activity
、服务Service
、广播接收器BroadcastReceiver
、内容提供器ContentProvider
共
4 种基本类型:
- 活动
Activity
:是用户交互的入口点,表示的是拥有界面的单个屏幕,通过继承Activity
类来实现; - 服务
Service
:表示一种运行在后台的组件,并不会提供界面,用于执行长时间运行的操作或者为其它进程执行任务,通过继承Service
类来实现; - 广播接收器
BroadcastReceiver
:Android 借助该组件向应用传递事件,从而允许应用响应系统范围的广播通知。虽然广播接收器不会显示界面,但是可以创建状态栏通知以提醒用户,通过继承BroadcastReceiver
类实现; - 内容提供器
ContentProvider
:用于管理共享的应用数据,可以将这些数据存储在文件系统、SQLite 数据库、网络或者应用可访问的其它存储位置。其它应用则可以通过内容提供器查询、修改数据,通过继承ContentProvider
类实现;
组件启动
上述 4
种组件类型当中,Activity
、Service
、BroadcastReceiver
三种均可以通过异步消息Intent
进行启动,Intent
会在运行时将各个组件绑定在一起,因此可以将Intent
视为组件间操作的纽带。Intent
通过Intent
对象进行创建,该对象可以通过定义消息来启动特定组件(显式)或者组件类型(隐式)。
使用Intent
可以启动Activity
、Service
、BroadcastReceiver
组件,我们即可以在Intent
使用类名显式声明目标组件,也可以使用隐式的Intent
来描述所要执行的操作类型与待操作数据。如果当前存在多个可执行Intent
描述操作的组件,则由用户通过过滤器选择具体使用哪一个。
对于Activity
和Service
,Intent
会定义所要执行的操作,并且可以指定待操作数据的URI
,以及当前正在启动组件所需的信息。对于BroadcastReceiver
,Intent
仅会定义当前等待广播的通知信息。而ContentProvider
并非由Intent
启动,它只会在成为内容解析器ContentResolver
的请求目标时才会启动。
- 启动
Activity
,如果需要让Activity
返回结果,可以向startActivity()
或者startActivityForResult()
传递Intent
,或者为其安排其它任务; - 通过向
startService()
传递Intent
启动Service
,或者可以向bindService()
传递Intent
来绑定到该服务; - 发起广播,可以向
sendBroadcast()
、sendOrderedBroadcast()
、sendStickyBroadcast()
等方法传递Intent
; - 为
ContentProvider
提供查询服务,可以在ContentResolver
上调用query()
;
工程清单文件
Android
系统启动应用组件之前,必须通过读取工程清单文件AndroidManifest.xml
确认组件存在,因此必须在该文件中声明所有组件,且该文件必须位于工程的根目录。除此之外,清单文件还拥有如下的用途:
- 声明应用所需的权限,例如:互联网访问权限或者联系人的读取权限;
- 声明应用所需的最低 API 级别;
- 声明应用所需的软硬件功能,例如:相机、蓝牙、多点触摸屏幕;
- 声明应用链接的 API 库,例如:Google 地图库;
下面代码中,<application>
元素的android:icon
属性用于标识应用的图标,<activity>
元素的android:name
属性指定了对应Activity
子类的完整类名,android:label
属性指定用于Activity
的用户可见标签字符串。
1 |
|
活动Activity
、服务Service
、广播接收器BroadcastReceiver
、内容提供器ContentProvider
分别对应AndroidManifest.xml
当中的<activity>
、<service>
、<receiver>
、<provider>
元素。如果没有在工程清单文件中提供这些内容,那么这些组件在系统运行时是不可见的,永远都不会得到执行。
组件 XML 声明
上述的Activity
、Service
、BroadcastReceiver
组件都可以使用Intent
进行启动,即可以通过在Intent
中显式命名目标组件的类名来使用Intent
,还可以使用隐式Intent
来描述所要执行的操作类型和数据。通过隐式Intent
,Android
可以在设备上查询并启动可以执行该操作的组件。如果存在多个组件可以执行Intent
所描述的操作,则由开发人员选择具体使用哪个组件。
通过将接收到的Intent
与设备上其它应用的AndroidManifest.xml
工程清单文件上的Intent
过滤器相比较,Android
系统就可以正确的查询出可以响应该Intent
的组件。AndroidManifest.xml
当中声明Activity
时,可以添加<intent-filter>
元素作为Activity
的子元素,从而为该组件声明一个Intent
过滤器。
例如,构建一个包含撰写新邮件Activity
的电子邮件应用程序,可以通过声明Intent
过滤器来响应名称为send
的Intent
来实现发送新邮件的目的:
1 |
|
如果设备上另一个应用创建包含有ACTION_SEND
操作的Intent
并将其传递给startActivity()
,则系统就会启动该Activity
完成邮件发送操作。
其它 XML 声明
Android
设备比较碎片化,并非所有设备都提供相同的特性与功能,为了防止将应用安装在缺少相关特性的设备上,必须在AndroidManifest.xml
工程清单文件中声明设备与软件要求。Android
操作系统本身并不会读取其中的大部分声明,但是Google Play
等应用商店会读取这些信息,从而便于用户搜索应用提供相应的过滤功能。
例如,如果当前设备的应用需要相机功能,并使用Android 2.1(API 级别 7)
中引入的
API,则可以在AndroidManifest.xml
中声明如下要求:
1 |
|
,没有相机且 Android 版本低于 2.1 的设备将无法从 Google Play 安装您的应用。不过,您可以声明您的应用使用相机,但并不要求必须使用。在此情况下,您的应用必须将 required 属性设置为 false,并在运行时检查设备是否拥有相机,然后根据需要停用任何相机功能。
应用资源
Android 应用当中的图片、音频等资源,Android SDK
都会为其定义唯一的整型ID,在工程当中可以利用该ID引用这些资源。例如,SDK
会为工程res/drawable/
目录下的logo.png
图像文件生成名为R.drawable.logo
的整型资源
ID,然后在代码中就可以通过该 ID 引用图像。
通过代码与资源的分离,可以方便的为不同设备提供对应的备用资源,Android
支持许多不同的备用资源限定符。限定符是资源目录名称中加入的短字符串,用于定义这些资源适用的设备配置。例如:根据设屏幕方向与尺寸为Activity
创建不同的布局,当需要更换布局时,可以对每个布局的目录名称采用限定符,这样系统就会根据当前设备的水平和垂直方向自动应用对应的布局。
经典蓝牙
传统蓝牙适用于较为耗电的操作,可用于 Android
设备之间数据流的传输等场景。Android SDK 提供的 Bluetooth API
可以完成蓝牙通信的 4
大任务:设置蓝牙
、查找区域内的配对设备或者可用设备
、连接设备
、在设备之间传输数据
。
蓝牙设备之间进行数据传输之前,首先必须通过配对形成通信通道,即将其中一台设备设置为可检测状态,另一台设备通过搜索发现该设备,两个设备配对期间会交换并且缓存安全密钥,以供下次连接使用。配对完成以后,两台设备即可开始进行数据传输。会话完成以后,两台设备仍将维持绑定状态,未来如果需要再次打开连接会话,则需要两个设备在有效通信距离内均未移除绑定,即可自动完成连接。
权限
Android
应用中使用蓝牙功能,必须在工程清单文件AndroidManifest.xml
声明如下
3 个权限:
BLUETOOTH
权限:需要该权限才能执行蓝牙通信任务;BLUETOOTH_ADMIN
权限:用于启动设备发现或者操作设备的蓝牙设置;ACCESS_FINE_LOCATION
权限:由于蓝牙扫描可用于收集用户位置信息,此类信息通常来自用户设备或者各种外设蓝牙信标;
1 | <manifest ... > |
蓝牙 Profile
蓝牙 Profile 是适用于蓝牙设备之间不同应用场景的一系列协议栈,Android 的蓝牙 API 为如下 Profile 提供了实现:
- 蓝牙耳机:提供
BluetoothHeadset
类控制蓝牙耳机服务代理; - 蓝牙立体声音频传输:提供
BluetoothA2dp
类控制蓝牙 A2DP 服务代理; - 健康设备:提供 Bluetooth Health API
控制蓝牙健康设备,包含有
BluetoothHealth
、BluetoothHealthCallback
、BluetoothHealthAppConfiguration
类;
蓝牙 Profile 的基本使用步骤如下所示:
- 获取默认适配器;
- 设置
BluetoothProfile.ServiceListener
监听BluetoothProfile
客户端,在其连接或者断开服务时向其发送通知; - 使用
getProfileProxy()
与 Profile 关联的设备对象建立连接; - 通过
onServiceConnected()
获取 Profile 代理对象的句柄; - 获得 Profile 代理对象后,可以监视连接状态,并执行与其相关的其它操作;
下面的示例代码,Profile 的代理对象是 1
个用于控制耳机的BluetoothHeadset
实例:
1 | BluetoothHeadset bluetoothHeadset; |
设置蓝牙
如果当前 Android 设备不支持蓝牙,则应该停用应用的蓝牙功能;如果设备支持蓝牙,但是已经停用该功能,则应在不离开应用的同时启用蓝牙。
所有蓝牙相关的Activity
都需要使用BluetoothAdapter
(代表当前设备的蓝牙适配器),通过调用静态的getDefaultAdapter()
方法可以获取BluetoothAdapter
对象,如果该方法返回null
,则表示设备不支持蓝牙。
1 | BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
接下来需要调用isEnabled()
判断蓝牙是否启用,该方法返回false
表示蓝牙处于停用状态,那么调用startActivityForResult()
可以启用蓝牙,并且传入ACTION_REQUEST_ENABLE
作为
Intent 参数向系统设置请求启用蓝牙。
1 | if (!bluetoothAdapter.isEnabled()) { |
此时系统显示【请求用户允许启用蓝牙】的对话框,如果选择【Yes】就会启用蓝牙,完成后焦点将会返回应用。
传递给startActivityForResult()
的REQUEST_ENABLE_BT
常量为局部定义的正整数,Android
系统会将该常量回传至onActivityResult()
的requestCode
参数。成功启用蓝牙以后,Activity
就会在onActivityResult()
回调中接收到RESULT_OK
结果代码,如果蓝牙启动失败,则返回的结果代码为RESULT_CANCELED
。
注意:除此之外,每当系统蓝牙状态发生变化时,系统都会广播
ACTION_STATE_CHANGED
这个 Intent。该广播包含EXTRA_STATE
(目前的蓝牙状态)和EXTRA_PREVIOUS_STATE
(之前的蓝牙状态)两个额外字段。这些额外字段的取值包含STATE_TURNING_ON
、STATE_ON
、STATE_TURNING_OFF
、STATE_OFF
。
查找设备
BluetoothAdapter
还可用于扫描附近可被发现的蓝牙设备,这些设备会共享名称、类型、MAC
地址来响应扫描请求。当扫描设备与可被发现设备建立连接以后,Android
系统就会自动向用户显示配对请求。完成配对以后,Android
系统会保存该设备的名称、类型、MAC 地址,借助这些设备的 MAC
地址,可以随时向其发起连接,而无需再次执行发现操作。
调用BluetoothAdapter
提供的getBondedDevices()
,可以返回一组表示已经配对设备的BluetoothDevice
对象,进而可以查询每台设备的名称和
MAC 地址。
1 | Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices(); |
注意:调用
startDiscovery()
可以发现附近的蓝牙设备(12 秒),该操作为异步的进程,最后返回一个用于标示发现进程是否成功启动的布尔值。
Android 系统会为每台设备广播ACTION_FOUND
这个
Intent,因此必须为其注册一个BroadcastReceiver
,以便接收每台被发现设备的相关信息。该
Intent
包含EXTRA_DEVICE
(包含BluetoothDevice
类)和EXTRA_CLASS
(包含BluetoothClass
类)两个额外字段。下面代码段展示了发现设备时,如何处理ACTION_FOUND
广播:
1 |
|
使用ACTION_REQUEST_DISCOVERABLE
这个 Intent
调用startActivityForResult(Intent, int)
方法,可以将本机设备设置为可检测状态,这样可以避免进入到设置界面进行相关操作。Android
设备默认处于可检测模式的时间为120
秒,通过设置EXTRA_DISCOVERABLE_DURATION
这个额外属性定义持续时间,最高设置达3600
秒。下面示例将
Android 设备的蓝牙可被检测时间设置为300
秒:
1 | Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); |
此时 Android
系统会显示【请求用户允许将设备设为可检测模式】对话框,如果用户选择【Yes】,则设备进入可检测模式,并在指定时间内保持该模式。然后
Activity
将会执行onActivityResult()
回调函数。如果用户选择【No】则结果代码为RESULT_CANCELED
。
如果希望设备检测状态发生变化时接收通知,则可以考虑为ACTION_SCAN_MODE_CHANGED
这个
Intent
注册BroadcastReceiver
。该Intent
包含EXTRA_SCAN_MODE
和EXTRA_PREVIOUS_SCAN_MODE
两个额外字段,每个属性可以包含这些值:SCAN_MODE_CONNECTABLE_DISCOVERABLE
(设备处于可检测模式)、SCAN_MODE_CONNECTABLE
(设备未处于可检测模式,但是仍然能够接收连接)、SCAN_MODE_NONE
(设备未处于可检测模式,并且无法接收到连接)。
连接设备
两台连接的设备必须分别同时实现服务端(开放服务器
Socket 服务)和客户端(使用服务器设备的 MAC
地址发起连接),当打开设备之间连接的 RFCOMM
通道以后,通过BluetoothSocket
即可完成双向的流式数据传输。
服务端
服务端设备需要保持开放的BluetoothServerSocket
监听听传入的连接请求,并在接受到请求后提供已连接的BluetoothSocket
(即从BluetoothServerSocket
获取BluetoothSocket
)。设置
Socket 服务并接受连接,需要依次实现如下步骤:
- 调用
listenUsingRfcommWithServiceRecord(String, UUID)
获取BluetoothServerSocket
。其中参数String
是服务的可识别名称,参数UUID
是 128 位的通用唯一标识符,用于对应用的蓝牙服务进行唯一化标识;这 2 个参数都会被写入新服务发现协议 (SDP)的数据库。 - 调用
accept()
监听连接请求,该函数为阻塞调用,当服务器接受连接或者发生异常,该调用就会中断返回。仅当客户端发送包含UUID
的连接请求,且该UUID
与服务端注册的UUID
匹配时,服务端才会接受连接。连接成功以后,accept()
返回已经连接的BluetoothSocket
; - 调用
close()
关闭连接,释放服务端 Socket 及其占用的资源,但是并不会关闭accept()
返回的已连接BluetoothSocket
。RFCOMM 只允许每个通道存在 1 个已连接客户端,因此服务端接收到已连接的 Socket 之后,就可以立刻在BluetoothServerSocket
上调用close()
;
1 | private class AcceptThread extends Thread { |
注意:
BluetoothServerSocket
和BluetoothSocket
中的所有方法都是线程安全的方法。由于accept()
是阻塞调用,因此不能在主 Activity 界面线程执行该调用,通常需要在一个新的线程中完成所有涉及BluetoothServerSocket
或者BluetoothSocket
的工作。如果要取消accept()
等被阻塞的调用,同样通过另一个线程,在BluetoothServerSocket
或者BluetoothSocket
上调用close()
。
客户端
客户端必须首先获取表示该远程设备的BluetoothDevice
对象,然后从中获取BluetoothSocket
并且发起连接,基本步骤如下所示:
- 使用
BluetoothDevice
,通过调用createRfcommSocketToServiceRecord(UUID)
获取BluetoothSocket
。该方法会初始化BluetoothSocket
对象,以用于客户端连接至BluetoothDevice
。此处UUID
必须与服务端listenUsingRfcommWithServiceRecord(String, UUID)
中的UUID
保持一致。 - 通过调用
connect()
发起连接,客户端调用该方法以后,Android 会在新服务发现协议(SDP)数据库中查找UUID
匹配的远程服务,如果查找并且连接成功,就会共享 RFCOMM 通道,同时connect()
方法将会返回。如果连接失败,或者connect()
超时约 12 秒以后,则该方法将会引发IOException
。connect()
也是阻塞调用,因此需要在主 Activity 之外的线程中执行此连接操作。
1 | private class ConnectThread extends Thread { |
数据读写
当成功连接多台设备以后,每台设备都会拥有已经完成连接的BluetoothSocket
,接下来通过BluetoothSocket
读写数据的步骤如下所示:
- 使用
getInputStream()
和getOutputStream()
,分别获取通过 Socket 处理数据传输的InputStream
和OutputStream
类; - 使用
read(byte[])
和write(byte[])
读取或者写入数据流,由于这 2 个方法都属于阻塞调用,因此需要使用单独的线程来进行读写操作。
1 | public class MyBluetoothService { |
低功耗蓝牙
NFC
Android 物联网应用开发实例