1. 介绍
HarmonyOS支持应用以Ability为单位进行部署,Ability可以分为FA(Feature Ability)和PA(Particle Ability)两种类型,本篇Codelab将会使用到Page Ability以及Service Ability来进行开发,其中Page Ability是FA唯一支持的模板,用于提供与用户交互的能力,Service Ability是PA的一种,用于提供后台运行任务的能力。
分布式运动健康Codelab包含两个HarmonyOS应用(手机端和智能穿戴端),本篇要介绍的是基于手机的HarmonyOS分布式运动健康应用,在这个应用中,您将使用到HarmonyOS中的常用Java UI布局DirectionalLayout,以及跨设备拉起PA服务,跨设备同步数据等能力来共同实现一个基于分布式的HarmonyOS运动健康手机侧应用。
本篇Codelab实现了如下功能:
- 智能穿戴设备的心率和步数数据到手机A的实时传输;
- 实时心率异常提醒;
- 手机A得到的健康数据共享至手机B。
最终效果预览
我们最终会构建一个简易的HarmonyOS分布式运动健康智能穿戴设备客户端和手机客户端。手机应用包含两个页面,分别是健康数据页面和他人数据页面,两个页面都展示了来自于个人智能穿戴设备或他人智能穿戴设备的运动步数和心率数据,其中健康数据页面的实现逻辑中会定时地去读取与之绑定的智能穿戴设备数据,实现跨设备同步数据。效果如下图所示。本篇Codelab我们将会一起完成这个客户端,其中包括:
将步数转换成距离、热量、爬楼层数;步数小于等于400为低强度,步数大于400小于等于600为中等强度,步数大于600小于等于800为中高强度,步数大于800为高强度;手机端效果展示:
2. 搭建HarmonyOS环境 我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。
- 安装DevEco Studio,详情请参考下载和安装软件。
- 设置DevEco Studio开发环境,DevEco Studio开发环境依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
- 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
- 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
- 开发者可以参考以下链接,完成设备调试的相关配置:
说明
智能穿戴侧应用需要通过运动健康APP完成和手机侧绑定,才可以实现分布式相关调测。
3. 代码结构解读
本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在第十章节参考中提供下载方式,接下来我们会用一小节来讲解整个工程的代码结构:
- annotation:Bind是一个自定义注解,用来初始化页面中的组件。
- Entity:WatchEntity为封装了手表数据的实体类。
- enums:KeyEnum是一个枚举类,用来表示运动健康数据(K-V键值对)的键值。
- factory:MyKvManagerFactory是一个工厂类,通过懒汉式+DCL产生我们需要的单例KvManager。
- slice:HealthSlice是主要代码逻辑实现的一个类,同时对应我们的页面。
- task:NotifyTask是一个通知任务,被手机端的一个PA服务(ServiceAbility)调用,PushDataTask是一个写数据(智能穿戴设备-手机A之间DB数据的一次拷贝)的任务。
- util:存放所有封装好的公共方法,如CommonUtils,LogUtils等。
- view:加载页面相关的类。
- ServiceAbility:一个PA服务类,主要的功能是当智能穿戴设备端发现心率异常,会拉起手机端的这个PA服务(即状态栏收到心率异常通知,点击通知拉起手机FA进入主页面)。
- resources:存放工程使用到的资源文件,其中resourcesbaselayout下存放xml布局文件;resourcesbasemedia下存放图片资源。
- config.json:工程相关配置文件。
4. 页面布局
健康数据页面此页面包含同步手表数据定时器开关,手动同步智能穿戴设备数据,总步数,最新心率以及心率范围的布局,对应的xml文件名为ability_main.xml,下面一一介绍相关布局:
同步智能穿戴设备数据定时器开关对应布局文件如下,具体开关逻辑和样式的设置参考第五章节
- <Switch
- ohos:id="$+id:btn_switch"
- ohos:height="30vp"
- ohos:width="60vp"
- ohos:layout_alignment="left"
- ohos:text_state_off="OFF"
- ohos:text_state_on="ON"/>
复制代码手动同步智能穿戴设备数据对应布局文件如下,点击事件代码请参考第五章节
- <Button
- ohos:id="$+id:button_syncData"
- ohos:height="35vp"
- ohos:width="200vp"
- ohos:background_element="$graphic:step"
- ohos:layout_alignment="horizontal_center"
- ohos:text="同步手表数据"
- ohos:text_color="#FFF7F7FA"
- ohos:text_size="27fp"
- ohos:top_margin="20vp"
- ohos:top_padding="10px"
- />
复制代码运动步数布局文件如下,其中用到了RoundProgressBar组件,详细使用方法可参考RoundProgressBar
- <DirectionalLayout
- ohos:id="$+id:stepLayout"
- ohos:height="240vp"
- ohos:width="370vp"
- ohos:alignment="left"
- ohos:background_element="$graphic:step"
- ohos:top_margin="50vp">
-
- <DirectionalLayout
- ohos:height="match_content"
- ohos:width="match_parent"
- ohos:alignment="vertical_center"
- ohos:orientation="horizontal">
-
- <Text
- ohos:id="$+id:stepTimeText"
- ohos:height="match_content"
- ohos:width="match_content"
- ohos:left_margin="115vp"
- ohos:text="同步时间 00:00"
- ohos:text_alignment="center"
- ohos:text_color="white"
- ohos:text_size="20fp"/>
- </DirectionalLayout>
-
- <DirectionalLayout
- ohos:height="match_content"
- ohos:width="match_parent"
- ohos:alignment="vertical_center"
- ohos:orientation="horizontal">
-
- <RoundProgressBar
- ohos:progress_hint_text="步数0步"
- .....
- />
-
- <RoundProgressBar
- ohos:progress_hint_text="低强度"
- .....
- />
- </DirectionalLayout>
-
- <DirectionalLayout
- ohos:height="match_content"
- ohos:width="match_parent"
- ohos:alignment="vertical_center"
- ohos:orientation="horizontal">
-
- <Text
- ohos:text="距离0.00公里 |"
- .....
- />
-
- <Text
- ohos:text="热量0.00千卡 |"
- .....
- />
-
- <Text
- ohos:text="爬楼0层"
- .....
- />
- </DirectionalLayout>
- </DirectionalLayout>
复制代码心脏健康和心率范围布局文件如下,同步逻辑可参考第六章节
他人数据页面布局与健康页面布局大致相同,对应的文件名为tablist_layout.xml,具体可见参考部分的完整代码。
底部Tab切换布局对应的文件名为root.xml
- <DependentLayout
- ohos:id="$+id:root"
- xmlns:ohos="http://schemas.huawei.com/res/ohos"
- ohos:width="match_parent"
- ohos:height="match_parent">
-
- <DirectionalLayout
- ohos:id="$+id:page_wrap"
- ohos:width="match_parent"
- ohos:height="match_parent"
- ohos:above="$+id:footer"
- ohos:orientation="vertical"
- >
- <ScrollView
- ohos:id="$+id:page"
- ohos:width="match_parent"
- ohos:height="match_parent"
- ></ScrollView>
- </DirectionalLayout>
-
- <Component
- ohos:width="match_parent"
- ohos:height="1vp"
- ohos:above="$+id:footer"
- ohos:background_element="#EDF1F5"
- ></Component>
-
- <DirectionalLayout
- ohos:id="$+id:footer"
- ohos:width="match_parent"
- ohos:height="54vp"
- ohos:align_parent_bottom="true"
- ohos:orientation="horizontal">
- <TabList
- ohos:id="$+id:footer_tab"
- ohos:width="match_parent"
- ohos:height="match_parent"
- ohos:top_padding="8vp"/>
- </DirectionalLayout>
-
- </DependentLayout>
复制代码 5. 应用初始化 当我们首次安装应用打开时会进入到类MainAbility的onStart(Intent intent)方法。这里面做了两件事,设置页面路由和授予权限。
权限配置说明
权限需要在config.json中添加如下配置,同时需要在应用启动时手动授予手机分布式数据同步权限。
- "reqPermissions": [
- {
- "name": "ohos.permission.DISTRIBUTED_DATASYNC"
- },
- {
- "name": "ohos.permission.VIBRATE"
- },
- {
- "name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE"
- },
- {
- "name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO"
- },
- {
- "name": "ohos.permission.GET_BUNDLE_INFO"
- }
- ]
复制代码MainAbility#requestPermission():手动授予权限
- private void requestPermission() {
- if (verifySelfPermission(DISTRIBUTED_DATASYNC) != IBundleManager.PERMISSION_GRANTED) {
- // 校验没有权限时,手动授权
- if (canRequestPermission(DISTRIBUTED_DATASYNC)) {
- // toast
- requestPermissionsFromUser(new String[] {DISTRIBUTED_DATASYNC}, PERMISSION_CODE);
- }
- }
- }
复制代码 应用加载流程在HealthSlice类的onStart(Intent intent)方法按步骤完成:
- 设置页面UI
- manager = new HomeLayoutManager(this);
- rootLayout = manager.initSliceLayout();
- super.setUIContent(rootLayout);
复制代码
- 初始化页面上的组件,此处只展示部分组件变量,其余部分代码请浏览第十章节
- @Bind(ResourceTable.Id_stepData)
- private RoundProgressBar stepData;
-
- @Bind(ResourceTable.Id_stepTimeText)
- private Text stepTime;
-
- .....
-
- private void initViewAnnotation() {
- Field[] fields = getClass().getDeclaredFields();
- for (Field field : fields) {
- Bind bind = field.getAnnotation(Bind.class);
- if (bind != null) {
- if (bind.value() == -1) {
- LogUtils.error(TAG, "bind.value must set!");
- return;
- }
- try {
- field.setAccessible(true);
- field.set(this, findComponentById(bind.value()));
- } catch (IllegalAccessException e) {
- LogUtils.error(TAG, "IllegalAccessException :" + e.getMessage());
- }
- }
- }
- }
复制代码
- 给按钮设置监听事件
- private void initListener() {
- // 同步手表数据
- syncData.setClickedListener(
- va -> {
- syncData(singleKvStore);
- });
- // 同步其他手机数据
- syncPhoneData.setClickedListener(
- va -> {
- syncData(singleKvStore2);
- });
- // 定时刷新开关
- aSwitch.setCheckedStateChangedListener(new AbsButton.CheckedStateChangedListener() {
- // 回调处理Switch状态改变事件
- [url=home.php?mod=space&uid=2735960]@Override[/url]
- public void onCheckedChanged(AbsButton button, boolean isChecked) {
- if (isChecked) {
- timer = new Timer();
- initMyHandler();
- }else{
- timer.cancel();
- }
- }
- });
- }
复制代码
- 投递定时任务事件(当开关处于开的状态时,应用启动时默认关闭状态)
- private MyEventHandler myHandler;
-
- private Timer timer ;
-
- private void initMyHandler() {
- myHandler = new MyEventHandler(EventRunner.current());
- long param = 0L;
- Object object = null;
- InnerEvent normalInnerEvent = InnerEvent.get(EVENT_MESSAGE_NORMAL, param, object);
- myHandler.sendEvent(normalInnerEvent, 0, EventHandler.Priority.IMMEDIATE);
- }
-
- // 处理投递的事件
- class MyEventHandler extends EventHandler {
- MyEventHandler(EventRunner runner) throws IllegalArgumentException {
- super(runner);
- }
-
- @Override
- protected void processEvent(InnerEvent event) {
- super.processEvent(event);
- if (event == null) {
- return;
- }
- int eventId = event.eventId;
- switch (eventId) {
- case EVENT_MESSAGE_NORMAL:
- timer.schedule(new TimerTask() {
- public void run() {
- LogUtils.info(",", "定时同步" + "");
- getUITaskDispatcher().syncDispatch(new Runnable() {
- @Override
- public void run() {
- syncData(singleKvStore);
- }
- });
- }
- }, 0, INTERVAL);
- break;
- default:
- break;
- }
- }
- }
复制代码
- 初始化开关样式
- private StateElement trackElementInit(ShapeElement on, ShapeElement off) {
- StateElement trackElement = new StateElement();
- trackElement.addState(new int[]{ComponentState.COMPONENT_STATE_CHECKED}, on);
- trackElement.addState(new int[]{ComponentState.COMPONENT_STATE_EMPTY}, off);
- return trackElement;
- }
-
- private StateElement thumbElementInit(ShapeElement on, ShapeElement off) {
- StateElement thumbElement = new StateElement();
- thumbElement.addState(new int[]{ComponentState.COMPONENT_STATE_CHECKED}, on);
- thumbElement.addState(new int[]{ComponentState.COMPONENT_STATE_EMPTY}, off);
- return thumbElement;
- }
-
- private void initSwitch() {
- // 开启状态下滑块的样式
- ShapeElement elementThumbOn = new ShapeElement();
- elementThumbOn.setShape(ShapeElement.OVAL);
- elementThumbOn.setRgbColor(RgbColor.fromArgbInt(ON_COLOR_SLIDER));
- elementThumbOn.setCornerRadius(CORNER_RADIUS);
-
- // 关闭状态下滑块的样式
- ShapeElement elementThumbOff = new ShapeElement();
- elementThumbOff.setShape(ShapeElement.OVAL);
- elementThumbOff.setRgbColor(RgbColor.fromArgbInt(OFF_COLOR_SLIDER));
- elementThumbOff.setCornerRadius(CORNER_RADIUS);
-
- // 开启状态下轨迹样式
- ShapeElement elementTrackOn = new ShapeElement();
- elementTrackOn.setShape(ShapeElement.RECTANGLE);
- elementTrackOn.setRgbColor(RgbColor.fromArgbInt(ON_COLOR));
- elementTrackOn.setCornerRadius(CORNER_RADIUS);
-
- // 关闭状态下轨迹样式
- ShapeElement elementTrackOff = new ShapeElement();
- elementTrackOff.setShape(ShapeElement.RECTANGLE);
- elementTrackOff.setRgbColor(RgbColor.fromArgbInt(OFF_COLOR));
- elementTrackOff.setCornerRadius(CORNER_RADIUS);
-
- sw.setTrackElement(trackElementInit(elementTrackOn, elementTrackOff));
- sw.setThumbElement(thumbElementInit(elementThumbOn, elementThumbOff));
- }
复制代码
- 使用单例模式初始化分布式数据库
- private void initDb() {
- KvManagerConfig config = new KvManagerConfig(this);
- kvManager = MyKvManagerFactory.getInstance(config);
- Options options = new Options();
- options.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION);
- try {
- singleKvStore = kvManager.getKvStore(options, STORE_ID);
- singleKvStore2 = kvManager.getKvStore(options, STORE_ID2);
- } catch (KvStoreException e) {
- LogUtils.info(TAG, "KvStore Exception Occur");
- }
- }
-
- public class MyKvManagerFactory {
- private static KvManager instance = null;
-
- private MyKvManagerFactory() { }
-
- public static KvManager getInstance(KvManagerConfig config) {
- if (instance == null) {
- synchronized (MyKvManagerFactory.class) {
- if (instance == null) {
- try {
- instance = KvManagerFactory.getInstance().createKvManager(config);
- } catch (KvStoreException e) {
- LogUtils.info("KvStoreException", e.getMessage());
- }
- }
- }
- }
- return instance;
- }
- }
复制代码
说明
- 此demo模拟的是单向场景:智能穿戴设备->手机A->手机B
- STORE_ID:用于智能穿戴设备与手机A之间的数据同步
- STORE_ID2:用于手机A和手机B之间的数据同步(手机A在读取智能穿戴设备写入STORE_ID的同时,将数据备份至此数据库中,用于手机B查看他人数据)
6. 同步数据 HealthSlice类的syncData(SingleKvStore kvStore)方法为手动同步(拉取)智能穿戴设备或手机的数据,这里根据kvStore来进行区分。
说明
singleKvStore: 智能穿戴设备写数据,手机A读数据singleKvStore2:手机A写数据 ,手机B读数据获取组网内的设备列表,将设备ID存入容器中,同时推断需要同步的设备类型,进而进行相关业务处理操作
- private void syncData(SingleKvStore kvStore) {
- List<DeviceInfo> deviceInfoList = kvManager.getConnectedDevicesInfo(DeviceFilterStrategy.NO_FILTER);
- if (deviceInfoList.size() == 0) {
- String message = "未发现手表/手机设备";
- showTip(message);
- renderingData(kvStore);
- return;
- }
- deviceIdList.clear();
- for (DeviceInfo deviceInfo : deviceInfoList) {
- deviceIdList.add(deviceInfo.getId());
- }
- inferredSyncDevice(kvStore);
- }
-
- private void inferredSyncDevice(SingleKvStore kvStore) {
- if (kvStore.equals(singleKvStore)) {
- String message = "同步手表数据成功";
- doSync(message, kvStore);
- renderingData(kvStore);
- } else if (kvStore.equals(singleKvStore2)) {
- String message = "同步手机数据成功";
- doSync(message, kvStore);
- renderingData(kvStore);
- } else {
- LogUtils.info("syncData()==>", "DATA BASE NOT EXIST");
- }
- }
复制代码手动调用同步接口进行分布式数据库的数据同步,同步完成会进入此回调
- private void doSync(String message, SingleKvStore kvStore) {
- kvStore.registerSyncCallback(
- new SyncCallback() {
- @Override
- public void syncCompleted(Map<String, Integer> map) {
- getUITaskDispatcher()
- .asyncDispatch(
- new Runnable() {
- @Override
- public void run() {
- showTip(message);
- }
- });
- kvStore.unRegisterSyncCallback();
- }
- });
- try {
- LogUtils.info(TAG, "Start to get data");
- kvStore.sync(deviceIdList, SyncMode.PULL_ONLY);
- } catch (KvStoreException e) {
- LogUtils.info(TAG, "doSync KvStoreException");
- }
- }
复制代码 7. 渲染数据 同步数据完成后,进行数据在页面上的渲染,同时会将智能穿戴设备写入的数据备份一份至singleKvStore2中,用于手机B查看他人数据
- private void renderingData(SingleKvStore kvStore) {
- if (kvStore != null) {
- List<Entry> entries = kvStore.getEntries("");
- if (entries.size() == 0 || entries.isEmpty() || entries.equals(null)) {
- String message = "未找到数据";
- showTip(message);
- return;
- }
- dealEntries(entries, kvStore);
- }
- }
-
- private void dealEntries(List<Entry> entries, SingleKvStore kvStore) {
- for (Entry entry : entries) {
- // 完成页面数据的加载
- finishPageInitialization(entry.getKey(), kvStore, createWatchDataEntity(entry));
- }
- // 填充心率范围数据
- populateRateRangePage(entries, kvStore);
- }
-
- private void finishPageInitialization(String key, SingleKvStore kvStore, WatchEntity watchEntity) {
- if (KeyEnum.LATEST_STEP.getValue().equals(key)) {
- // 填充最新步数数据
- populateStepPage(kvStore, watchEntity);
- } else if (KeyEnum.LATEST_RATE.getValue().equals(key)) {
- if (kvStore.equals(singleKvStore)) {
- // 填充最新心率数据
- populateRatePage(rateData, rateTime, watchEntity);
- } else {
- populateRatePage(otherRateData, otherRateTime, watchEntity);
- }
- } else if (key.contains(KeyEnum.RATE.getValue())) {
- rateList.add(Integer.valueOf(watchEntity.getData()));
- } else {
- stepList.add(Integer.valueOf(watchEntity.getData()));
- }
- }
-
- private void populateRateRangePage(List<Entry> entries, SingleKvStore kvStore) {
- if (rateList.size() == 0) {
- return;
- }
- doPopulateRateRangePage(entries, kvStore);
- }
-
- // 执行心率范围数据填充以及数据的备份操作
- private void doPopulateRateRangePage(List<Entry> entries, SingleKvStore kvStore) {
- Integer min = Collections.min(rateList);
- Integer max = Collections.max(rateList);
- if (kvStore.equals(singleKvStore)) {
- rateRange.setText(min + "~" + max);
- List<Entry> entries2 = singleKvStore2.getEntries("");
- getUITaskDispatcher().asyncDispatch(new PushDataTask(entries, entries2, singleKvStore2));
- } else {
- otherRateRangeData.setText(min + "~" + max);
- if (min < MIN_HEART_RATE || max > MAX_HEART_RATE) {
- String message = "他人心率异常";
- showTip(message);
- }
- }
- }
-
- // 封装手表数据为Entity,方便传参
- private WatchEntity createWatchDataEntity(Entry entry) {
- String value = entry.getValue().getString();
- String[] split = value.split(regex);
- WatchEntity watchEntity = new WatchEntity();
- watchEntity.setTime(split[0]);
- watchEntity.setData(split[1].substring(0, split[1].indexOf(DOT)));
- return watchEntity;
- }
-
- private void populateStepPage(SingleKvStore kvStore, WatchEntity watchDataEntity) {
- if (kvStore.equals(singleKvStore)) { // 我的数据
- stepData.setProgressHintText("步数 " + watchDataEntity.getData() + " 步");
- stepData.setProgressValue(CommonUtils.getProgressWithSteps(Integer.parseInt(watchDataEntity.getData())));
- stepTime.setText(watchDataEntity.getTime());
- // 距离
- stepDistances.setText("距离" + CommonUtils.getDistanceWithSteps(
- Integer.parseInt(watchDataEntity.getData())) + "公里 |");
- // 热量
- stepHeat.setText("热量" + CommonUtils.getCalorieWithSteps(
- Integer.parseInt(watchDataEntity.getData())) + "千卡 |");
- // 爬楼层
- stepFloor.setText("爬楼" + CommonUtils.getFloorWithSteps(
- Integer.parseInt(watchDataEntity.getData())) + "层");
- // 强度
- Map<String, String> intensityMap = CommonUtils.getIntensityWithSteps(
- Integer.parseInt(watchDataEntity.getData()));
- stepData1.setProgressHintText(intensityMap.get("text"));
- stepData1.setProgressValue(Integer.parseInt(intensityMap.get("progress")));
- } else { // 他人数据
- otherStepData.setProgressHintText("步数 " + watchDataEntity.getData() + " 步");
- otherStepData.setProgressValue(CommonUtils.getProgressWithSteps(
- Integer.parseInt(watchDataEntity.getData())));
- otherStepTime.setText(watchDataEntity.getTime());
- // 距离
- otherStepDistances.setText("距离" + CommonUtils.getDistanceWithSteps(
- Integer.parseInt(watchDataEntity.getData())) + "公里 |");
- // 热量
- otherStepHeat.setText("热量" + CommonUtils.getCalorieWithSteps(
- Integer.parseInt(watchDataEntity.getData())) + "千卡 |");
- // 爬楼层
- otherStepFloor.setText("爬楼" + CommonUtils.getFloorWithSteps(
- Integer.parseInt(watchDataEntity.getData())) + "层");
- // 强度
- Map<String, String> intensityMap = CommonUtils.getIntensityWithSteps(
- Integer.parseInt(watchDataEntity.getData()));
- otherStepData1.setProgressHintText(intensityMap.get("text"));
- otherStepData1.setProgressValue(Integer.parseInt(intensityMap.get("progress")));
- }
- }
-
- private void populateRatePage(Text rateData, Text rateTime, WatchEntity watchDataEntity) {
- rateData.setText(watchDataEntity.getData());
- rateTime.setText(watchDataEntity.getTime());
- }
-
- // 弹框处理
- private void showTip(String message) {
- myHandler.postTask(new Runnable() {
- @Override
- public void run() {
- new ToastDialog(getContext())
- .setAlignment(LayoutAlignment.TOP)
- .setText(message)
- .setDuration(SHOW_TIME)
- .setSize(DIALOG_SIZE_WIDTH, DIALOG_SIZE_HEIGHT)
- .show();
- }
- });
- }
复制代码说明
以上代码仅demo演示参考使用
8. 回顾和总结本篇Codelab我们介绍了应用的初始化加载过程和同步数据渲染数据的代码逻辑,在主页面可以通过底部的按钮切换到不同类别的数据(健康数据和他人数据)。点击上方手动同步按钮会进行数据的手动同步,同时也提供自动刷新的机制。当智能穿戴设备发现心率异常时会拉起手机端的PA服务,手机端会在状态栏收到手表心率异常的通知,点击该通知可以进入应用主页面。
9. 恭喜你 目前你已经成功完成了Codelab并且学到了:
- 如何进行布局编写及页面切换
- 如何进行心率和步数数据的采集
- 状态栏通知,点击通知进入FA和分布式数据库数据同步
10. 参考