zhuguifei
2025-12-30 61eee1173c00a7ba9d9c748d28fe3acdb33b9441
首次提交
已添加182个文件
55130 ■■■■■ 文件已修改
.gitignore 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/build.gradle 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/proguard-rules.pro 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/proguardMapping.txt 34468 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/AndroidManifest.xml 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/Application.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/SplashActivity.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/activity/AdvancedSettingActivity.java 377 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/activity/ConnActivity.java 444 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/activity/CurrentDeviceActivity.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/activity/HomeActivity.java 355 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/activity/NormalWriteActivity.java 536 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/activity/ParameterSettingActivity.java 665 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/activity/TestActivity.java 357 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/activity/WriteActivity.java 627 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/activity/WriteActivity2.java 531 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/adapter/BaseArrayListAdapter.java 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/adapter/DeviceListAdapter.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/adapter/DeviceListAdapter2.java 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/adapter/HomeAdapter.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/adapter/HomeDeviceListAdapter.java 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/adapter/WriteAdapter.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/base/BaseActivity.java 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/event/UpdateEvent.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/fragment/DeviceListFragment.java 358 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/fragment/SettingsFragment.java 712 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/manager/BleGlobalManager.java 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/model/BluetoothLeDeviceStore.java 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/model/HomeItem.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/util/CMD.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/util/CRCutil.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/util/Constants.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/util/CsvWriterHelper.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/util/Singletion.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/util/TimeFormatter.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/view/ParticleView.java 379 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/view/particleview/LineEvaluator.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/view/particleview/Particle.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/shlb/comb/view/t.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/color/s_app_color_blue_2.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/color/selector_drawer_icon.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/color/selector_drawer_text.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable-v24/ic_launcher_foreground.xml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/bg_box_empty.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/bg_box_error.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/bg_box_full.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/bg_box_offline.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/bg_card.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/bg_drawer_header_gradient.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/bg_drawer_item.xml 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/bg_edittext.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/bg_input_field.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/bg_round.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/btn_gray.xml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/ic_launcher_background.xml 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/item_flag_view.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/radius_button_bg.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/radius_button_bg_pressed.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/s_radius_button_bg.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_advanced_setting.xml 280 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_base.xml 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_conn.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_current_device.xml 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_home.xml 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_parameter_setting.xml 458 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_splash.xml 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_test.xml 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_write.xml 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_write2.xml 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/dialog_layout_bluetooth_item.xml 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/dialog_layout_bluetooth_list.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/dialog_layout_write_item.xml 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/dialog_layout_write_list.xml 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/fragment_device_list.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/fragment_settings.xml 465 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/home_item_view.xml 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/home_list_bluetooth_item.xml 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/include_head.xml 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/include_head_home.xml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/item_grid_box.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/layout_loading_view.xml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/list_item_device.xml 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/normal_activity_write.xml 304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/tipdialog_custom.xml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/top_view.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/menu/menu_main.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-hdpi/ic_launcher.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-mdpi/ic_launcher.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/about_logo.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/animation_img1.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/example_image0.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/ic_bluetooth.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/ic_device_ibeacon.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/ic_down.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/ic_launcher.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/ic_sanjiao.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/ic_sport_bg.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/icon_back.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/icon_colse.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/icon_head.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/icon_menu.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/icon_record.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/icon_topbar_about.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/icon_topbar_overflow.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/icon_topbar_right.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/load_err.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/monkey.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/top_background.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xxhdpi/animation_img1.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xxhdpi/animation_img2.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xxhdpi/animation_img3.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xxhdpi/ic_launcher.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xxhdpi/icon_topbar_about.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values-land/dimens.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values-w1240dp/dimens.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values-w600dp/dimens.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/arrays.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/colors.xml 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/dimens.xml 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/ids.xml 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/strings.xml 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/themes.xml 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/values.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/xml/backup_rules.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/xml/data_extraction_rules.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/build.gradle 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/proguard-rules.pro 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/AndroidManifest.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/BleManager.java 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/BleNotAvailableException.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/BleParamsOptions.java 285 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/BluetoothConnectInterface.java 398 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/BluetoothConnectManager.java 475 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/BluetoothOperatorQueue.java 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/BluetoothSubScribeData.java 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/ConnectConfig.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/ConnectState.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/ConnectStateListener.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/GattError.java 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/ReconnectParamsBean.java 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/multiple/ConnectRequestQueue.java 609 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/multiple/MultiConnectManager.java 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BeaconDevice.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BeaconManufacturerData.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BeaconType.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BeaconUtils.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BluetoothLeDevice.java 440 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BluetoothService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/adrecord/AdRecord.java 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/adrecord/AdRecordStore.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/adrecord/AdRecordUtils.java 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/ibeacon/IBeaconConstants.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/ibeacon/IBeaconDevice.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/ibeacon/IBeaconDistanceDescriptor.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/ibeacon/IBeaconManufacturerData.java 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/ibeacon/IBeaconUtils.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/resolvers/BluetoothClassResolver.java 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/resolvers/CompanyIdentifierResolver.java 646 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/resolvers/GattAttributeResolver.java 380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/BackgroundPowerSaver.java 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/BluetoothScanManager.java 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/CycledLeScanner.java 363 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/ScanOverListener.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/BluetoothLeScannerCompat.java 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/BluetoothUuidCompat.java 310 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ObjectsCompat.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ScanCallbackCompat.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ScanFilterCompat.java 627 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ScanRecordCompat.java 353 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ScanResultCompat.java 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ScanSettingsCompat.java 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/util/BluetoothUtils.java 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/util/ByteUtils.java 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/util/LimitedLinkHashMap.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bluetooth-manager-lib/src/main/res/values/strings.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
build.gradle 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
comb.jks 补丁 | 查看 | 原始文档 | blame | 历史
gradle.properties 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
settings.gradle 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
.idea
app/release
app/src/androidTest
app/src/test
bluetooth-manager-lib/src/androidTest
bluetooth-manager-lib/src/test
gradle
gradlew
gradlew.bat
app/.gitignore
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
/build
app/build.gradle
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
plugins {
    id 'com.android.application'
}
android {
    namespace "com.shlb.comb"
    compileSdk 34
    defaultConfig {
        applicationId "com.shlb.comb"
        minSdk 21
        targetSdk 34
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            minifyEnabled true
            shrinkResources true
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        debug {
            minifyEnabled false
            minifyEnabled true
            shrinkResources true
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    lintOptions {
        abortOnError false
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    buildFeatures {
        viewBinding true
    }
    applicationVariants.all { variant ->
        if (variant.buildType.name == "release") {
            variant.outputs.all {
                def date = new Date().format("yyyyMMddHHmmss")
                outputFileName = "comb-release-${date}.apk"
            }
        }
    }
}
dependencies {
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation project(path: ':bluetooth-manager-lib')
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    implementation 'com.qmuiteam:qmui:2.0.1'
    implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.0'
    implementation 'org.greenrobot:eventbus:3.0.0'
    implementation 'pub.devrel:easypermissions:3.0.0'
}
app/proguard-rules.pro
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,207 @@
#-------------------------------------------定制化区域----------------------------------------------
#---------------------------------1.实体类---------------------------------
-keep class com.shlb.comb.model.** { *; }
#-------------------------------------------------------------------------
#---------------------------------2.第三方包-------------------------------
#eventbus
-keepattributes *Annotation*
-keepclassmembers class * {
    @org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# If using AsyncExecutord, keep required constructor of default event used.
# Adjust the class name if a custom failure event type is used.
-keepclassmembers class org.greenrobot.eventbus.util.ThrowableFailureEvent {
    <init>(java.lang.Throwable);
}
##--------------- Androidx ----------
-keep class com.google.android.material.** {*;}
-keep class androidx.** {*;}
-keep public class * extends androidx.**
-keep interface androidx.** {*;}
-dontwarn com.google.android.material.**
-dontnote com.google.android.material.**
-dontwarn androidx.**
##--------------- Androidx ----------
##--------------- QmUi ----------
-keep class **_FragmentFinder { *; }
-keep class androidx.fragment.app.* { *; }
-keep class com.qmuiteam.qmui.arch.record.RecordIdClassMap { *; }
-keep class com.qmuiteam.qmui.arch.record.RecordIdClassMapImpl { *; }
-keep class com.qmuiteam.qmui.arch.scheme.SchemeMap {*;}
-keep class com.qmuiteam.qmui.arch.scheme.SchemeMapImpl {*;}
##--------------- QmUi ----------
# Accessed via reflection, avoid renaming or removal
-keep class org.greenrobot.eventbus.android.AndroidComponentsImpl
#fastjson
-keepattributes Signature
-dontwarn com.alibaba.fastjson.**
-keep class com.alibaba.fastjson.**{*; }
#picasso
-dontwarn com.squareup.okhttp.**
#okhttp #okio
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
#retrofit2
-dontwarn retrofit2.**
#rx
-dontwarn rx.**
#alipay
-dontwarn com.alipay.**
#rxeasyhttp
-keep class com.zhouyou.http.model.** {*;}
-keep class com.zhouyou.http.cache.model.** {*;}
-keep class com.zhouyou.http.cache.stategy.**{*;}
# -keep class com.zhouyou.http.** { *;}
#MPAndroidchart
-keep class com.github.mikephil.** { *;}
#pgy
-dontwarn com.pgyersdk.**
-keep class com.pgyersdk.** { *; }
#litpal
-keep class org.litepal.** {
    *;
}
-keep class * extends org.litepal.crud.DataSupport {
    *;
}
#-------------------------------------------------------------------------
#---------------------------------3.与js互相调用的类------------------------
#-------------------------------------------------------------------------
#---------------------------------4.反射相关的类和方法-----------------------
#-keep class com.shlanbao.lbmonitoring.activity.LbSearchActivity
#
#-keep class com.shlanbao.lbmonitoring.view.LbSearchView
#----------------------------------------------------------------------------
#---------------------------------------------------------------------------------------------------
#-------------------------------------------基本不用动区域--------------------------------------------
#---------------------------------基本指令区----------------------------------
# ä»£ç æ··æ·†åŽ‹ç¼©æ¯”ï¼Œåœ¨0~7之间
-optimizationpasses 5
# æ··åˆæ—¶ä¸ä½¿ç”¨å¤§å°å†™æ··åˆï¼Œæ··åˆåŽçš„类名为小写
-dontusemixedcaseclassnames
# æŒ‡å®šä¸åŽ»å¿½ç•¥éžå…¬å…±åº“çš„ç±»æˆå‘˜
-dontskipnonpubliclibraryclasses
# ä¸åšé¢„校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
-dontpreverify
# æ··æ·†æ—¶æ˜¯å¦è®°å½•日志,这句话能够使我们的项目混淆后产生映射文件
# åŒ…含有类名->混淆后类名的映射关系
-verbose
# é¿å…æ··æ·†æ³›åž‹
-keepattributes Signature
# ä¿ç•™Annotation不混淆
-keepattributes *Annotation*,InnerClasses
#google推荐算法
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# é¿å…æ··æ·†Annotation、内部类、泛型、匿名类
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod
# é‡å‘½åæŠ›å‡ºå¼‚常时的文件名称
-renamesourcefileattribute SourceFile
# æŠ›å‡ºå¼‚常时保留代码行号
-keepattributes SourceFile,LineNumberTable
# å¿½ç•¥è­¦å‘Š
-ignorewarnings
# ä¼˜åŒ–不优化输入的类文件
-dontoptimize
# ä¿ç•™R下面的资源
-keep class **.R$* {*;}
# ä¿ç•™æžšä¸¾ç±»ä¸è¢«æ··æ·†
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}
#----------------------------------------------------------------------------
#---------------------------------默认保留区---------------------------------
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
-keep class android.support.** {*;}
-keep public class * extends android.view.View{
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}
-keepclasseswithmembers class * {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}
-keep class **.R$* {
 *;
}
-keepclassmembers class * {
    void *(**On*Event);
}
#----------------------------------------------------------------------------
#---------------------------------webview------------------------------------
-keepclassmembers class fqcn.of.javascript.interface.for.Webview {
   public *;
}
-keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.WebView, jav.lang.String);
}
#----------------------------------------------------------------------------
#---------------------------------------------------------------------------------------------------
app/proguardMapping.txt
¶Ô±ÈÐÂÎļþ
ÎļþÌ«´ó
app/src/main/AndroidManifest.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.shlb.comb">
    <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-sdk
        tools:overrideLibrary="com.qmuiteam.qmui"
        />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name="com.shlb.comb.SplashActivity"
            android:screenOrientation="portrait"
            android:theme="@style/SplashTheme"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".activity.ParameterSettingActivity"
            android:screenOrientation="portrait"
            android:windowSoftInputMode="adjustPan" />
        <activity
            android:name=".activity.AdvancedSettingActivity"
            android:screenOrientation="portrait"
            android:windowSoftInputMode="adjustPan" />
        <activity android:name=".activity.HomeActivity"
            android:screenOrientation="portrait"
            />
        <activity android:name=".activity.ConnActivity"
            android:screenOrientation="portrait"
            />
        <activity android:name=".activity.WriteActivity"
            android:screenOrientation="portrait"
            />
        <activity android:name=".activity.CurrentDeviceActivity"
            android:screenOrientation="portrait"
            />
        <activity android:name=".activity.TestActivity"
            android:screenOrientation="portrait" />
        <activity android:name=".activity.NormalWriteActivity" />
    </application>
</manifest>
app/src/main/java/com/shlb/comb/Application.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
package com.shlb.comb;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class Application  extends  android.app.Application{
    private static final String TAG = "Application:";
    private static Application instance;
    private int count = 0;
    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
    }
    public Application getInstance() {
        return instance;
    }
    private void ActivityLifecycleCallbacks() {
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityStopped(Activity activity) {
//                Logger.d(TAG + activity + "onActivityStopped");
                count--;
                if (count == 0) {
                    Log.e(TAG , activity.getClass().getSimpleName()+">>>>>>>>>>>>>>>>>>>切到后台  lifecycle");
//                    if(activity.getClass().getSimpleName().contains("MainActivity")) {
//                        Intent tent = new Intent(getPackageName() + AppConfig.BACKSTAGE);// å¹¿æ’­çš„æ ‡ç­¾ï¼Œä¸€å®šè¦å’Œéœ€è¦æŽ¥å—的一致。
////                    sendBroadcast(tent);// å‘送广播
//                        LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(tent);
//                    }
                }
            }
            @Override
            public void onActivityStarted(Activity activity) {
//                Logger.e(TAG + activity + "onActivityStarted");
                if (count == 0) {
                    Log.e(TAG , ">>>>>>>>>>>>>>>>>>>切到前台  lifecycle");
//                    if(activity.getClass().getSimpleName().contains("MainActivity")){
//                        Intent tent = new Intent(getPackageName() + AppConfig.RECEPTION);// å¹¿æ’­çš„æ ‡ç­¾ï¼Œä¸€å®šè¦å’Œéœ€è¦æŽ¥å—的一致。
////                    sendBroadcast(tent);// å‘送广播
//                        LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(tent);
//                    }
                }
                count++;
            }
            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
               Log.e(TAG ,  activity + "onActivitySaveInstanceState");
            }
            @Override
            public void onActivityResumed(Activity activity) {
              Log.e(TAG ,  activity + "onActivityResumed");
            }
            @Override
            public void onActivityPaused(Activity activity) {
              Log.e(TAG ,  activity + "onActivityPaused");
            }
            @Override
            public void onActivityDestroyed(Activity activity) {
            Log.e(TAG , activity + "onActivityDestroyed");
            }
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
               Log.e(TAG , activity + "onActivityCreated");
            }
        });
    }
}
app/src/main/java/com/shlb/comb/SplashActivity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package com.shlb.comb;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.shlb.comb.activity.HomeActivity;
import com.shlb.comb.view.ParticleView;
public class SplashActivity extends AppCompatActivity {
    ParticleView ptview;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (Build.VERSION.SDK_INT >= 28) {
            WindowManager.LayoutParams lp = getWindow().getAttributes();
            lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            getWindow().setAttributes(lp);
        }
        setContentView(R.layout.activity_splash);
        ptview = findViewById(R.id.ptview);
        ptview.startAnim();
        ptview.setOnParticleAnimListener(new ParticleView.ParticleAnimListener() {
            @Override
            public void onAnimationEnd() {
                startActivity(new Intent(SplashActivity.this, HomeActivity.class));
                finish();
            }
        });
    }
}
app/src/main/java/com/shlb/comb/activity/AdvancedSettingActivity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,377 @@
package com.shlb.comb.activity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.text.Html;
import android.widget.Toast;
import com.shlb.comb.R;
import com.shlb.comb.base.BaseActivity;
import com.shlb.comb.event.UpdateEvent;
import com.shlb.comb.manager.BleGlobalManager;
import com.shlb.comb.util.CMD;
import com.shlb.comb.util.CRCutil;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class AdvancedSettingActivity extends BaseActivity {
    private EditText et_mainboard_version;
    private Button btn_mainboard_read;
    private EditText et_display_version;
    private Button btn_display_read;
    private Button btn_start_addressing;
    private Button btn_end_addressing;
    private Button btn_clear_log;
    private TextView tv_log;
    private TextView tv_right_text;
    private TextView tv_mainboard_status;
    private TextView tv_sensor_status;
    private StringBuilder logBuilder = new StringBuilder();
    private String currentExecutingCmd = "";
    @Override
    protected void contentView() {
        setContentView(R.layout.activity_advanced_setting);
    }
    @Override
    protected void initView() {
        super.initHead();
        if (tv_center != null) {
            tv_center.setText("高级设置");
        }
        if (iv_left != null) {
            iv_left.setImageResource(R.mipmap.icon_back);
        }
        et_mainboard_version = findViewById(R.id.et_mainboard_version);
        btn_mainboard_read = findViewById(R.id.btn_mainboard_read);
        et_display_version = findViewById(R.id.et_display_version);
        btn_display_read = findViewById(R.id.btn_display_read);
        btn_start_addressing = findViewById(R.id.btn_start_addressing);
        btn_end_addressing = findViewById(R.id.btn_end_addressing);
        btn_clear_log = findViewById(R.id.btn_clear_log);
        tv_log = findViewById(R.id.tv_log);
        tv_right_text = findViewById(R.id.tv_right_text);
        tv_mainboard_status = findViewById(R.id.tv_mainboard_status);
        tv_sensor_status = findViewById(R.id.tv_sensor_status);
        // Initialize log
        logBuilder.append("日志记录:<br>");
        tv_log.setText(Html.fromHtml(logBuilder.toString()));
    }
    @Override
    protected void initData() {
        updateBluetoothStatus();
        if (BleGlobalManager.getInstance().isConnected()) {
            sendCmdWithCrc(CMD.ENTER_SETTING);
            triggerAutoRead();
        }
    }
    @Override
    protected void onDestroy() {
        if (BleGlobalManager.getInstance().isConnected()) {
            sendCmdWithCrc(CMD.EXIT_SETTING);
        }
        super.onDestroy();
    }
    @Override
    protected void initEvent() {
        View.OnClickListener listener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int id = v.getId();
                String action = "";
                if (id == R.id.btn_mainboard_read) {
                    action = "读取主板版本";
                    currentExecutingCmd = CMD.READ_BOARD_VERSION;
                    sendCmdWithCrc(CMD.READ_BOARD_VERSION);
                } else if (id == R.id.btn_display_read) {
                    action = "读取传感器版本";
                    currentExecutingCmd = CMD.READ_SENSOR_VERSION;
                    sendCmdWithCrc(CMD.READ_SENSOR_VERSION);
                } else if (id == R.id.btn_start_addressing) {
                    action = "开始编址";
                    currentExecutingCmd = CMD.WRITE_START_ADDRESS;
                    sendCmdWithCrc(CMD.WRITE_START_ADDRESS);
                } else if (id == R.id.btn_end_addressing) {
                    action = "结束编址";
                    currentExecutingCmd = CMD.WRITE_END_ADDRESS;
                    sendCmdWithCrc(CMD.WRITE_END_ADDRESS);
                } else if (id == R.id.btn_clear_log) {
                    logBuilder.setLength(0);
                    logBuilder.append("日志记录:<br>");
                    tv_log.setText(Html.fromHtml(logBuilder.toString()));
                }
                if (!action.isEmpty() && id != R.id.btn_start_addressing && id != R.id.btn_end_addressing) {
                    // Toast.makeText(AdvancedSettingActivity.this, action, Toast.LENGTH_SHORT).show();
                    // addLog(action);
                }
            }
        };
        btn_mainboard_read.setOnClickListener(listener);
        btn_display_read.setOnClickListener(listener);
        btn_start_addressing.setOnClickListener(listener);
        btn_end_addressing.setOnClickListener(listener);
        btn_clear_log.setOnClickListener(listener);
    }
    private void addLog(String message) {
        appendLog(message, null);
    }
    private String getCmdDescription(String hex) {
        if (hex == null) return "";
        // å‘送指令匹配
        if (hex.startsWith(CMD.ENTER_SETTING)) return "-进入设定";
        if (hex.startsWith(CMD.EXIT_SETTING)) return "-退出设定";
        if (hex.startsWith(CMD.READ_BOARD_VERSION)) return "-读取主板版本";
        if (hex.startsWith(CMD.READ_SENSOR_VERSION)) return "-读取传感器版本";
        if (hex.startsWith(CMD.WRITE_START_ADDRESS)) return "-开始编址";
        if (hex.startsWith(CMD.WRITE_END_ADDRESS)) return "-结束编址";
        // æŽ¥æ”¶æ•°æ®åŒ¹é…
        // è¯»å–主板版本返回 (A55A03 0A ...)
        if (hex.startsWith("A55A030A")) return "-读取主板版本";
        // è¯»å–传感器版本返回 (A55A03 0B ...)
        if (hex.startsWith("A55A030B")) return "-读取传感器版本";
        // å†™å…¥è¿”回 (A55A06开头)
        if (hex.startsWith("A55A06") && hex.length() >= 12) {
             return "-写入返回";
        }
        return "";
    }
    private void appendLog(String msg, Boolean isSent) {
        String time = com.blankj.utilcode.util.TimeUtils.getNowString(new java.text.SimpleDateFormat("HH:mm:ss.SSS"));
        String displayMsg = msg;
        String cmdDesc = "";
        // å¦‚果是 hex æŒ‡ä»¤ (纯 0-9 A-F a-f),加空格格式化
        if (msg.matches("^[0-9A-Fa-f]+$")) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < msg.length(); i += 2) {
                if (i + 2 <= msg.length()) {
                    sb.append(msg.substring(i, i + 2)).append(" ");
                } else {
                    sb.append(msg.substring(i));
                }
            }
            displayMsg = sb.toString().trim();
            // èŽ·å–æŒ‡ä»¤æè¿°
            cmdDesc = getCmdDescription(msg);
        }
        String logLine;
        if (isSent != null) {
            String color = isSent ? "#1890ff" : "#05aa87"; // Blue for sent, Green for received
            // æ‹¼æŽ¥åˆ°å‰ç¼€åŽé¢: "发送-读站号: " æˆ– "收到-读站号: "
            String prefix = (isSent ? "发送" : "收到") + cmdDesc + ": ";
            logLine = time + " <font color='" + color + "'>" + prefix + displayMsg + "</font><br>";
        } else {
            // System/Local log (Black)
            logLine = time + " " + displayMsg + "<br>";
        }
        logBuilder.append(logLine);
        if (tv_log != null) {
            tv_log.setText(Html.fromHtml(logBuilder.toString()));
        }
    }
    private void updateBluetoothStatus() {
        if (tv_right_text == null) return;
        if (BleGlobalManager.getInstance().isConnected()) {
            String name = "未知设备";
            if (BleGlobalManager.getInstance().getBluetoothLeDevice() != null) {
                String deviceName = BleGlobalManager.getInstance().getBluetoothLeDevice().getName();
                if (deviceName != null && !deviceName.isEmpty()) {
                    name = deviceName;
                }
            }
            tv_right_text.setText(name);
        } else {
            tv_right_text.setText("未连接");
        }
    }
    private void triggerAutoRead() {
        showLoading("正在同步版本信息...");
        // Reset status
        setLabelStatus(tv_mainboard_status, "", R.color.base_text);
        setLabelStatus(tv_sensor_status, "", R.color.base_text);
        // Read Mainboard
        new android.os.Handler().postDelayed(() -> {
            if (btn_mainboard_read != null) btn_mainboard_read.performClick();
        }, 200);
        // Read Sensor
        new android.os.Handler().postDelayed(() -> {
            if (btn_display_read != null) btn_display_read.performClick();
        }, 1200);
        // Hide loading
        new android.os.Handler().postDelayed(() -> hiddeLoading(), 2500);
    }
    private void setLabelStatus(TextView view, String text, int colorResId) {
        if (view != null) {
            view.setText(text);
            view.setTextColor(getResources().getColor(colorResId));
        }
    }
    private void sendCmdWithCrc(String cmd) {
        if (!BleGlobalManager.getInstance().isConnected()) {
            Toast.makeText(this, "请先连接蓝牙", Toast.LENGTH_SHORT).show();
            addLog("错误: è“ç‰™æœªè¿žæŽ¥");
            return;
        }
        byte[] cmdBytes = BleGlobalManager.hexStringToBytes(cmd);
        if (cmdBytes != null) {
            String crc = CRCutil.getCRC(cmdBytes);
            // Pad CRC to 4 chars if needed
            while (crc.length() < 4) {
                crc = "0" + crc;
            }
            String fullCmd = cmd + crc.toUpperCase();
            appendLog(fullCmd, true); // true for sent (Blue)
            BleGlobalManager.getInstance().sendCmd(fullCmd);
        } else {
            addLog("错误: æŒ‡ä»¤è½¬æ¢å¤±è´¥");
        }
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvent(UpdateEvent event) {
        if (event.getType() == UpdateEvent.Type.CONN_STATU) {
            updateBluetoothStatus();
            if (event.getObj() instanceof Integer && (int) event.getObj() == android.bluetooth.BluetoothProfile.STATE_CONNECTED) {
                triggerAutoRead();
            }
        } else if (event.getType() == UpdateEvent.Type.DEVICE_INFO) {
            String hex = event.getMsg();
            appendLog(hex, false); // false for received (Green)
            parseAndRefresh(hex);
        }
    }
    private void parseAndRefresh(String hex) {
        if (hex == null) return;
        // å†™å…¥æŒ‡ä»¤çš„返回 (A55A06开头)
        if (hex.startsWith("A55A06") && hex.length() >= 12) {
            parseWriteResponse(hex);
            return;
        }
        // è¯»å–指令的返回 (A55A03开头)
        if (hex.startsWith("A55A03") && hex.length() >= 8) {
             parseReadResponse(hex);
        }
    }
    private void parseReadResponse(String hex) {
        try {
            // å‘½ä»¤ç±»åž‹: ç¬¬7-8位 (Index 6-8)
            String cmdType = hex.substring(6, 8);
            // æ•°æ®éƒ¨åˆ†é€šå¸¸ä»Žç¬¬11-12位开始 (Index 10)
            // å‡è®¾ç‰ˆæœ¬å·æ•°æ®å°±åœ¨åŽé¢
            String dataHex = "";
            if (hex.length() > 10) {
                // åŽ»æŽ‰CRC (最后4位)? ä¸ç¡®å®šé•¿åº¦ï¼Œå…ˆå–所有
                // æš‚时取 substring(10) ç›´åˆ° length-4 (如果带CRC)
                // ç®€å•起见,取 substring(10)
                dataHex = hex.substring(10);
                // å¦‚果最后4位是CRC,可能需要去掉。通常 CRC æ˜¯æœ€åŽ4字符。
                if (dataHex.length() > 4) {
                    dataHex = dataHex.substring(0, dataHex.length() - 4);
                }
            }
            if ("0A".equals(cmdType)) {
                // ä¸»æ¿ç‰ˆæœ¬
                String version = formatVersionHex(dataHex);
                if (et_mainboard_version != null) et_mainboard_version.setText(version);
                setLabelStatus(tv_mainboard_status, "(读取值)", R.color.base_color);
                addLog("读取主板版本成功: " + version);
            } else if ("0B".equals(cmdType)) {
                // ä¼ æ„Ÿå™¨ç‰ˆæœ¬
                String version = formatVersionHex(dataHex);
                if (et_display_version != null) et_display_version.setText(version);
                setLabelStatus(tv_sensor_status, "(读取值)", R.color.base_color);
                addLog("读取传感器版本成功: " + version);
            }
        } catch (Exception e) {
            e.printStackTrace();
            addLog("解析读取返回异常: " + e.getMessage());
        }
    }
    private String formatVersionHex(String hex) {
        if (hex == null || hex.isEmpty()) return "";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < hex.length(); i++) {
            sb.append(hex.charAt(i));
            if (i % 2 == 1 && i < hex.length() - 1) {
                sb.append("-");
            }
        }
        return sb.toString();
    }
    private void parseWriteResponse(String hex) {
        try {
            // å†™å…¥æŒ‡ä»¤çš„返回 (A55A06开头)
            if (hex.startsWith("A55A06") && hex.length() >= 12) {
                // ç»“果状态: ç¬¬11-12位 (Index 10-12)
                String statusHex = hex.substring(10, 12);
                boolean isSuccess = "01".equals(statusHex);
                if (!currentExecutingCmd.isEmpty()) {
                    if (isSuccess) {
                        if (currentExecutingCmd.startsWith(CMD.WRITE_START_ADDRESS)) {
                            addLog("开始编址成功");
                            Toast.makeText(this, "开始编址成功", Toast.LENGTH_SHORT).show();
                        } else if (currentExecutingCmd.startsWith(CMD.WRITE_END_ADDRESS)) {
                            addLog("结束编址成功");
                            Toast.makeText(this, "结束编址成功", Toast.LENGTH_SHORT).show();
                        }
                    } else {
                         addLog("操作失败: " + hex);
                         Toast.makeText(this, "操作失败", Toast.LENGTH_SHORT).show();
                    }
                    // Reset current command after handling
                    currentExecutingCmd = "";
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            addLog("解析异常: " + e.getMessage());
        }
    }
}
app/src/main/java/com/shlb/comb/activity/ConnActivity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,444 @@
package com.shlb.comb.activity;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Intent;
import android.view.View;
import android.view.ViewGroup;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothConnectManager;
import com.blakequ.bluetooth_manager_lib.connect.ConnectState;
import com.blakequ.bluetooth_manager_lib.connect.ConnectStateListener;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.util.BluetoothUtils;
import com.blankj.utilcode.util.LogUtils;
import com.qmuiteam.qmui.util.QMUIDisplayHelper;
import com.qmuiteam.qmui.widget.QMUIEmptyView;
import com.qmuiteam.qmui.widget.dialog.QMUIDialog;
import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction;
import com.qmuiteam.qmui.widget.grouplist.QMUICommonListItemView;
import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView;
import com.shlb.comb.R;
import com.shlb.comb.base.BaseActivity;
import com.shlb.comb.event.UpdateEvent;
import com.shlb.comb.util.Singletion;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ConnActivity extends BaseActivity {
    QMUIGroupListView mGroupListView;
    QMUIEmptyView mEmptyView;
    //连接管理
    private BluetoothConnectManager connectManager;
    //连接状态
    private int connectState = 0;
    //是否连接
    private boolean mConnected = false;
    @Override
    protected void contentView() {
        setContentView(R.layout.activity_conn);
        mGroupListView = findViewById(R.id.groupListView);
        mEmptyView = findViewById(R.id.emptyView);
    }
    @Override
    protected void initView() {
        super.initHead();
        initTitlebar();
        tv_center.setText("蓝牙信息");
        mEmptyView.show(true);
        initGroupListView();
    }
    @Override
    protected void initData() {
        BluetoothLeDevice mDevice = Singletion.getInstance().mDevice;
        initConn(mDevice);
    }
    @Override
    protected void initEvent() {
    }
    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.iv_left) {
            this.onBackPressed();
        } else if (id == R.id.iv_right) {
        }
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventRefresh(UpdateEvent event) {
        switch (event.getType()) {
            case CONN_STATU:
                int statu = (int) event.getObj();
                //0-断开  2-连接
                switch (statu) {
                    case 0:
                        mConnected = false;
                        Singletion.getInstance().mConnected = false;
                        break;
                    case 2:
                        mConnected = true;
                        Singletion.getInstance().mConnected = true;
                        mEmptyView.hide();
                        Toast("连接成功!");
                        break;
                }
                break;
            case CONN_SERVICE:
                //获取服务成功后更新UI
                updateUi();
                break;
            case SCAN_UPDATE:
                break;
            case CONFIG_CHANGE:
                break;
        }
    }
    private ConnectStateListener stateListener = new ConnectStateListener() {
        @Override
        public void onConnectStateChanged(String address, ConnectState state) {
            switch (state) {
                case CONNECTED:
                    break;
                case CONNECTING:
                    break;
                case NORMAL:
                    break;
            }
            invalidateOptionsMenu();
        }
    };
    //连接设备
    private void initConn(BluetoothLeDevice device) {
        if (connectManager == null) {
            connectManager = BluetoothConnectManager.getInstance(this);
            connectManager.addConnectStateListener(stateListener);
            connectManager.setBluetoothGattCallback(new BluetoothGattCallback() {
                @Override
                public void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
                    if (status == BluetoothGatt.GATT_SUCCESS) {
                        EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.BLE_DATA, characteristic, "read"));
                    } else {
                        EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.BLE_DATA, characteristic, "fail"));
                        LogUtils.e(TAG, "fail to read characteristic");
                    }
                }
                @Override
                public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
                    super.onCharacteristicWrite(gatt, characteristic, status);
                    if (status == BluetoothGatt.GATT_SUCCESS) {
                        EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.BLE_DATA, characteristic, "write"));
                    } else {
                        EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.BLE_DATA, characteristic, "fail"));
                        LogUtils.e(TAG, "fail to write characteristic");
                    }
                }
                @Override
                public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
                    super.onCharacteristicChanged(gatt, characteristic);
                    EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.BLE_DATA, characteristic, "notify"));
                }
                @Override
                public void onConnectionStateChange(BluetoothGatt gatt, final int status, int newState) {
                    super.onConnectionStateChange(gatt, status, newState);
                    EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.CONN_STATU, newState, "conn_statu"));
                    connectState = newState;
                }
                @Override
                public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
                    super.onServicesDiscovered(gatt, status);
                    if (status == BluetoothGatt.GATT_SUCCESS) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                displayGattServices(gatt.getServices());
                            }
                        });
                    }
                }
            });
        }
        connectManager.connect(device.getAddress());
    }
    /**
     * èŽ·å–å½“å‰è¿žæŽ¥è“ç‰™å¯å†™æœåŠ¡
     *
     * @param gattServices
     */
    private void displayGattServices(List<BluetoothGattService> gattServices) {
        Singletion.getInstance().mbluetoothServices.clear();
        List<Map<String, String>> res = new ArrayList<>();
        for (final BluetoothGattService gattService : gattServices) {
            final List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics();
            for (final BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
                Map<String, String> map = new HashMap<>();
                String property = getPropertyString(gattCharacteristic.getProperties());
                //只需要可写服务
                if (property.contains("Write")) {
                    map.put("name", property);
                    map.put("uuid", gattCharacteristic.getUuid().toString());
                    res.add(map);
                }
            }
        }
        Singletion.getInstance().mbluetoothServices.addAll(res);
        EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.CONN_SERVICE));
    }
    private String getPropertyString(int property) {
        StringBuilder sb = new StringBuilder("(");
        //Read
        if ((property & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
            sb.append("Read ");
        }
        //Write
        if ((property & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0
                || (property & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
            sb.append("Write ");
        }
        //Notify
        if ((property & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
            sb.append("Notity ");
        }
        if ((property & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
            sb.append("Indicate ");
        }
        //Broadcast
        if ((property & BluetoothGattCharacteristic.PROPERTY_BROADCAST) > 0) {
            sb.append("Broadcast ");
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.append(")");
        return sb.toString();
    }
    public void initTitlebar() {
        iv_right.setVisibility(View.VISIBLE);
        iv_left.setImageDrawable(getResources().getDrawable(R.mipmap.icon_back));
        iv_right.setImageDrawable(getResources().getDrawable(R.mipmap.icon_topbar_overflow));
    }
    QMUICommonListItemView nameItem;
    QMUICommonListItemView macItem;
    QMUICommonListItemView statuItem;
    View.OnClickListener onClickListener;
    private void initGroupListView() {
        nameItem = mGroupListView.createItemView(null,
                "蓝牙", "未连接",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_NONE);
        nameItem.setId(R.id.address_ble_name);
        macItem = mGroupListView.createItemView(null,
                "mac", "",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_NONE);
        macItem.setId(R.id.address_ble_mac);
        statuItem = mGroupListView.createItemView(null,
                "状态", "未连接",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_NONE);
        statuItem.setId(R.id.address_ble_statu);
        onClickListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (v instanceof QMUICommonListItemView) {
                    CharSequence text = ((QMUICommonListItemView) v).getDetailText();
                    List<Map<String, String>> mbluetoothServices = Singletion.getInstance().mbluetoothServices;
                    CharSequence title = ((QMUICommonListItemView) v).getText();
                    if(title.equals("(传感器编址)简易模式")){
                        if(mbluetoothServices == null ||  mbluetoothServices.size() < 1 ){
                            Toast("服务信息错误,请重新连接");
                            return;
                        }
                        Map<String, String> map = mbluetoothServices.get(0);
                        Singletion.getInstance().uuid  = map.get("uuid").toString();
                        Intent intent = new Intent(ConnActivity.this,WriteActivity.class);
                        startActivity(intent);
                    }else if(title.equals("(传感器编址)专家模式")){
                        if(mbluetoothServices == null ||  mbluetoothServices.size() < 2 ){
                            Toast("服务信息错误,请重新连接");
                            return;
                        }
                        Map<String, String> map = mbluetoothServices.get(1);
                        Singletion.getInstance().uuid  = map.get("uuid").toString();
                        Intent intent = new Intent(ConnActivity.this,NormalWriteActivity.class);
                        startActivity(intent);
                    }
                }
            }
        };
        int size = QMUIDisplayHelper.dp2px(this, 20);
        QMUIGroupListView.newSection(this)
                .setTitle("Section 1:蓝牙信息")
                .setDescription("")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                .addItemView(nameItem, onClickListener)
                .addItemView(statuItem, onClickListener)
                .addItemView(macItem, onClickListener)
                .addTo(mGroupListView);
    }
    private void updateUi() {
        //TODO åŠ¨æ€æ›´æ–°UI
        BluetoothLeDevice mDevice = Singletion.getInstance().mDevice;
        List<Map<String, String>> mbluetoothServices = Singletion.getInstance().mbluetoothServices;
        boolean mConnected = Singletion.getInstance().mConnected;
        if (!mConnected || mbluetoothServices == null || mDevice == null) {
            Toast("异常错误,请退出重试!");
        } else {
            if (nameItem != null) nameItem.setDetailText(mDevice.getName());
            if (statuItem != null) statuItem.setDetailText("已连接");
            if (macItem != null) macItem.setDetailText(mDevice.getAddress());
            //创建可写服务信息
            for (int i = 0; i < mbluetoothServices.size(); i++) {
                Map<String, String> map = mbluetoothServices.get(i);
                //map.get("uuid")
                String title =  i == 0 ? "(传感器编址)简易模式"   : "(传感器编址)专家模式" ;
                QMUICommonListItemView itemView = mGroupListView.createItemView(title);
                itemView.setOrientation(QMUICommonListItemView.VERTICAL);
                itemView.setDetailText(map.get("name"));
                int size = QMUIDisplayHelper.dp2px(this, 20);
                int section = i + 2;
                int service = i + 1;
                itemView.setAccessoryType(QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
                QMUIGroupListView.newSection(this)
                        .setTitle("Section " + section + ":编址服务" + service)
                        .setDescription("")
                        .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                        .addItemView(itemView, onClickListener)
                        .addTo(mGroupListView);
            }
        }
    }
    private void disconnect() {
        if (connectManager != null) {
            if (connectManager.isConnectDevice()) {
                if (Singletion.getInstance().mDevice != null && Singletion.getInstance().mDevice.getAddress() != null) {
                    connectManager.disconnect(Singletion.getInstance().mDevice.getAddress());
                    connectManager.removeConnectStateListener(stateListener);
                }
            }
        }
    }
    private void connect() {
        if (connectManager != null) {
            if (!connectManager.isConnectDevice()) {
                if (Singletion.getInstance().mDevice != null && Singletion.getInstance().mDevice.getAddress() != null)
                    connectManager.connect(Singletion.getInstance().mDevice.getAddress());
            }
        }
    }
    private void showDisconnectDialog() {
        new QMUIDialog.MessageDialogBuilder(ConnActivity.this)
                .setTitle("提示")
                .setMessage("蓝牙已连接,是否断开连接?")
                .addAction("取消", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        dialog.dismiss();
                    }
                })
                .addAction("确定", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        disconnect();
                        dialog.dismiss();
                        finish();
                    }
                })
                .create(com.qmuiteam.qmui.R.style.QMUI_Dialog).show();
    }
    @Override
    public void onBackPressed() {
        if (connectManager != null && connectManager.isConnectDevice()) {
            showDisconnectDialog();
        } else {
            super.onBackPressed();
        }
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        disconnect();
    }
}
app/src/main/java/com/shlb/comb/activity/CurrentDeviceActivity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,151 @@
package com.shlb.comb.activity;
import android.bluetooth.BluetoothAdapter;
import android.content.pm.PackageManager;
import android.os.Build;
import android.view.View;
import com.qmuiteam.qmui.widget.grouplist.QMUICommonListItemView;
import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView;
import com.shlb.comb.R;
import com.shlb.comb.base.BaseActivity;
import com.shlb.comb.event.UpdateEvent;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
public class CurrentDeviceActivity extends BaseActivity {
    private QMUIGroupListView mGroupHardware;
    private QMUIGroupListView mGroupBle;
    private QMUIGroupListView mGroupAudio;
    private QMUIGroupListView mGroupScreen;
    @Override
    protected void contentView() {
        setContentView(R.layout.activity_current_device);
    }
    @Override
    protected void initView() {
        super.initHead();
        tv_center.setText("设备信息");
        if (iv_right != null) iv_right.setVisibility(View.GONE);
        if (iv_left != null) {
            iv_left.setImageDrawable(getResources().getDrawable(R.mipmap.icon_back));
            iv_left.setOnClickListener(v -> finish());
        }
        mGroupHardware = findViewById(R.id.group_hardware);
        mGroupBle = findViewById(R.id.group_ble);
        mGroupAudio = findViewById(R.id.group_audio);
        mGroupScreen = findViewById(R.id.group_screen);
    }
    @Override
    protected void initData() {
        initHardwareInfo();
        initBleInfo();
        initAudioInfo();
        initScreenInfo();
    }
    @Override
    protected void initEvent() {
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvent(UpdateEvent event) {
        // Required by BaseActivity's EventBus registration
    }
    private void initHardwareInfo() {
        QMUIGroupListView.newSection(this)
                .setTitle("硬件信息")
                .addItemView(createItem("设备名称", Build.DEVICE), null)
                .addItemView(createItem("Android ç‰ˆæœ¬", Build.VERSION.RELEASE), null)
                .addItemView(createItem("制造商", Build.MANUFACTURER), null)
                .addItemView(createItem("型号", Build.MODEL), null)
                .addItemView(createItem("构建版本", Build.DISPLAY), null)
                .addItemView(createItem("主板", Build.BOARD), null)
                .addItemView(createItem("产品名", Build.PRODUCT), null)
                .addTo(mGroupHardware);
    }
    private void initBleInfo() {
        PackageManager pm = getPackageManager();
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        boolean isBleSupported = pm.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
        QMUIGroupListView.Section section = QMUIGroupListView.newSection(this)
                .setTitle("低功耗蓝牙");
        section.addItemView(createItem("支持 BLE", isBleSupported ? "是" : "否", isBleSupported), null);
        if (adapter != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                boolean isOffloadedFilteringSupported = adapter.isOffloadedFilteringSupported();
                boolean isOffloadedScanBatchingSupported = adapter.isOffloadedScanBatchingSupported();
                boolean isMultipleAdvertisementSupported = adapter.isMultipleAdvertisementSupported();
                section.addItemView(createItem("支持脱机过滤", isOffloadedFilteringSupported ? "是" : "否", isOffloadedFilteringSupported), null);
                section.addItemView(createItem("支持脱机扫描批处理", isOffloadedScanBatchingSupported ? "是" : "否", isOffloadedScanBatchingSupported), null);
                section.addItemView(createItem("支持多重广播", isMultipleAdvertisementSupported ? "是" : "否", isMultipleAdvertisementSupported), null);
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                boolean isLe2MPhySupported = adapter.isLe2MPhySupported();
                boolean isLeCodedPhySupported = adapter.isLeCodedPhySupported();
                boolean isLeExtendedAdvertisingSupported = adapter.isLeExtendedAdvertisingSupported();
                boolean isLePeriodicAdvertisingSupported = adapter.isLePeriodicAdvertisingSupported();
                section.addItemView(createItem("支持高速 (PHY 2M)", isLe2MPhySupported ? "是" : "否", isLe2MPhySupported), null);
                section.addItemView(createItem("支持远距离 (PHY Coded)", isLeCodedPhySupported ? "是" : "否", isLeCodedPhySupported), null);
                section.addItemView(createItem("支持扩展广播", isLeExtendedAdvertisingSupported ? "是" : "否", isLeExtendedAdvertisingSupported), null);
                section.addItemView(createItem("支持周期性广播", isLePeriodicAdvertisingSupported ? "是" : "否", isLePeriodicAdvertisingSupported), null);
            }
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                 int maxDataLength = adapter.isLeExtendedAdvertisingSupported() ? 1650 : 31;
                 section.addItemView(createItem("最大广播数据长度", String.valueOf(maxDataLength)), null);
             }
        }
        section.addTo(mGroupBle);
    }
    private void initAudioInfo() {
        QMUIGroupListView.newSection(this)
                .setTitle("蓝牙音频")
                .addItemView(createItem("支持 LE Audio", "否", false), null)
                .addItemView(createItem("支持 LE å¹¿æ’­æº", "否", false), null)
                .addItemView(createItem("支持 LE å¹¿æ’­åŠ©æ‰‹", "否", false), null)
                .addItemView(createItem("最大连接音频设备数", "0"), null)
                .addTo(mGroupAudio);
    }
    private void initScreenInfo() {
        android.util.DisplayMetrics dm = getResources().getDisplayMetrics();
        int screenWidth = dm.widthPixels;
        int screenHeight = dm.heightPixels;
        float density = dm.density;
        int densityDpi = dm.densityDpi;
        QMUIGroupListView.newSection(this)
                .setTitle("屏幕信息")
                .addItemView(createItem("分辨率", screenWidth + " x " + screenHeight), null)
                .addItemView(createItem("密度", density + " (" + densityDpi + " dpi)"), null)
                .addTo(mGroupScreen);
    }
    private QMUICommonListItemView createItem(String title, String detail) {
        return createItem(title, detail, true);
    }
    private QMUICommonListItemView createItem(String title, String detail, boolean isSupported) {
        QMUICommonListItemView item = mGroupHardware.createItemView(title);
        item.setOrientation(QMUICommonListItemView.HORIZONTAL);
        item.setDetailText(detail);
        return item;
    }
}
app/src/main/java/com/shlb/comb/activity/HomeActivity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,355 @@
package com.shlb.comb.activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import com.blakequ.bluetooth_manager_lib.scan.BluetoothScanManager;
import com.blakequ.bluetooth_manager_lib.util.BluetoothUtils;
import com.qmuiteam.qmui.widget.dialog.QMUIDialog;
import com.shlb.comb.R;
import com.shlb.comb.base.BaseActivity;
import com.shlb.comb.event.UpdateEvent;
import com.shlb.comb.fragment.DeviceListFragment;
import com.shlb.comb.fragment.SettingsFragment;
import com.shlb.comb.manager.BleGlobalManager;
import com.shlb.comb.model.BluetoothLeDeviceStore;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
public class HomeActivity extends BaseActivity implements AdapterView.OnItemClickListener {
    private DrawerLayout drawerLayout;
    protected TextView tv_scan;
    private long lastScanClickTime = 0; // ç”¨äºŽèŠ‚æµ
    // Bluetooth Variables
    private BluetoothScanManager scanManager;
    private BluetoothLeDeviceStore mDeviceStore;
    private BluetoothUtils mBluetoothUtils;
    private SettingsFragment settingsFragment;
    private DeviceListFragment deviceListFragment;
    @Override
    protected void contentView() {
        setContentView(R.layout.activity_home);
        drawerLayout = findViewById(R.id.drawer_layout);
        // è®¾ç½®å…¨å±ï¼Œä½¿æŠ½å±‰å¯ä»¥è¦†ç›–状态栏和导航栏
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = getWindow();
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(Color.TRANSPARENT);
            window.setNavigationBarColor(Color.TRANSPARENT);
        }
    }
    @Override
    protected void initView() {
        super.initHead();
        tv_scan = findViewById(R.id.tv_scan);
        try {
            TextView tvVersion = findViewById(R.id.tv_version);
            if (tvVersion != null) {
                String versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
                tvVersion.setText("Version " + versionName);
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        initTitlebar();
        getSupportFragmentManager().addOnBackStackChangedListener(() -> {
            Fragment current = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
            if (current instanceof SettingsFragment) {
                tv_center.setText("实时监控");
                if (iv_left != null) {
                    iv_left.setImageResource(R.mipmap.icon_menu);
                    iv_left.setOnClickListener(v -> {
                        if (drawerLayout != null) {
                            drawerLayout.openDrawer(GravityCompat.START);
                        }
                    });
                }
                if (tv_scan != null) {
                    tv_scan.setVisibility(View.VISIBLE);
                    if (BleGlobalManager.getInstance().isConnected()) {
                        updateConnectedUI();
                    } else {
                        tv_scan.setText("连接蓝牙");
                        tv_scan.setOnClickListener(v -> showDeviceListFragment());
                    }
                }
                if (iv_right != null) iv_right.setVisibility(View.GONE);
            } else if (current instanceof DeviceListFragment) {
                tv_center.setText("蓝牙设备");
                if (iv_left != null) {
                    iv_left.setImageResource(R.mipmap.icon_back);
                    iv_left.setOnClickListener(v -> onBackPressed());
                }
                if (tv_scan != null) {
                    tv_scan.setVisibility(View.VISIBLE);
                    tv_scan.setText("SCAN");
                    tv_scan.setOnClickListener(v -> {
                        long currentTime = System.currentTimeMillis();
                        if (currentTime - lastScanClickTime < 2000) {
                            Toast("点击过快,请稍后重试!");
                            return;
                        }
                        lastScanClickTime = currentTime;
                        if (deviceListFragment != null && deviceListFragment.isVisible()) {
                            deviceListFragment.startScan();
                        }
                    });
                }
                if (iv_right != null) iv_right.setVisibility(View.VISIBLE);
            }
        });
        // Default show SettingsFragment
        showSettingsFragment();
    }
    @Override
    protected void initData() {
        mBluetoothUtils = BluetoothUtils.getInstance(this);
        mDeviceStore = new BluetoothLeDeviceStore();
        scanManager = BluetoothScanManager.getInstance(this);
    }
    @Override
    protected void initEvent() {
        iv_left.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (drawerLayout != null) {
                    drawerLayout.openDrawer(GravityCompat.START);
                }
            }
        });
        // æŠ½å±‰èœå•点击事件
        // å®žæ—¶ç›‘控
        findViewById(R.id.menu_monitor).setOnClickListener(v -> {
            updateDrawerSelection(R.id.menu_monitor);
            Toast.makeText(this, "实时监控", Toast.LENGTH_SHORT).show();
            showSettingsFragment();
            drawerLayout.closeDrawer(GravityCompat.START);
        });
        // è“ç‰™è®¾å¤‡
        findViewById(R.id.menu_devices).setOnClickListener(v -> {
            updateDrawerSelection(R.id.menu_devices);
            showDeviceListFragment();
            drawerLayout.closeDrawer(GravityCompat.START);
        });
        // å½“前设备
        findViewById(R.id.menu_current_device).setOnClickListener(v -> {
            drawerLayout.closeDrawer(GravityCompat.START);
            startActivity(new Intent(this, CurrentDeviceActivity.class));
        });
        // å‚数设定
        findViewById(R.id.menu_settings).setOnClickListener(v -> {
            drawerLayout.closeDrawer(GravityCompat.START);
            startActivity(new Intent(this, ParameterSettingActivity.class));
        });
        // é«˜çº§è®¾ç½®
        findViewById(R.id.menu_advanced_settings).setOnClickListener(v -> {
            drawerLayout.closeDrawer(GravityCompat.START);
            startActivity(new Intent(this, AdvancedSettingActivity.class));
        });
    }
    private void updateDrawerSelection(int selectedId) {
        int[] menuIds = {R.id.menu_monitor, R.id.menu_devices, R.id.menu_current_device, R.id.menu_settings, R.id.menu_advanced_settings};
        for (int id : menuIds) {
            View view = findViewById(id);
            if (view != null) {
                boolean isSelected = (id == selectedId);
                view.setSelected(isSelected);
                // Manually set selected state for children because duplicateParentState="true" might be missing
                if (view instanceof android.view.ViewGroup) {
                    android.view.ViewGroup group = (android.view.ViewGroup) view;
                    for (int i = 0; i < group.getChildCount(); i++) {
                        group.getChildAt(i).setSelected(isSelected);
                    }
                }
            }
        }
    }
    public void showSettingsFragment() {
        tv_center.setText("实时监控");
        if (tv_scan != null) {
            tv_scan.setVisibility(View.VISIBLE);
            tv_scan.setText("连接蓝牙");
            tv_scan.setOnClickListener(v -> showDeviceListFragment());
        }
        if (iv_right != null) iv_right.setVisibility(View.GONE);
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        if (settingsFragment == null) {
            settingsFragment = new SettingsFragment();
        }
        ft.replace(R.id.fragment_container, settingsFragment);
        ft.commit();
    }
    public void showDeviceListFragment() {
        updateDrawerSelection(R.id.menu_devices);
        tv_center.setText("蓝牙设备");
        if (tv_scan != null) {
            tv_scan.setVisibility(View.VISIBLE);
            tv_scan.setText("SCAN");
            tv_scan.setOnClickListener(v -> {
                // èŠ‚æµé€»è¾‘ï¼šä¸¤ç§’å†…åªèƒ½ç‚¹ä¸€æ¬¡
                long currentTime = System.currentTimeMillis();
                if (currentTime - lastScanClickTime < 2000) {
                    Toast("点击过快,请稍后重试!");
                    return;
                }
                lastScanClickTime = currentTime;
                if (deviceListFragment != null && deviceListFragment.isVisible()&&scanManager!=null&&!scanManager.isScanning()) {
                    deviceListFragment.startScan();
                }
            });
        }
        if (iv_right != null) iv_right.setVisibility(View.VISIBLE);
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        if (deviceListFragment == null) {
            deviceListFragment = new DeviceListFragment();
        }
        ft.replace(R.id.fragment_container, deviceListFragment);
        ft.addToBackStack(null);
        ft.commit();
    }
    public BluetoothScanManager getScanManager() {
        return scanManager;
    }
    public BluetoothLeDeviceStore getDeviceStore() {
        return mDeviceStore;
    }
    public BluetoothUtils getBluetoothUtils() {
        return mBluetoothUtils;
    }
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventRefresh(UpdateEvent event) {
        if (event.getType() == UpdateEvent.Type.CONN_STATU) {
            int status = 0;
            if (event.getObj() instanceof Integer) {
                status = (int) event.getObj();
            }
            if (status == android.bluetooth.BluetoothProfile.STATE_CONNECTED) {
                updateConnectedUI();
            } else if (status == android.bluetooth.BluetoothProfile.STATE_DISCONNECTED) {
                if (tv_scan != null) {
                    tv_scan.setText("连接蓝牙");
                    tv_scan.setOnClickListener(v -> showDeviceListFragment());
                }
            }
        }
    }
    private void updateConnectedUI() {
        if (tv_scan == null) return;
        com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice device = BleGlobalManager.getInstance().getBluetoothLeDevice();
        String name = (device != null) ? device.getName() : "Unknown";
        if (name == null) name = "Unknown";
        if (name.length() > 8) {
            name = name.substring(0, 8) + "...";
        }
        tv_scan.setText(name);
        tv_scan.setOnClickListener(v -> {
            new QMUIDialog.MessageDialogBuilder(this)
                    .setTitle("提示")
                    .setMessage("是否断开蓝牙连接?")
                    .addAction("取消", (dialog, index) -> dialog.dismiss())
                    .addAction("确定", (dialog, index) -> {
                        dialog.dismiss();
                        BleGlobalManager.getInstance().disconnect();
                    })
                    .create(com.qmuiteam.qmui.R.style.QMUI_Dialog).show();
        });
    }
    public void initTitlebar() {
        if (iv_right != null) {
            iv_right.setVisibility(View.VISIBLE);
            iv_right.setOnClickListener(v -> {
                androidx.appcompat.widget.PopupMenu popup = new androidx.appcompat.widget.PopupMenu(this, v);
                popup.getMenu().add(0, 0, 0, "监控页");
                popup.getMenu().add(0, 1, 1, "测试页");
                popup.setOnMenuItemClickListener(item -> {
                    if (item.getItemId() == 0) {
                        DeviceListFragment.setNavigationMode(DeviceListFragment.MODE_RETURN_SETTINGS);
                        Toast.makeText(HomeActivity.this, "已切换:连接后返回监控页", Toast.LENGTH_SHORT).show();
                    } else if (item.getItemId() == 1) {
                        DeviceListFragment.setNavigationMode(DeviceListFragment.MODE_GO_TO_TEST);
                        Toast.makeText(HomeActivity.this, "已切换:连接后跳转到测试页", Toast.LENGTH_SHORT).show();
                    }
                    return true;
                });
                popup.show();
            });
        }
    }
    @Override
    public void onBackPressed() {
        if (drawerLayout != null && drawerLayout.isDrawerOpen(GravityCompat.START)) {
            drawerLayout.closeDrawer(GravityCompat.START);
        } else {
            // Check if we are in DeviceListFragment and can go back
            Fragment current = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
            if (current instanceof DeviceListFragment) {
                super.onBackPressed();
            } else {
                ExitApp();
            }
        }
    }
    private long exitTime = 0;
    public void ExitApp() {
        if ((System.currentTimeMillis() - exitTime) > 2000) {
            Toast.makeText(this, "再按一次退出程序!", Toast.LENGTH_SHORT).show();
            exitTime = System.currentTimeMillis();
        } else {
            finish();
        }
    }
}
app/src/main/java/com/shlb/comb/activity/NormalWriteActivity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,536 @@
package com.shlb.comb.activity;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.text.InputType;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothConnectManager;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothSubScribeData;
import com.blakequ.bluetooth_manager_lib.connect.ConnectState;
import com.blakequ.bluetooth_manager_lib.connect.ConnectStateListener;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.util.BluetoothUtils;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.qmuiteam.qmui.util.QMUIDisplayHelper;
import com.qmuiteam.qmui.widget.dialog.QMUIDialog;
import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction;
import com.qmuiteam.qmui.widget.grouplist.QMUICommonListItemView;
import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView;
import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton;
import com.shlb.comb.R;
import com.shlb.comb.base.BaseActivity;
import com.shlb.comb.event.UpdateEvent;
import com.shlb.comb.util.CMD;
import com.shlb.comb.util.CRCutil;
import com.shlb.comb.util.Singletion;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class NormalWriteActivity extends BaseActivity {
    QMUIGroupListView mGroupListView;
    Button send;
    LinearLayout llRoot;
    private BluetoothConnectManager connectManager;
    private BluetoothGatt gatt;
    private BluetoothGattCharacteristic characteristic;
    @Override
    protected void contentView() {
        setContentView(R.layout.normal_activity_write);
    }
    @Override
    protected void initView() {
        super.initHead();
        mGroupListView = findViewById(R.id.groupListView);
        send = findViewById(R.id.send);
        llRoot = findViewById(R.id.ll_root);
        initTitlebar();
        tv_center.setText("编址");
        initGroupListView();
    }
    @Override
    protected void initData() {
        String uuid = Singletion.getInstance().uuid;
        initWrite(uuid);
    }
    @Override
    protected void initEvent() {
        send.setOnClickListener(this);
        int[] ids = {R.id.btnExit, R.id.btnInto, R.id.btnClean, R.id.btn1, R.id.btn2, R.id.btn3, R.id.btn4, R.id.btn5, R.id.btn6, R.id.btn7, R.id.btn8, R.id.btn9, R.id.btn10, R.id.btn11, R.id.btn12, R.id.btn13, R.id.btn14, R.id.btn15, R.id.btn16};
        for (int id : ids) {
            View v = findViewById(id);
            if (v != null) v.setOnClickListener(this);
        }
    }
    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.iv_left) {
            finish();
        } else if (id == R.id.btnExit) {
            writeCmd(CMD.EXIT);
        } else if (id == R.id.btnInto) {
            writeCmd(CMD.GETINTO);
        } else if (id == R.id.btnClean) {
            writeCmd(CMD.CLEAN);
        } else if (id == R.id.btn1) {
            sendYJCmd(1);
        } else if (id == R.id.btn2) {
            sendYJCmd(2);
        } else if (id == R.id.btn3) {
            sendYJCmd(3);
        } else if (id == R.id.btn4) {
            sendYJCmd(4);
        } else if (id == R.id.btn5) {
            sendYJCmd(5);
        } else if (id == R.id.btn6) {
            sendYJCmd(6);
        } else if (id == R.id.btn7) {
            sendYJCmd(7);
        } else if (id == R.id.btn8) {
            sendYJCmd(8);
        } else if (id == R.id.btn9) {
            sendYJCmd(9);
        } else if (id == R.id.btn10) {
            sendYJCmd(10);
        } else if (id == R.id.btn11) {
            sendYJCmd(11);
        } else if (id == R.id.btn12) {
            sendYJCmd(12);
        } else if (id == R.id.btn13) {
            sendYJCmd(13);
        } else if (id == R.id.btn14) {
            sendYJCmd(14);
        } else if (id == R.id.btn15) {
            sendYJCmd(15);
        } else if (id == R.id.btn16) {
            sendYJCmd(16);
        }
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventReceiveNotify(UpdateEvent event) {
        if (event != null && event.getType() == UpdateEvent.Type.BLE_DATA) {
            BluetoothGattCharacteristic characteristic = (BluetoothGattCharacteristic) event.getObj();
            final byte[] dataArr = characteristic.getValue();
            final String flag = event.getMsg();
            if (dataArr != null && dataArr.length > 0) {
                if (flag.equals("read")) {
                } else if (flag.equals("write")) {
                } else if (flag.equals("notify")) {
                    //返回结果
                    String crc = CRCutil.getCRC(dataArr);
                    LogUtils.e("resCRC", crc);
                    //CRC计算结果为0时命令执行成功,自动执行退出命令
                    if (crc.equals("0")) {
                        writeCmd(CMD.EXIT);
                        Toast("操作成功,自动执行退出编址指令");
                        curCmd = -1;
                        item6.setText("编址成功");
                    }
                    //计算返回为0时 è‡ªåŠ¨é€€å‡º
                    LogUtils.e("res", ByteUtils.byteArrayToHexString(dataArr));
                } else {
                    Toast("解析返回信息失败!");
                }
            } else {
                Toast("返回信息为空!");
            }
        }
    }
    QMUICommonListItemView item1;
    QMUICommonListItemView item2;
    QMUICommonListItemView item3;
    QMUICommonListItemView item4;
    QMUICommonListItemView item5;
    QMUICommonListItemView item6;
    View.OnClickListener onClickListener;
    int curCmd = -1;
    String[] cmdName = {"进入编址", "清除地址", "编辑地址", "退出编址", "一键编址"};
    private void initGroupListView() {
        item1 = mGroupListView.createItemView(null,
                cmdName[0], "",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item2 = mGroupListView.createItemView(null,
                cmdName[1], "",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item3 = mGroupListView.createItemView(null,
                cmdName[2], "点击选择编号",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item4 = mGroupListView.createItemView(null,
                cmdName[3], "",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        onClickListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (v instanceof QMUICommonListItemView) {
                    CharSequence text = ((QMUICommonListItemView) v).getText();
                    //进入编址
                    if (text.equals(cmdName[0])) {
                        curCmd = 1;
                        item6.setText(cmdName[0]);
                        item6.setDetailText("");
                        //清除地址
                    } else if (text.equals(cmdName[1])) {
                        curCmd = 2;
                        item6.setText(cmdName[1]);
                        item6.setDetailText("");
                        //编辑地址
                    } else if (text.equals(cmdName[2])) {
                        showEditTextDialog();
                        //退出编址
                    } else if (text.equals(cmdName[3])) {
                        curCmd = 4;
                        item6.setText(cmdName[3]);
                        item6.setDetailText("");
                        //一键编址
                    } else if (text.equals(cmdName[4])) {
                        showQuickDialog();
                    }
                }
            }
        };
        int size = QMUIDisplayHelper.dp2px(this, 20);
        QMUIGroupListView.newSection(this)
                .setTitle("Section 1:操作指令")
                .setDescription("")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                .addItemView(item1, onClickListener)
                .addItemView(item2, onClickListener)
                .addItemView(item3, onClickListener)
                .addItemView(item4, onClickListener)
                .addTo(mGroupListView);
        item5 = mGroupListView.createItemView(cmdName[4]);
        item5.setOrientation(QMUICommonListItemView.VERTICAL);
        item5.setDetailText("进入-清除-编址-退出");
        item5.setAccessoryType(QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item6 = mGroupListView.createItemView("待选择");
        item6.setOrientation(QMUICommonListItemView.HORIZONTAL);
        QMUIGroupListView.newSection(this)
                .setTitle("Section 2:快捷指令")
                .setDescription("")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                .addItemView(item5, onClickListener)
                .addTo(mGroupListView);
        QMUIGroupListView.newSection(this)
                .setTitle("Section 3:当前指令")
                .setDescription("")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                .addItemView(item6, onClickListener)
                .addTo(mGroupListView);
    }
    /**
     * åˆå§‹åŒ–写入服务
     *
     * @param uu
     */
    private void initWrite(String uu) {
        BluetoothLeDevice mDevice = Singletion.getInstance().mDevice;
        connectManager = BluetoothConnectManager.getInstance(this);
        connectManager.addConnectStateListener(listener);
        String uuid = uu;
        UUID serverUUid = null;
        gatt = connectManager.getBluetoothGatt(mDevice.getAddress());
        if (gatt != null) {
            List<BluetoothGattService> list = gatt.getServices();
            if (list != null) {
                for (BluetoothGattService service : list) {
                    for (BluetoothGattCharacteristic characteristics : service.getCharacteristics()) {
                        if (characteristics.getUuid().toString().equals(uuid)) {
                            characteristic = characteristics;
                            serverUUid = service.getUuid();
                            break;
                        }
                    }
                }
            }
        }
        if (serverUUid == null) {
            Toast("serverUUid不能为空!");
            return;
        }
        //1.set service uuid
        connectManager.setServiceUUID(serverUUid.toString());
        //2.clean history descriptor data
        connectManager.cleanSubscribeData();
        //3.add subscribe params
        if (BluetoothUtils.isCharacteristicRead(characteristic.getProperties())) {
            connectManager.addBluetoothSubscribeData(
                    new BluetoothSubScribeData.Builder().setCharacteristicRead(characteristic.getUuid()).build());
        }
        if (BluetoothUtils.isCharacteristicNotify(characteristic.getProperties())) {
            connectManager.addBluetoothSubscribeData(
                    new BluetoothSubScribeData.Builder().setCharacteristicNotify(characteristic.getUuid()).build()
            );
        }
        connectManager.startSubscribe(gatt);
    }
    private void writeCmd(String cmd) {
        int len = cmd.length();
        if (len > 0 && len % 2 == 0) {
            byte[] bytes = invertStringToBytes(cmd);
            if (bytes != null) {
                characteristic.setValue(bytes);
                gatt.writeCharacteristic(characteristic);
                runOnUiThread(() -> {
                    item6.setDetailText(item6.getDetailText() + "  å·²å‘送");
                });
            } else {
                Log.e("tag", "write value fail");
            }
        } else {
            runOnUiThread(() -> {
                Toast("指令错误!");
            });
        }
    }
    private byte[] invertStringToBytes(String value) {
        int len = value.length() / 2;
        if (len > 0) {
            byte[] bytes = new byte[len];
            for (int i = 0; i < len; i++) {
                Integer val = Integer.valueOf(value.substring(i * 2, i * 2 + 2), 16);
                bytes[i] = val.byteValue();
            }
            return bytes;
        }
        return null;
    }
    private List<Map<String, Object>> getWriteData() {
        List<Map<String, Object>> res = new ArrayList<>();
        Map<String, Object> map1 = new HashMap<>();
        map1.put("name", "进入编址状态");
        map1.put("flag", "0");
        map1.put("value", "A55A060100000000002F35");
        res.add(map1);
        Map<String, Object> map2 = new HashMap<>();
        map2.put("name", "退出编址状态");
        map2.put("flag", "0");
        map2.put("value", "A55A060400000000002F60");
        res.add(map2);
        Map<String, Object> map3 = new HashMap<>();
        map3.put("name", "编址");
        map3.put("flag", "0");
        map3.put("value", "A55A060300020000002F6F");
        res.add(map3);
        Map<String, Object> map4 = new HashMap<>();
        map4.put("name", "清除地址");
        map4.put("flag", "0");
        map4.put("value", "A55A060200000000002F06");
        res.add(map4);
        return res;
    }
    private ConnectStateListener listener = new ConnectStateListener() {
        @Override
        public void onConnectStateChanged(String address, ConnectState state) {
            switch (state) {
                case CONNECTED:
                    break;
                case CONNECTING:
                    break;
                case NORMAL:
                    break;
            }
        }
    };
    /**
     * è¾“入编址序号后 æ‰§è¡Œç¼–址指令
     */
    private void showEditTextDialog() {
        final QMUIDialog.EditTextDialogBuilder builder = new QMUIDialog.EditTextDialogBuilder(this);
        builder.setTitle("提示")
                .setPlaceholder("请输入编址号码(1-16)")
                .setInputType(InputType.TYPE_CLASS_TEXT)
                .addAction("取消", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        dialog.dismiss();
                    }
                })
                .addAction("确定", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        String text = builder.getEditText().getText().toString();
                        Integer res = -1;
                        try {
                            res = Integer.parseInt(text);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        if (res == null || res < 0 || res > 16) {
                            Toast("请输入1-16数字");
                        } else {
                            item3.setDetailText(text);
                            curCmd = 3;
                            item6.setText(cmdName[2]);
                            item6.setDetailText(text);
                            dialog.dismiss();
                        }
                    }
                })
                .create(com.qmuiteam.qmui.R.style.QMUI_Dialog).show();
    }
    /**
     * è¾“入编址序号后  ä¸€é”®æ‰§è¡Œæ‰€æœ‰æŒ‡ä»¤
     */
    private void showQuickDialog() {
        final QMUIDialog.EditTextDialogBuilder builder = new QMUIDialog.EditTextDialogBuilder(this);
        builder.setTitle("提示")
                .setPlaceholder("请输入编址号码(1-16)")
                .setInputType(InputType.TYPE_CLASS_TEXT)
                .addAction("取消", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        dialog.dismiss();
                    }
                })
                .addAction("确定", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        String text = builder.getEditText().getText().toString();
                        Integer addrrNo = -1;
                        try {
                            addrrNo = Integer.parseInt(text);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        if (addrrNo == null || addrrNo < 0 || addrrNo > 16) {
                            Toast("请输入1-16数字");
                        } else {
                            curCmd = 5;
                            item5.setDetailText(text);
                            item6.setText(cmdName[4]);
                            item6.setDetailText(text);
                            dialog.dismiss();
                        }
                    }
                })
                .create(com.qmuiteam.qmui.R.style.QMUI_Dialog).show();
    }
    public void initTitlebar() {
        iv_right.setVisibility(View.VISIBLE);
        iv_left.setImageDrawable(getResources().getDrawable(R.mipmap.icon_back));
        iv_right.setImageDrawable(getResources().getDrawable(R.mipmap.icon_topbar_overflow));
    }
    /**
     * å‘送一键命令
     * @param no2
     */
   private  void sendYJCmd(int no2){
       if (no2 > 16|| no2<1 ) {
           Toast("地址数据异常");
           return;
       }
       ThreadUtils.executeBySingle(new ThreadUtils.SimpleTask<Object>() {
           @Override
           public Object doInBackground() throws Throwable {
               writeCmd(CMD.GETINTO);
               Thread.sleep(200);
               writeCmd(CMD.CLEAN);
               Thread.sleep(200);
               String sNo2 = Integer.toHexString(no2);
               //
               if (sNo2.length() < 2) {
                   sNo2 = "0" + sNo2;
               }
               String cmd = CMD.ADDR_PREFIX + sNo2 + CMD.ADDR_SUFFIX;
               byte[] bytes = invertStringToBytes(cmd);
               String crc = CRCutil.getCRC(bytes);
               String zl = cmd + crc;
               LogUtils.e("reszl", zl);
               Thread.sleep(200);
               writeCmd(zl);
               return null;
           }
           @Override
           public void onSuccess(Object result) {
           }
       });
       Toast("编址指令已发送!");
   }
}
app/src/main/java/com/shlb/comb/activity/ParameterSettingActivity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,665 @@
package com.shlb.comb.activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton;
import com.shlb.comb.R;
import com.shlb.comb.base.BaseActivity;
import com.shlb.comb.event.UpdateEvent;
import com.shlb.comb.manager.BleGlobalManager;
import com.shlb.comb.util.CMD;
import com.shlb.comb.util.CRCutil;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class ParameterSettingActivity extends BaseActivity {
    private Spinner sp_threshold_mode;
    private EditText et_threshold_value;
    private Button btn_threshold_read;
    private Button btn_threshold_write;
    private Spinner sp_sensitivity_mode;
    private EditText et_sensitivity_value;
    private Button btn_sensitivity_read;
    private Button btn_sensitivity_write;
    private Button btn_start_check;
    private Button btn_end_check;
    private Button btn_reset_mainboard;
    private Button btn_reset_sensor;
    private TextView tv_log;
    private TextView tv_right_text;
    // New components
    private RecyclerView rvGrid;
    private QMUIRoundButton btnReadData;
    private QMUIRoundButton btnClearLog;
    private ScrollView svLog;
    private GridAdapter mAdapter;
    private List<BoxStatus> boxStatusList = new ArrayList<>();
    private StringBuilder logBuilder = new StringBuilder();
    private boolean isSelfCheckMode = false;
    private String pendingSettingCmd = "";
    private SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
    @Override
    protected void contentView() {
        setContentView(R.layout.activity_parameter_setting);
    }
    @Override
    protected void initView() {
        super.initHead();
        if (tv_center != null) {
            tv_center.setText("参数设定");
        }
        if (iv_left != null) {
            iv_left.setImageResource(R.mipmap.icon_back);
        }
        sp_threshold_mode = findViewById(R.id.sp_threshold_mode);
        et_threshold_value = findViewById(R.id.et_threshold_value);
        btn_threshold_read = findViewById(R.id.btn_threshold_read);
        btn_threshold_write = findViewById(R.id.btn_threshold_write);
        sp_sensitivity_mode = findViewById(R.id.sp_sensitivity_mode);
        et_sensitivity_value = findViewById(R.id.et_sensitivity_value);
        btn_sensitivity_read = findViewById(R.id.btn_sensitivity_read);
        btn_sensitivity_write = findViewById(R.id.btn_sensitivity_write);
        btn_start_check = findViewById(R.id.btn_start_check);
        btn_end_check = findViewById(R.id.btn_end_check);
        btn_reset_mainboard = findViewById(R.id.btn_reset_mainboard);
        btn_reset_sensor = findViewById(R.id.btn_reset_sensor);
        tv_log = findViewById(R.id.tv_log);
        tv_right_text = findViewById(R.id.tv_right_text);
        // Init new components
        rvGrid = findViewById(R.id.rv_grid);
        // btnReadData was removed from layout
        btnClearLog = findViewById(R.id.btn_clear_log);
        svLog = findViewById(R.id.sv_log);
        // Log setup
        if (svLog != null) {
            svLog.setOnTouchListener((v, event) -> {
                v.getParent().requestDisallowInterceptTouchEvent(true);
                if ((event.getAction() & android.view.MotionEvent.ACTION_MASK) == android.view.MotionEvent.ACTION_UP) {
                    v.getParent().requestDisallowInterceptTouchEvent(false);
                }
                return false;
            });
        }
        if (logBuilder.length() > 0) {
            tv_log.setText(Html.fromHtml(logBuilder.toString()));
        } else {
            logBuilder.append("日志记录:<br>");
            tv_log.setText(Html.fromHtml(logBuilder.toString()));
        }
    }
    @Override
    protected void initData() {
        updateBluetoothStatus();
        // Initialize box status list
        boxStatusList.clear();
        for (int i = 1; i <= 30; i++) {
            boxStatusList.add(new BoxStatus(i));
        }
        // Grid Setup
        rvGrid.setLayoutManager(new GridLayoutManager(this, 10));
        mAdapter = new GridAdapter();
        rvGrid.setAdapter(mAdapter);
        if (BleGlobalManager.getInstance().isConnected()) {
             sendCmdWithCrc(CMD.ENTER_SETTING);
             // sendCmdWithCrc(CMD.READ_DATA);
        }
    }
    @Override
    protected void onDestroy() {
        if (BleGlobalManager.getInstance().isConnected()) {
            sendCmdWithCrc(CMD.EXIT_SETTING);
        }
        super.onDestroy();
    }
    @Override
    protected void initEvent() {
        View.OnClickListener listener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int id = v.getId();
                String action = "";
                if (id == R.id.btn_threshold_read) {
                    action = "读取阈值";
                    Toast.makeText(ParameterSettingActivity.this, "该功能暂未开放", Toast.LENGTH_SHORT).show();
                } else if (id == R.id.btn_threshold_write) {
                    handleThresholdWrite();
                } else if (id == R.id.btn_sensitivity_read) {
                    action = "读取灵敏度";
                    Toast.makeText(ParameterSettingActivity.this, "该功能暂未开放", Toast.LENGTH_SHORT).show();
                } else if (id == R.id.btn_sensitivity_write) {
                    handleSensitivityWrite();
                } else if (id == R.id.btn_start_check) {
                    action = "开始自检";
                    sendCmdWithCrc(CMD.WRITE_START_CHECK);
                    isSelfCheckMode = true;
                } else if (id == R.id.btn_end_check) {
                    action = "结束自检";
                    sendCmdWithCrc(CMD.WRITE_END_CHECK);
                    isSelfCheckMode = false;
                } else if (id == R.id.btn_reset_mainboard) {
                    showResetConfirmDialog("确定要执行主板复位吗?", CMD.WRITE_BOARD_RESET);
                } else if (id == R.id.btn_reset_sensor) {
                    showResetConfirmDialog("确定要执行传感器复位吗?", CMD.WRITE_SENSOR_RESET);
                }
                if (!action.isEmpty()) {
                    // Toast.makeText(ParameterSettingActivity.this, action, Toast.LENGTH_SHORT).show();
                    // appendLog(action);
                }
            }
        };
        btn_threshold_read.setOnClickListener(listener);
        btn_threshold_write.setOnClickListener(listener);
        btn_sensitivity_read.setOnClickListener(listener);
        btn_sensitivity_write.setOnClickListener(listener);
        btn_start_check.setOnClickListener(listener);
        btn_end_check.setOnClickListener(listener);
        btn_reset_mainboard.setOnClickListener(listener);
        btn_reset_sensor.setOnClickListener(listener);
        btnClearLog.setOnClickListener(v -> {
            logBuilder.setLength(0);
            logBuilder.append("日志记录:<br>");
            tv_log.setText(Html.fromHtml(logBuilder.toString()));
        });
    }
    private String getCmdDescription(String hex, Boolean isSent) {
        if (hex == null) return "";
        // Special handling for 3B (Enter/Exit Setting)
        if (hex.startsWith(CMD.ENTER_SETTING.substring(0, 8))) {
            if (hex.startsWith(CMD.ENTER_SETTING)) return "-进入设定";
            if (hex.startsWith(CMD.EXIT_SETTING)) return "-退出设定";
        }
        // å‘送指令匹配
        if (hex.startsWith(CMD.WRITE_THRESHOLD_SETTING)) return "-阈值设定";
        if (hex.startsWith(CMD.WRITE_SENSITIVITY_SETTING)) return "-灵敏度设定";
        if (hex.startsWith(CMD.WRITE_BOARD_RESET)) return "-主板复位";
        if (hex.startsWith(CMD.WRITE_SENSOR_RESET)) return "-传感器复位";
        if (hex.startsWith(CMD.WRITE_START_CHECK)) return "-开始自检";
        if (hex.startsWith(CMD.WRITE_END_CHECK)) return "-结束自检";
        if (hex.startsWith(CMD.READ_DATA.substring(0, 8))) return "-读数据";
        // æŽ¥æ”¶æ•°æ®åŒ¹é…
        if (hex.startsWith(CMD.WRITE_THRESHOLD_SETTING.substring(0, 8))) return "-阈值返回";
        if (hex.startsWith(CMD.WRITE_SENSITIVITY_SETTING.substring(0, 8))) return "-灵敏度返回";
        if (hex.startsWith(CMD.WRITE_BOARD_RESET.substring(0, 8))) return "-主板复位返回";
        if (hex.startsWith(CMD.WRITE_SENSOR_RESET.substring(0, 8))) return "-传感器复位返回";
        if (hex.startsWith(CMD.WRITE_START_CHECK.substring(0, 8))) return "-自检返回";
        if (hex.startsWith(CMD.WRITE_SUFFIX)) return "-写入返回";
        return "";
    }
    private void appendLog(String msg) {
        appendLog(msg, null);
    }
    private void appendLog(String msg, Boolean isSent) {
        String time = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()).format(new Date());
        String displayMsg = msg;
        String cmdDesc = "";
        // å¦‚果是 hex æŒ‡ä»¤ (纯 0-9 A-F a-f),加空格格式化
        if (msg.matches("^[0-9A-Fa-f]+$")) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < msg.length(); i += 2) {
                if (i + 2 <= msg.length()) {
                    sb.append(msg.substring(i, i + 2)).append(" ");
                } else {
                    sb.append(msg.substring(i));
                }
            }
            displayMsg = sb.toString().trim();
            // èŽ·å–æŒ‡ä»¤æè¿°
            cmdDesc = getCmdDescription(msg, isSent);
        }
        String logLine;
        if (isSent != null) {
            String color = isSent ? "#1890ff" : "#05aa87"; // Blue for sent, Green for received
            // æ‹¼æŽ¥åˆ°å‰ç¼€åŽé¢: "发送-读站号: " æˆ– "收到-读站号: "
            String prefix = (isSent ? "发送" : "收到") + cmdDesc + ": ";
            logLine = time + " <font color='" + color + "'>" + prefix + displayMsg + "</font><br>";
        } else {
            // æ™®é€šæ—¥å¿—
            logLine = time + " " + displayMsg + "<br>";
        }
        logBuilder.append(logLine);
        // Update UI
        if (tv_log != null) {
            tv_log.setText(Html.fromHtml(logBuilder.toString()));
             if (svLog != null) {
                svLog.post(() -> svLog.fullScroll(View.FOCUS_DOWN));
            }
        }
    }
    // Kept for backward compatibility if needed, but redirected to appendLog
    private void addLog(String message) {
        appendLog(message);
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvent(UpdateEvent event) {
        if (event.getType() == UpdateEvent.Type.CONN_STATU) {
            updateBluetoothStatus();
        } else if (event.getType() == UpdateEvent.Type.DEVICE_INFO) {
            String hex = event.getMsg();
            appendLog(hex, false); // false for received (Green)
            parseAndRefresh(hex);
        }
    }
    private void parseAndRefresh(String hex) {
        if (hex == null) return;
        // å†™å…¥æŒ‡ä»¤çš„返回 (A55A06开头)
        if (hex.startsWith("A55A06") && hex.length() >= 12) {
             String statusHex = hex.substring(10, 12);
             boolean isSuccess = "01".equals(statusHex);
             if (hex.startsWith(CMD.ENTER_SETTING.substring(0, 8))) {
                 if (isSuccess) {
                     appendLog("进入设定模式成功");
                 } else {
                     appendLog("进入设定模式失败");
                 }
                 return;
             }
             if (hex.startsWith(CMD.EXIT_SETTING.substring(0, 8))) {
                 appendLog("退出设定模式成功");
                 return;
             }
             if (hex.startsWith(CMD.WRITE_THRESHOLD_SETTING.substring(0, 8))) {
                 if (isSuccess) {
                     appendLog("阈值写入成功");
                     Toast.makeText(this, "阈值写入成功", Toast.LENGTH_SHORT).show();
                 } else {
                     appendLog("阈值写入失败");
                     Toast.makeText(this, "阈值写入失败", Toast.LENGTH_SHORT).show();
                 }
             } else if (hex.startsWith(CMD.WRITE_SENSITIVITY_SETTING.substring(0, 8))) {
                 if (isSuccess) {
                     appendLog("灵敏度写入成功");
                     Toast.makeText(this, "灵敏度写入成功", Toast.LENGTH_SHORT).show();
                 } else {
                     appendLog("灵敏度写入失败");
                     Toast.makeText(this, "灵敏度写入失败", Toast.LENGTH_SHORT).show();
                 }
             } else if (hex.startsWith(CMD.WRITE_BOARD_RESET.substring(0, 8))) {
                 boolean isBoardReset = pendingSettingCmd.equals(CMD.WRITE_BOARD_RESET);
                 String name = isBoardReset ? "主板复位" : "传感器复位";
                 if (isSuccess) {
                     appendLog(name + "成功");
                     Toast.makeText(this, name + "成功", Toast.LENGTH_SHORT).show();
                 } else {
                     appendLog(name + "失败");
                     Toast.makeText(this, name + "失败", Toast.LENGTH_SHORT).show();
                 }
             }
        }
        // Check for Self Check command response
        if (hex.startsWith(CMD.WRITE_START_CHECK.substring(0, 8)) && hex.length() >= 12) {
             String statusHex = hex.substring(10, 12);
             boolean isSuccess = "01".equals(statusHex);
             String opName = isSelfCheckMode ? "开始自检" : "结束自检";
             if (isSuccess) {
                 appendLog(opName + "成功");
                 Toast.makeText(this, opName + "成功", Toast.LENGTH_SHORT).show();
                 // If success and isSelfCheckMode, wait 5s then read data
                 if (isSelfCheckMode) {
                     appendLog("5秒后读取自检结果...");
                     new android.os.Handler().postDelayed(() -> {
                         sendCmdWithCrc(CMD.READ_DATA);
                     }, 5000);
                 }
             } else {
                 appendLog(opName + "失败");
                 Toast.makeText(this, opName + "失败", Toast.LENGTH_SHORT).show();
             }
        }
        // Handle READ_DATA response for Self Check results
        // A55A0301...
        if (hex.startsWith(CMD.READ_DATA.substring(0, 8)) && hex.length() >= 26) {
             parseSelfCheckData(hex);
        }
    }
    private void parseSelfCheckData(String hex) {
        try {
             // 11th hex -> index 10-18 (8 chars)
             String part1Hex = hex.substring(10, 18);
             long part1Bits = Long.parseLong(part1Hex, 16);
             // 18th hex -> index 18-26 (8 chars)
             String part2Hex = hex.substring(18, 26);
             long part2Bits = Long.parseLong(part2Hex, 16);
             appendLog("自检数据解析: " + part1Hex + ", " + part2Hex);
             boolean hasChange = false;
             for (BoxStatus box : boxStatusList) {
                 int bitIndex = box.id - 1;
                 if (bitIndex >= 0 && bitIndex < 32) {
                     // Determine success based on both bits
                     // Assuming both need to be 1, or following specific logic.
                     // Based on user request "11th and 18th hex", we check both.
                     // Usually part1 is Glass (Detection), part2 is Online.
                     // We'll assume success means bit is set in both (Online and Detected?)
                     // OR maybe just Online?
                     // Let's stick to combining them to be safe as user mentioned both.
                     // But if Self Check is just checking if it's working, maybe Online is enough?
                     // Let's use: Success = (part1 & 1) && (part2 & 1) ?
                     // Let's use bitwise AND of the two parts for the result.
                     boolean bit1 = ((part1Bits >> bitIndex) & 1) == 1;
                     boolean bit2 = ((part2Bits >> bitIndex) & 1) == 1;
                     // If user meant "11th OR 18th", or "11th is this, 18th is that".
                     // Given "Success/Fail", I'll assume both must be valid.
                     // However, in normal operation:
                     // Glass=0, Online=1 -> Empty box, but sensor working.
                     // Glass=1, Online=1 -> Full box, sensor working.
                     // Online=0 -> Sensor broken/offline.
                     // If "Self Check" puts the sensor in a state where it should report "1" for Glass?
                     // If so, then we expect Glass=1 AND Online=1.
                     // If "Self Check" just checks health, then Glass might be 0.
                     // "确定哪些格子自检成功" (Determine which grids passed self-check).
                     // If I use bit1 && bit2:
                     // If a sensor is working but empty (Glass=0), it fails self-check?
                     // This implies Self Check expects a "1" signal.
                     // This is common in self-checks (force a signal).
                     if (bit1 && bit2) {
                         box.checkResult = 1; // Green
                     } else {
                         box.checkResult = 0; // Red
                     }
                     hasChange = true;
                 }
             }
             if (hasChange && mAdapter != null) {
                 mAdapter.notifyDataSetChanged();
                 appendLog("自检结果已更新");
             }
        } catch (Exception e) {
            e.printStackTrace();
            appendLog("自检数据解析异常: " + e.getMessage());
        }
    }
    private void sendCmdWithCrc(String cmd) {
        if (!BleGlobalManager.getInstance().isConnected()) {
            Toast.makeText(this, "请先连接蓝牙", Toast.LENGTH_SHORT).show();
            appendLog("错误: è“ç‰™æœªè¿žæŽ¥");
            return;
        }
        byte[] cmdBytes = BleGlobalManager.hexStringToBytes(cmd);
        if (cmdBytes != null) {
            String crc = CRCutil.getCRC(cmdBytes);
            // Pad CRC to 4 chars if needed
            while (crc.length() < 4) {
                crc = "0" + crc;
            }
            String fullCmd = cmd + crc.toUpperCase();
            appendLog(fullCmd, true); // true for sent (Blue)
            BleGlobalManager.getInstance().sendCmd(fullCmd);
        } else {
            appendLog("错误: æŒ‡ä»¤è½¬æ¢å¤±è´¥");
        }
    }
    private void handleThresholdWrite() {
        int position = sp_threshold_mode.getSelectedItemPosition();
        String inputValue = et_threshold_value.getText().toString().trim();
        String modeHex = "";
        String valueHex = "";
        if (position == 0) { // ä¸€é”®è®¾å®š
            modeHex = "00";
            valueHex = "00";
        } else if (position == 1) { // å•层设定
            modeHex = "01";
            if (inputValue.isEmpty()) {
                Toast.makeText(this, "请输入阈值(1-30)", Toast.LENGTH_SHORT).show();
                return;
            }
            try {
                int val = Integer.parseInt(inputValue);
                if (val < 1 || val > 30) {
                    Toast.makeText(this, "阈值范围1-30", Toast.LENGTH_SHORT).show();
                    return;
                }
                valueHex = String.format("%02X", val);
            } catch (NumberFormatException e) {
                Toast.makeText(this, "输入格式错误", Toast.LENGTH_SHORT).show();
                return;
            }
        } else {
            return;
        }
        // Construct pending command: CMD (32) + Len (02) + Mode + Value
        // CMD.WRITE_THRESHOLD_SETTING is "A55A063202"
        pendingSettingCmd = CMD.WRITE_THRESHOLD_SETTING + modeHex + valueHex;
        sendCmdWithCrc(pendingSettingCmd);
    }
    private void handleSensitivityWrite() {
        int position = sp_sensitivity_mode.getSelectedItemPosition();
        String inputValue = et_sensitivity_value.getText().toString().trim();
        String modeHex = "";
        String valueHex = "";
        // 0: æ‰¹é‡å¢žåŠ (00), 1: æ‰¹é‡å‡å°‘(01), 2: å•层增加(02), 3: å•层减少(03)
        if (position == 0) {
            modeHex = "00";
            valueHex = "00";
        } else if (position == 1) {
            modeHex = "01";
            valueHex = "00";
        } else if (position == 2) {
            modeHex = "02";
        } else if (position == 3) {
            modeHex = "03";
        } else {
            return;
        }
        if (position == 2 || position == 3) {
            if (inputValue.isEmpty()) {
                Toast.makeText(this, "请输入数值(1-30)", Toast.LENGTH_SHORT).show();
                return;
            }
            try {
                int val = Integer.parseInt(inputValue);
                if (val < 1 || val > 30) {
                    Toast.makeText(this, "数值范围1-30", Toast.LENGTH_SHORT).show();
                    return;
                }
                valueHex = String.format("%02X", val);
            } catch (NumberFormatException e) {
                Toast.makeText(this, "输入格式错误", Toast.LENGTH_SHORT).show();
                return;
            }
        }
        // Construct pending command: CMD (33) + Len (02) + Mode + Value
        // CMD.WRITE_SENSITIVITY_SETTING is "A55A063302"
        pendingSettingCmd = CMD.WRITE_SENSITIVITY_SETTING + modeHex + valueHex;
        sendCmdWithCrc(pendingSettingCmd);
    }
    private void updateBluetoothStatus() {
        if (tv_right_text == null) return;
        if (BleGlobalManager.getInstance().isConnected()) {
            String name = "未知设备";
            if (BleGlobalManager.getInstance().getBluetoothLeDevice() != null) {
                String deviceName = BleGlobalManager.getInstance().getBluetoothLeDevice().getName();
                if (deviceName != null && !deviceName.isEmpty()) {
                    name = deviceName;
                }
            }
            tv_right_text.setText(name);
        } else {
            tv_right_text.setText("未连接");
        }
    }
    private void showResetConfirmDialog(String message, final String cmd) {
        new AlertDialog.Builder(this)
                .setTitle("提示")
                .setMessage(message)
                .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                         pendingSettingCmd = cmd;
                         sendCmdWithCrc(cmd);
                    }
                })
                .setNegativeButton("取消", null)
                .show();
    }
    // Inner classes
    private static class BoxStatus {
        int id;
        // -1: Unknown/Initial (White), 0: Fail (Red), 1: Success (Green)
        int checkResult = -1;
        public BoxStatus(int id) {
            this.id = id;
            this.checkResult = -1;
        }
    }
    private class GridAdapter extends RecyclerView.Adapter<GridAdapter.ViewHolder> {
        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_grid_box, parent, false);
            return new ViewHolder(view);
        }
        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            // Calculate Box ID based on 3 rows of 10, Right to Left logic as seen in image
            // Row 1 (pos 0-9): 10 ... 1
            // Row 2 (pos 10-19): 20 ... 11
            // Row 3 (pos 20-29): 30 ... 21
            int row = position / 10;
            int col = position % 10;
            int boxId = (row + 1) * 10 - col;
            holder.tvBoxNumber.setText(String.valueOf(boxId));
            // Find status for this box
            BoxStatus status = null;
            for (BoxStatus s : boxStatusList) {
                if (s.id == boxId) {
                    status = s;
                    break;
                }
            }
            if (status != null) {
                if (status.checkResult == 1) {
                    // Success
                    holder.viewBox.setBackgroundResource(R.drawable.bg_box_full); // Green
                } else if (status.checkResult == 0) {
                    // Fail
                    holder.viewBox.setBackgroundResource(R.drawable.bg_box_error); // Red
                } else {
                    // Unknown / Initial
                    holder.viewBox.setBackgroundResource(R.drawable.bg_box_empty); // White
                }
            }
        }
        @Override
        public int getItemCount() {
            return 30;
        }
        class ViewHolder extends RecyclerView.ViewHolder {
            TextView tvBoxNumber;
            View viewBox;
            public ViewHolder(@NonNull View itemView) {
                super(itemView);
                tvBoxNumber = itemView.findViewById(R.id.tv_box_number);
                viewBox = itemView.findViewById(R.id.view_box);
            }
        }
    }
}
app/src/main/java/com/shlb/comb/activity/TestActivity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,357 @@
package com.shlb.comb.activity;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothProfile;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothConnectManager;
import com.blakequ.bluetooth_manager_lib.connect.ConnectState;
import com.blakequ.bluetooth_manager_lib.connect.ConnectStateListener;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import com.qmuiteam.qmui.util.QMUIDisplayHelper;
import com.qmuiteam.qmui.widget.grouplist.QMUICommonListItemView;
import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView;
import com.shlb.comb.R;
import com.shlb.comb.base.BaseActivity;
import com.shlb.comb.event.UpdateEvent;
import com.shlb.comb.util.Singletion;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class TestActivity extends BaseActivity {
    private TextView tvStatus;
    private TextView tvSelectedService;
    private QMUIGroupListView mGroupListView;
    private EditText etCommand;
    private Button btnSend;
    private TextView tvLog;
    private BluetoothConnectManager connectManager;
    private BluetoothLeDevice mDevice;
    private BluetoothGatt mGatt;
    private BluetoothGattCharacteristic selectedCharacteristic;
    // Store pairs of (Characteristic, UI Item) if needed, or just list
    private List<BluetoothGattCharacteristic> writeableCharacteristics = new ArrayList<>();
    @Override
    protected void contentView() {
        setContentView(R.layout.activity_test);
    }
    @Override
    protected void initView() {
        super.initHead();
        tv_center.setText("蓝牙测试");
        if (iv_left != null) {
            iv_left.setImageDrawable(getResources().getDrawable(R.mipmap.icon_back));
            iv_left.setOnClickListener(v -> finish());
        }
        tvStatus = findViewById(R.id.tv_status);
        tvSelectedService = findViewById(R.id.tv_selected_service);
        mGroupListView = findViewById(R.id.groupListView);
        etCommand = findViewById(R.id.et_command);
        btnSend = findViewById(R.id.btn_send);
        tvLog = findViewById(R.id.tv_log);
    }
    @Override
    protected void initData() {
        mDevice = Singletion.getInstance().mDevice;
        if (mDevice == null) {
            appendLog("未选择设备!");
            return;
        }
        initConn(mDevice);
    }
    @Override
    protected void initEvent() {
        btnSend.setOnClickListener(v -> {
            String cmd = etCommand.getText().toString().trim();
            if (TextUtils.isEmpty(cmd)) {
                Toast("请输入指令");
                return;
            }
            if (selectedCharacteristic == null) {
                Toast("请选择一个服务/特征值");
                return;
            }
            sendCmd(cmd);
        });
    }
    private void initConn(BluetoothLeDevice device) {
        appendLog("正在连接 " + device.getAddress() + "...");
        connectManager = BluetoothConnectManager.getInstance(this);
        connectManager.addConnectStateListener(stateListener);
        connectManager.setBluetoothGattCallback(new BluetoothGattCallback() {
            @Override
            public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
                super.onConnectionStateChange(gatt, status, newState);
                EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.CONN_STATU, newState, "conn_statu"));
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    mGatt = gatt;
                }
            }
            @Override
            public void onServicesDiscovered(BluetoothGatt gatt, int status) {
                super.onServicesDiscovered(gatt, status);
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    runOnUiThread(() -> processServices(gatt.getServices()));
                }
            }
            @Override
            public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
                super.onCharacteristicWrite(gatt, characteristic, status);
                String msg = (status == BluetoothGatt.GATT_SUCCESS) ? "写入成功" : "写入失败";
                String uuid = characteristic.getUuid().toString().substring(0, 8);
                runOnUiThread(() -> appendLog(msg + " (" + uuid + ")"));
            }
            @Override
            public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
                super.onCharacteristicChanged(gatt, characteristic);
                byte[] value = characteristic.getValue();
                String hex = bytesToHexString(value);
                String uuid = characteristic.getUuid().toString().substring(0, 8);
                runOnUiThread(() -> appendLog("收到(" + uuid + "): " + hex));
            }
        });
        connectManager.connect(device.getAddress());
    }
    private ConnectStateListener stateListener = new ConnectStateListener() {
        @Override
        public void onConnectStateChanged(String address, ConnectState state) {
            runOnUiThread(() -> {
                String stateStr = "";
                switch (state) {
                    case CONNECTED: stateStr = "已连接"; break;
                    case CONNECTING: stateStr = "连接中"; break;
                    case NORMAL: stateStr = "已断开"; break;
                    default: stateStr = state.toString();
                }
                tvStatus.setText("状态: " + stateStr);
                appendLog("状态变更: " + stateStr);
            });
        }
    };
    private void processServices(List<BluetoothGattService> services) {
        writeableCharacteristics.clear();
        int size = QMUIDisplayHelper.dp2px(this, 20);
        QMUIGroupListView.Section section = QMUIGroupListView.newSection(this)
                .setTitle("可写服务列表")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT);
        for (BluetoothGattService service : services) {
            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                int props = characteristic.getProperties();
                // Filter for Write properties
                if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0 ||
                    (props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0) {
                    writeableCharacteristics.add(characteristic);
                    String propertyStr = getPropertyString(props);
                    String uuidFull = characteristic.getUuid().toString();
                    QMUICommonListItemView item = mGroupListView.createItemView(null,
                            "UUID: " + uuidFull,
                            propertyStr,
                            QMUICommonListItemView.VERTICAL,
                            QMUICommonListItemView.ACCESSORY_TYPE_NONE);
                    item.setTag(characteristic);
                    section.addItemView(item, v -> {
                        if (v.getTag() instanceof BluetoothGattCharacteristic) {
                            handleCharacteristicSelection((BluetoothGattCharacteristic) v.getTag());
                        }
                    });
                }
            }
        }
        section.addTo(mGroupListView);
        if (!writeableCharacteristics.isEmpty()) {
            // Default select first
            handleCharacteristicSelection(writeableCharacteristics.get(0));
        } else {
            appendLog("未找到可写特征值.");
        }
    }
    private void handleCharacteristicSelection(BluetoothGattCharacteristic characteristic) {
        selectedCharacteristic = characteristic;
        tvSelectedService.setText("当前选择服务: " + selectedCharacteristic.getUuid().toString());
        appendLog("已选择特征值: " + selectedCharacteristic.getUuid());
        Toast("已选择该服务");
        // Attempt to find and enable notification on a characteristic in the same service
        BluetoothGattService service = characteristic.getService();
        boolean foundNotify = false;
        for (BluetoothGattCharacteristic c : service.getCharacteristics()) {
            int props = c.getProperties();
            if ((props & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0 ||
                    (props & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
                appendLog("尝试启用通知: " + c.getUuid());
                enableNotification(c);
                foundNotify = true;
                // Found one, stop searching to avoid concurrent descriptor writes
                break;
            }
        }
        if (!foundNotify) {
             appendLog("当前服务未找到可通知的特征值");
        }
    }
    private String getPropertyString(int property) {
        StringBuilder sb = new StringBuilder();
        if ((property & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
            sb.append("读 ");
        }
        if ((property & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0
                || (property & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
            sb.append("写 ");
        }
        if ((property & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
            sb.append("通知 ");
        }
        if ((property & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
            sb.append("指示 ");
        }
        return sb.toString();
    }
    private void enableNotification(BluetoothGattCharacteristic characteristic) {
        if (mGatt == null || characteristic == null) return;
        boolean success = mGatt.setCharacteristicNotification(characteristic, true);
        if (success) {
            appendLog("启用通知监听成功");
            // Write Descriptor for Notify/Indicate
            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
                    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
            if (descriptor != null) {
                int props = characteristic.getProperties();
                byte[] value = null;
                if ((props & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
                    value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
                } else if ((props & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
                    value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
                }
                if (value != null) {
                    descriptor.setValue(value);
                    boolean writeDesc = mGatt.writeDescriptor(descriptor);
                    appendLog("写入Descriptor: " + writeDesc);
                }
            }
        } else {
            appendLog("启用通知监听失败!");
        }
    }
    private void sendCmd(String hexCmd) {
        if (selectedCharacteristic == null || mGatt == null) return;
        byte[] data = hexStringToBytes(hexCmd);
        if (data == null) {
            Toast("Hex指令格式错误");
            return;
        }
        selectedCharacteristic.setValue(data);
        boolean status = mGatt.writeCharacteristic(selectedCharacteristic);
        appendLog("发送: " + hexCmd + ", ç»“æžœ: " + status);
    }
    private void appendLog(String msg) {
        tvLog.append(msg + "\n");
        ((View)tvLog.getParent()).post(() -> ((View)tvLog.getParent()).scrollTo(0, tvLog.getBottom()));
    }
    public static byte[] hexStringToBytes(String hexString) {
        if (hexString == null || hexString.equals("")) {
            return null;
        }
        hexString = hexString.toUpperCase();
        hexString = hexString.replace(" ", "");
        if (hexString.length() % 2 != 0) {
            hexString = "0" + hexString;
        }
        int length = hexString.length() / 2;
        char[] hexChars = hexString.toCharArray();
        byte[] d = new byte[length];
        for (int i = 0; i < length; i++) {
            int pos = i * 2;
            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
        }
        return d;
    }
    private static byte charToByte(char c) {
        return (byte) "0123456789ABCDEF".indexOf(c);
    }
    public static String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString().toUpperCase();
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventRefresh(UpdateEvent event) {
        // Handle global events if necessary
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (connectManager != null && mDevice != null) {
            connectManager.removeConnectStateListener(stateListener);
            connectManager.disconnect(mDevice.getAddress());
        }
    }
}
app/src/main/java/com/shlb/comb/activity/WriteActivity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,627 @@
package com.shlb.comb.activity;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.text.InputType;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothConnectManager;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothSubScribeData;
import com.blakequ.bluetooth_manager_lib.connect.ConnectState;
import com.blakequ.bluetooth_manager_lib.connect.ConnectStateListener;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.util.BluetoothUtils;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.qmuiteam.qmui.util.QMUIDisplayHelper;
import com.qmuiteam.qmui.widget.dialog.QMUIDialog;
import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction;
import com.qmuiteam.qmui.widget.grouplist.QMUICommonListItemView;
import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView;
import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton;
import com.shlb.comb.R;
import com.shlb.comb.base.BaseActivity;
import com.shlb.comb.event.UpdateEvent;
import com.shlb.comb.util.CMD;
import com.shlb.comb.util.CRCutil;
import com.shlb.comb.util.Singletion;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class WriteActivity extends BaseActivity {
    QMUIGroupListView mGroupListView;
    QMUIRoundButton send;
    LinearLayout llRoot;
    TextView sendNum;
    private BluetoothConnectManager connectManager;
    private BluetoothGatt gatt;
    private BluetoothGattCharacteristic characteristic;
    @Override
    protected void contentView() {
        setContentView(R.layout.activity_write);
    }
    @Override
    protected void initView() {
        super.initHead();
        mGroupListView = findViewById(R.id.groupListView);
        send = findViewById(R.id.send);
        llRoot = findViewById(R.id.ll_root);
        sendNum = findViewById(R.id.sendNum);
        initTitlebar();
        tv_center.setText("编址");
        initGroupListView();
    }
    @Override
    protected void initData() {
        String uuid = Singletion.getInstance().uuid;
        initWrite(uuid);
    }
    @Override
    protected void initEvent() {
        send.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.iv_left) {
            finish();
        } else if (id == R.id.btnExit) {
            writeCmd(CMD.EXIT);
        } else if (id == R.id.btnInto) {
            writeCmd(CMD.GETINTO);
        } else if (id == R.id.btnClean) {
            writeCmd(CMD.CLEAN);
        } else if (id == R.id.btn1) {
            sendYJCmd(1);
        } else if (id == R.id.btn2) {
            sendYJCmd(2);
        } else if (id == R.id.btn3) {
            sendYJCmd(3);
        } else if (id == R.id.btn4) {
            sendYJCmd(4);
        } else if (id == R.id.btn5) {
            sendYJCmd(5);
        } else if (id == R.id.btn6) {
            sendYJCmd(6);
        } else if (id == R.id.btn7) {
            sendYJCmd(7);
        } else if (id == R.id.btn8) {
            sendYJCmd(8);
        } else if (id == R.id.btn9) {
            sendYJCmd(9);
        } else if (id == R.id.btn10) {
            sendYJCmd(10);
        } else if (id == R.id.btn11) {
            sendYJCmd(11);
        } else if (id == R.id.btn12) {
            sendYJCmd(12);
        } else if (id == R.id.btn13) {
            sendYJCmd(13);
        } else if (id == R.id.btn14) {
            sendYJCmd(14);
        } else if (id == R.id.btn15) {
            sendYJCmd(15);
        } else if (id == R.id.btn16) {
            sendYJCmd(16);
        } else if (id == R.id.send) {
                switch (curCmd) {
                    case -1:
                        Toast("请选择指令!");
                        break;
                    case 1:
                        //进入编址
                        writeCmd(CMD.GETINTO);
                        break;
                    case 2:
                        //清除地址
                        writeCmd(CMD.CLEAN);
                        break;
                    case 3:
                        String addrNos = item3.getDetailText().toString();
                        Integer addrNo = -1;
                        try {
                            addrNo = Integer.parseInt(addrNos);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        if (addrNo == -1) {
                            Toast("地址数据异常");
                            return;
                        }
                        String sNo = Integer.toHexString(addrNo);
                        if (sNo.length() < 2) {
                            sNo = "0" + sNo;
                        }
                        String cmd = CMD.ADDR_PREFIX + sNo + CMD.ADDR_SUFFIX;
                        byte[] bytes = invertStringToBytes(cmd);
                        String crc = CRCutil.getCRC(bytes);
                        String zl = cmd + crc;
                        LogUtils.e("reszl", zl);
                        writeCmd(zl);
                        Toast("编址指令已发送!");
                        break;
                    case 4:
                        //退出编址
                        writeCmd(CMD.EXIT);
                        break;
                    case 5:
                        String addrNos2 = item5.getDetailText().toString();
                        Integer addrNo2 = -1;
                        try {
                            addrNo2 = Integer.parseInt(addrNos2);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        final Integer no2 = addrNo2;
                        if (no2 == -1) {
                            Toast("地址数据异常");
                            return;
                        }
                        ThreadUtils.executeBySingle(new ThreadUtils.SimpleTask<Object>() {
                            @Override
                            public Object doInBackground() throws Throwable {
                                writeCmd(CMD.GETINTO);
                                Thread.sleep(200);
                                writeCmd(CMD.CLEAN);
                                Thread.sleep(200);
                                String sNo2 = Integer.toHexString(no2);
                                //
                                if (sNo2.length() < 2) {
                                    sNo2 = "0" + sNo2;
                                }
                                String cmd = CMD.ADDR_PREFIX + sNo2 + CMD.ADDR_SUFFIX;
                                byte[] bytes = invertStringToBytes(cmd);
                                String crc = CRCutil.getCRC(bytes);
                                String zl = cmd + crc;
                                LogUtils.e("reszl", zl);
                                Thread.sleep(200);
                                writeCmd(zl);
                                return null;
                            }
                            @Override
                            public void onSuccess(Object result) {
                            }
                        });
                        Toast("编址指令已发送!");
                        break;
                }
        }
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventReceiveNotify(UpdateEvent event) {
        if (event != null && event.getType() == UpdateEvent.Type.BLE_DATA) {
            BluetoothGattCharacteristic characteristic = (BluetoothGattCharacteristic) event.getObj();
            final byte[] dataArr = characteristic.getValue();
            final String flag = event.getMsg();
            if (dataArr != null && dataArr.length > 0) {
                if (flag.equals("read")) {
                } else if (flag.equals("write")) {
                } else if (flag.equals("notify")) {
                    //返回结果
                    String crc = CRCutil.getCRC(dataArr);
                    LogUtils.e("resCRC", crc);
                    //CRC计算结果为0时命令执行成功,自动执行退出命令
                    if (crc.equals("0")) {
                        writeCmd(CMD.EXIT);
                        Toast("操作成功,自动执行退出编址指令");
                        curCmd = -1;
                        item6.setText("编址成功");
                    }
                    //计算返回为0时 è‡ªåŠ¨é€€å‡º
                    LogUtils.e("res", ByteUtils.byteArrayToHexString(dataArr));
                } else {
                    Toast("解析返回信息失败!");
                }
            } else {
                Toast("返回信息为空!");
            }
        }
    }
    QMUICommonListItemView item1;
    QMUICommonListItemView item2;
    QMUICommonListItemView item3;
    QMUICommonListItemView item4;
    QMUICommonListItemView item5;
    QMUICommonListItemView item6;
    View.OnClickListener onClickListener;
    int curCmd = -1;
    String[] cmdName = {"进入编址", "清除地址", "编辑地址", "退出编址", "一键编址"};
    private void initGroupListView() {
        item1 = mGroupListView.createItemView(null,
                cmdName[0], "",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item2 = mGroupListView.createItemView(null,
                cmdName[1], "",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item3 = mGroupListView.createItemView(null,
                cmdName[2], "点击选择编号",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item4 = mGroupListView.createItemView(null,
                cmdName[3], "",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        onClickListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (v instanceof QMUICommonListItemView) {
                    CharSequence text = ((QMUICommonListItemView) v).getText();
                    //进入编址
                    if (text.equals(cmdName[0])) {
                        curCmd = 1;
                        item6.setText(cmdName[0]);
                        item6.setDetailText("");
                        //清除地址
                    } else if (text.equals(cmdName[1])) {
                        curCmd = 2;
                        item6.setText(cmdName[1]);
                        item6.setDetailText("");
                        //编辑地址
                    } else if (text.equals(cmdName[2])) {
                        showEditTextDialog();
                        //退出编址
                    } else if (text.equals(cmdName[3])) {
                        curCmd = 4;
                        item6.setText(cmdName[3]);
                        item6.setDetailText("");
                        //一键编址
                    } else if (text.equals(cmdName[4])) {
                        showQuickDialog();
                    }
                }
            }
        };
        int size = QMUIDisplayHelper.dp2px(this, 20);
        QMUIGroupListView.newSection(this)
                .setTitle("Section 1:操作指令")
                .setDescription("")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                .addItemView(item1, onClickListener)
                .addItemView(item2, onClickListener)
                .addItemView(item3, onClickListener)
                .addItemView(item4, onClickListener)
                .addTo(mGroupListView);
        item5 = mGroupListView.createItemView(cmdName[4]);
        item5.setOrientation(QMUICommonListItemView.VERTICAL);
        item5.setDetailText("进入-清除-编址-退出");
        item5.setAccessoryType(QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item6 = mGroupListView.createItemView("待选择");
        item6.setOrientation(QMUICommonListItemView.HORIZONTAL);
        QMUIGroupListView.newSection(this)
                .setTitle("Section 2:快捷指令")
                .setDescription("")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                .addItemView(item5, onClickListener)
                .addTo(mGroupListView);
        QMUIGroupListView.newSection(this)
                .setTitle("Section 3:当前指令")
                .setDescription("")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                .addItemView(item6, onClickListener)
                .addTo(mGroupListView);
    }
    /**
     * åˆå§‹åŒ–写入服务
     *
     * @param uu
     */
    private void initWrite(String uu) {
        BluetoothLeDevice mDevice = Singletion.getInstance().mDevice;
        connectManager = BluetoothConnectManager.getInstance(this);
        connectManager.addConnectStateListener(listener);
        String uuid = uu;
        UUID serverUUid = null;
        gatt = connectManager.getBluetoothGatt(mDevice.getAddress());
        if (gatt != null) {
            List<BluetoothGattService> list = gatt.getServices();
            if (list != null) {
                for (BluetoothGattService service : list) {
                    for (BluetoothGattCharacteristic characteristics : service.getCharacteristics()) {
                        if (characteristics.getUuid().toString().equals(uuid)) {
                            characteristic = characteristics;
                            serverUUid = service.getUuid();
                            break;
                        }
                    }
                }
            }
        }
        if (serverUUid == null) {
            Toast("serverUUid不能为空!");
            return;
        }
        //1.set service uuid
        connectManager.setServiceUUID(serverUUid.toString());
        //2.clean history descriptor data
        connectManager.cleanSubscribeData();
        //3.add subscribe params
        if (BluetoothUtils.isCharacteristicRead(characteristic.getProperties())) {
            connectManager.addBluetoothSubscribeData(
                    new BluetoothSubScribeData.Builder().setCharacteristicRead(characteristic.getUuid()).build());
        }
        if (BluetoothUtils.isCharacteristicNotify(characteristic.getProperties())) {
            connectManager.addBluetoothSubscribeData(
                    new BluetoothSubScribeData.Builder().setCharacteristicNotify(characteristic.getUuid()).build()
            );
        }
        connectManager.startSubscribe(gatt);
    }
    private void writeCmd(String cmd) {
        int len = cmd.length();
        if (len > 0 && len % 2 == 0) {
            byte[] bytes = invertStringToBytes(cmd);
            if (bytes != null) {
                characteristic.setValue(bytes);
                gatt.writeCharacteristic(characteristic);
                runOnUiThread(() -> {
                    item6.setDetailText(item6.getDetailText() + "  å·²å‘送");
                });
            } else {
                Log.e("tag", "write value fail");
            }
        } else {
            runOnUiThread(() -> {
                Toast("指令错误!");
            });
        }
    }
    private byte[] invertStringToBytes(String value) {
        int len = value.length() / 2;
        if (len > 0) {
            byte[] bytes = new byte[len];
            for (int i = 0; i < len; i++) {
                Integer val = Integer.valueOf(value.substring(i * 2, i * 2 + 2), 16);
                bytes[i] = val.byteValue();
            }
            return bytes;
        }
        return null;
    }
    private List<Map<String, Object>> getWriteData() {
        List<Map<String, Object>> res = new ArrayList<>();
        Map<String, Object> map1 = new HashMap<>();
        map1.put("name", "进入编址状态");
        map1.put("flag", "0");
        map1.put("value", "A55A060100000000002F35");
        res.add(map1);
        Map<String, Object> map2 = new HashMap<>();
        map2.put("name", "退出编址状态");
        map2.put("flag", "0");
        map2.put("value", "A55A060400000000002F60");
        res.add(map2);
        Map<String, Object> map3 = new HashMap<>();
        map3.put("name", "编址");
        map3.put("flag", "0");
        map3.put("value", "A55A060300020000002F6F");
        res.add(map3);
        Map<String, Object> map4 = new HashMap<>();
        map4.put("name", "清除地址");
        map4.put("flag", "0");
        map4.put("value", "A55A060200000000002F06");
        res.add(map4);
        return res;
    }
    private ConnectStateListener listener = new ConnectStateListener() {
        @Override
        public void onConnectStateChanged(String address, ConnectState state) {
            switch (state) {
                case CONNECTED:
                    break;
                case CONNECTING:
                    break;
                case NORMAL:
                    break;
            }
        }
    };
    /**
     * è¾“入编址序号后 æ‰§è¡Œç¼–址指令
     */
    private void showEditTextDialog() {
        final QMUIDialog.EditTextDialogBuilder builder = new QMUIDialog.EditTextDialogBuilder(this);
        builder.setTitle("提示")
                .setPlaceholder("请输入编址号码(1-16)")
                .setInputType(InputType.TYPE_CLASS_TEXT)
                .addAction("取消", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        dialog.dismiss();
                    }
                })
                .addAction("确定", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        String text = builder.getEditText().getText().toString();
                        Integer res = -1;
                        try {
                            res = Integer.parseInt(text);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        if (res == null || res < 0 || res > 16) {
                            Toast("请输入1-16数字");
                        } else {
                            item3.setDetailText(text);
                            curCmd = 3;
                            item6.setText(cmdName[2]);
                            item6.setDetailText(text);
                            dialog.dismiss();
                        }
                    }
                })
                .create(com.qmuiteam.qmui.R.style.QMUI_Dialog).show();
    }
    /**
     * è¾“入编址序号后  ä¸€é”®æ‰§è¡Œæ‰€æœ‰æŒ‡ä»¤
     */
    private void showQuickDialog() {
        final QMUIDialog.EditTextDialogBuilder builder = new QMUIDialog.EditTextDialogBuilder(this);
        builder.setTitle("提示")
                .setPlaceholder("请输入编址号码(1-16)")
                .setInputType(InputType.TYPE_CLASS_TEXT)
                .addAction("取消", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        dialog.dismiss();
                    }
                })
                .addAction("确定", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        String text = builder.getEditText().getText().toString();
                        Integer addrrNo = -1;
                        try {
                            addrrNo = Integer.parseInt(text);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        if (addrrNo == null || addrrNo < 0 || addrrNo > 16) {
                            Toast("请输入1-16数字");
                        } else {
                            curCmd = 5;
                            item5.setDetailText(text);
                            item6.setText(cmdName[4]);
                            item6.setDetailText(text);
                            dialog.dismiss();
                        }
                    }
                })
                .create(com.qmuiteam.qmui.R.style.QMUI_Dialog).show();
    }
    public void initTitlebar() {
        iv_right.setVisibility(View.VISIBLE);
        iv_left.setImageDrawable(getResources().getDrawable(R.mipmap.icon_back));
        iv_right.setImageDrawable(getResources().getDrawable(R.mipmap.icon_topbar_overflow));
    }
    /**
     * å‘送一键命令
     * @param no2
     */
   private  void sendYJCmd(int no2){
       sendNum.setText(no2+"");
       if (no2 > 16|| no2<1 ) {
           Toast("地址数据异常");
           return;
       }
       ThreadUtils.executeBySingle(new ThreadUtils.SimpleTask<Object>() {
           @Override
           public Object doInBackground() throws Throwable {
               writeCmd(CMD.GETINTO);
               Thread.sleep(200);
               writeCmd(CMD.CLEAN);
               Thread.sleep(200);
               String sNo2 = Integer.toHexString(no2);
               //
               if (sNo2.length() < 2) {
                   sNo2 = "0" + sNo2;
               }
               String cmd = CMD.ADDR_PREFIX + sNo2 + CMD.ADDR_SUFFIX;
               byte[] bytes = invertStringToBytes(cmd);
               String crc = CRCutil.getCRC(bytes);
               String zl = cmd + crc;
               LogUtils.e("reszl", zl);
               Thread.sleep(200);
               writeCmd(zl);
               return null;
           }
           @Override
           public void onSuccess(Object result) {
           }
       });
       Toast("编址指令已发送!");
   }
}
app/src/main/java/com/shlb/comb/activity/WriteActivity2.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,531 @@
package com.shlb.comb.activity;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.text.InputType;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothConnectManager;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothSubScribeData;
import com.blakequ.bluetooth_manager_lib.connect.ConnectState;
import com.blakequ.bluetooth_manager_lib.connect.ConnectStateListener;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.util.BluetoothUtils;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.qmuiteam.qmui.util.QMUIDisplayHelper;
import com.qmuiteam.qmui.widget.dialog.QMUIDialog;
import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction;
import com.qmuiteam.qmui.widget.grouplist.QMUICommonListItemView;
import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView;
import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton;
import com.shlb.comb.R;
import com.shlb.comb.base.BaseActivity;
import com.shlb.comb.event.UpdateEvent;
import com.shlb.comb.util.CMD;
import com.shlb.comb.util.CRCutil;
import com.shlb.comb.util.Singletion;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class WriteActivity2 extends BaseActivity {
    QMUIGroupListView mGroupListView;
    QMUIRoundButton send;
    private BluetoothConnectManager connectManager;
    private BluetoothGatt gatt;
    private BluetoothGattCharacteristic characteristic;
    @Override
    protected void contentView() {
        setContentView(R.layout.activity_write);
    }
    @Override
    protected void initView() {
        super.initHead();
        mGroupListView = findViewById(R.id.groupListView);
        send = findViewById(R.id.send);
        initTitlebar();
        tv_center.setText("编址");
        initGroupListView();
    }
    @Override
    protected void initData() {
        String uuid = Singletion.getInstance().uuid;
        initWrite(uuid);
    }
    @Override
    protected void initEvent() {
        send.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.iv_left) {
            finish();
        } else if (id == R.id.iv_right) {
        } else if (id == R.id.send) {
                switch (curCmd){
                    case -1:
                        Toast("请选择指令!");
                        break;
                    case 1:
                        //进入编址
                        writeCmd(CMD.GETINTO);
                        break;
                    case 2:
                        //清除地址
                        writeCmd(CMD.CLEAN);
                        break;
                    case 3:
                        String addrNos = item3.getDetailText().toString();
                        Integer addrNo = -1;
                        try {
                            addrNo = Integer.parseInt(addrNos);
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                        if(addrNo == -1 ){
                            Toast("地址数据异常");
                            return;
                        }
                        String sNo = Integer.toHexString(addrNo);
                        if(sNo.length()<2){
                            sNo = "0" + sNo;
                        }
                        String cmd = CMD.ADDR_PREFIX + sNo + CMD.ADDR_SUFFIX;
                        byte[] bytes = invertStringToBytes(cmd);
                        String crc = CRCutil.getCRC(bytes);
                        String zl = cmd + crc;
                        LogUtils.e("reszl", zl);
                        writeCmd(zl);
                        Toast("编址指令已发送!");
                        break;
                    case 4:
                        //退出编址
                        writeCmd(CMD.EXIT);
                        break;
                    case 5:
                        String addrNos2 = item5.getDetailText().toString();
                        Integer addrNo2 = -1;
                        try {
                            addrNo2 = Integer.parseInt(addrNos2);
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                        final Integer no2 = addrNo2;
                        if(no2 == -1 ){
                            Toast("地址数据异常");
                            return;
                        }
                        ThreadUtils.executeBySingle(new ThreadUtils.SimpleTask<Object>() {
                            @Override
                            public Object doInBackground() throws Throwable {
                                writeCmd(CMD.GETINTO);
                                Thread.sleep(200);
                                writeCmd(CMD.CLEAN);
                                Thread.sleep(200);
                                String sNo2 = Integer.toHexString(no2);
                                //
                                if(sNo2.length()<2){
                                    sNo2 = "0" + sNo2;
                                }
                                String cmd = CMD.ADDR_PREFIX + sNo2 + CMD.ADDR_SUFFIX;
                                byte[] bytes = invertStringToBytes(cmd);
                                String crc = CRCutil.getCRC(bytes);
                                String zl = cmd + crc;
                                LogUtils.e("reszl", zl);
                                Thread.sleep(200);
                                writeCmd(zl);
                                return null;
                            }
                            @Override
                            public void onSuccess(Object result) {
                            }
                        });
                        Toast("编址指令已发送!");
                        break;
                }
        }
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventReceiveNotify(UpdateEvent event) {
        if (event != null && event.getType() == UpdateEvent.Type.BLE_DATA) {
            BluetoothGattCharacteristic characteristic = (BluetoothGattCharacteristic) event.getObj();
            final byte[] dataArr = characteristic.getValue();
            final String flag = event.getMsg();
            if (dataArr != null && dataArr.length > 0) {
                if (flag.equals("read")) {
                } else if (flag.equals("write")) {
                } else if (flag.equals("notify")) {
                    //返回结果
                    String crc = CRCutil.getCRC(dataArr);
                    LogUtils.e("resCRC", crc);
                    //CRC计算结果为0时命令执行成功,自动执行退出命令
                    if (crc.equals("0")) {
                        writeCmd(CMD.EXIT);
                        Toast("操作成功,自动执行退出编址指令");
                        curCmd  = -1;
                        item6.setText("编址成功");
                    }
                    //计算返回为0时 è‡ªåŠ¨é€€å‡º
                    LogUtils.e("res", ByteUtils.byteArrayToHexString(dataArr));
                } else {
                    Toast("解析返回信息失败!");
                }
            } else {
                Toast("返回信息为空!");
            }
        }
    }
    QMUICommonListItemView item1;
    QMUICommonListItemView item2;
    QMUICommonListItemView item3;
    QMUICommonListItemView item4;
    QMUICommonListItemView item5;
    QMUICommonListItemView item6;
    View.OnClickListener onClickListener;
    int  curCmd = -1;
    String[] cmdName = {"进入编址", "清除地址", "编辑地址", "退出编址", "一键编址"};
    private void initGroupListView() {
        item1 = mGroupListView.createItemView(null,
                cmdName[0], "",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item2 = mGroupListView.createItemView(null,
                cmdName[1], "",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item3 = mGroupListView.createItemView(null,
                cmdName[2], "点击选择编号",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item4 = mGroupListView.createItemView(null,
                cmdName[3], "",
                QMUICommonListItemView.HORIZONTAL,
                QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        onClickListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (v instanceof QMUICommonListItemView) {
                    CharSequence text = ((QMUICommonListItemView) v).getText();
                    //进入编址
                    if (text.equals(cmdName[0])) {
                      curCmd = 1;
                      item6.setText(cmdName[0]);
                        item6.setDetailText("");
                        //清除地址
                    } else if (text.equals(cmdName[1])) {
                        curCmd = 2;
                        item6.setText(cmdName[1]);
                        item6.setDetailText("");
                        //编辑地址
                    } else if (text.equals(cmdName[2])) {
                        showEditTextDialog();
                        //退出编址
                    } else if (text.equals(cmdName[3])) {
                       curCmd = 4;
                        item6.setText(cmdName[3]);
                        item6.setDetailText("");
                        //一键编址
                    }else if (text.equals(cmdName[4])) {
                        showQuickDialog();
                    }
                }
            }
        };
        int size = QMUIDisplayHelper.dp2px(this, 20);
        QMUIGroupListView.newSection(this)
                .setTitle("Section 1:操作指令")
                .setDescription("")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                .addItemView(item1, onClickListener)
                .addItemView(item2, onClickListener)
                .addItemView(item3, onClickListener)
                .addItemView(item4, onClickListener)
                .addTo(mGroupListView);
        item5  = mGroupListView.createItemView(cmdName[4]);
        item5.setOrientation(QMUICommonListItemView.VERTICAL);
        item5.setDetailText("进入-清除-编址-退出");
        item5.setAccessoryType(QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON);
        item6  = mGroupListView.createItemView("待选择");
        item6.setOrientation(QMUICommonListItemView.HORIZONTAL);
        QMUIGroupListView.newSection(this)
                .setTitle("Section 2:快捷指令")
                .setDescription("")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                .addItemView(item5, onClickListener)
                .addTo(mGroupListView);
        QMUIGroupListView.newSection(this)
                .setTitle("Section 3:当前指令")
                .setDescription("")
                .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT)
                .addItemView(item6, onClickListener)
                .addTo(mGroupListView);
    }
    /**
     * åˆå§‹åŒ–写入服务
     *
     * @param uu
     */
    private void initWrite(String uu) {
        BluetoothLeDevice mDevice = Singletion.getInstance().mDevice;
        connectManager = BluetoothConnectManager.getInstance(this);
        connectManager.addConnectStateListener(listener);
        String uuid = uu;
        UUID serverUUid = null;
        gatt = connectManager.getBluetoothGatt(mDevice.getAddress());
        if (gatt != null) {
            List<BluetoothGattService> list = gatt.getServices();
            if (list != null) {
                for (BluetoothGattService service : list) {
                    for (BluetoothGattCharacteristic characteristics : service.getCharacteristics()) {
                        if (characteristics.getUuid().toString().equals(uuid)) {
                            characteristic = characteristics;
                            serverUUid = service.getUuid();
                            break;
                        }
                    }
                }
            }
        }
        if (serverUUid == null) {
            Toast("serverUUid不能为空!");
            return;
        }
        //1.set service uuid
        connectManager.setServiceUUID(serverUUid.toString());
        //2.clean history descriptor data
        connectManager.cleanSubscribeData();
        //3.add subscribe params
        if (BluetoothUtils.isCharacteristicRead(characteristic.getProperties())) {
            connectManager.addBluetoothSubscribeData(
                    new BluetoothSubScribeData.Builder().setCharacteristicRead(characteristic.getUuid()).build());
        }
        if (BluetoothUtils.isCharacteristicNotify(characteristic.getProperties())) {
            connectManager.addBluetoothSubscribeData(
                    new BluetoothSubScribeData.Builder().setCharacteristicNotify(characteristic.getUuid()).build()
            );
        }
        connectManager.startSubscribe(gatt);
    }
    private void writeCmd(String cmd) {
        int len = cmd.length();
        if (len > 0 && len % 2 == 0) {
            byte[] bytes = invertStringToBytes(cmd);
            if (bytes != null) {
                characteristic.setValue(bytes);
                gatt.writeCharacteristic(characteristic);
                runOnUiThread(()->{
                    item6.setDetailText(item6.getDetailText()+"  å·²å‘送");
                });
            } else {
                Log.e("tag", "write value fail");
            }
        } else {
            runOnUiThread(()->{
                Toast("指令错误!");
            });
        }
    }
    private byte[] invertStringToBytes(String value) {
        int len = value.length() / 2;
        if (len > 0) {
            byte[] bytes = new byte[len];
            for (int i = 0; i < len; i++) {
                Integer val = Integer.valueOf(value.substring(i * 2, i * 2 + 2), 16);
                bytes[i] = val.byteValue();
            }
            return bytes;
        }
        return null;
    }
    private List<Map<String, Object>> getWriteData() {
        List<Map<String, Object>> res = new ArrayList<>();
        Map<String, Object> map1 = new HashMap<>();
        map1.put("name", "进入编址状态");
        map1.put("flag", "0");
        map1.put("value", "A55A060100000000002F35");
        res.add(map1);
        Map<String, Object> map2 = new HashMap<>();
        map2.put("name", "退出编址状态");
        map2.put("flag", "0");
        map2.put("value", "A55A060400000000002F60");
        res.add(map2);
        Map<String, Object> map3 = new HashMap<>();
        map3.put("name", "编址");
        map3.put("flag", "0");
        map3.put("value", "A55A060300020000002F6F");
        res.add(map3);
        Map<String, Object> map4 = new HashMap<>();
        map4.put("name", "清除地址");
        map4.put("flag", "0");
        map4.put("value", "A55A060200000000002F06");
        res.add(map4);
        return res;
    }
    private ConnectStateListener listener = new ConnectStateListener() {
        @Override
        public void onConnectStateChanged(String address, ConnectState state) {
            switch (state) {
                case CONNECTED:
                    break;
                case CONNECTING:
                    break;
                case NORMAL:
                    break;
            }
        }
    };
    /**
     * è¾“入编址序号后 æ‰§è¡Œç¼–址指令
     */
    private void showEditTextDialog() {
        final QMUIDialog.EditTextDialogBuilder builder = new QMUIDialog.EditTextDialogBuilder(this);
        builder.setTitle("提示")
                .setPlaceholder("请输入编址号码(1-16)")
                .setInputType(InputType.TYPE_CLASS_TEXT)
                .addAction("取消", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        dialog.dismiss();
                    }
                })
                .addAction("确定", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        String text = builder.getEditText().getText().toString();
                        Integer res = -1;
                        try {
                            res = Integer.parseInt(text);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        if (res == null || res < 0 || res > 16) {
                            Toast("请输入1-16数字");
                        } else {
                            item3.setDetailText(text);
                            curCmd = 3;
                            item6.setText(cmdName[2]);
                            item6.setDetailText(text);
                            dialog.dismiss();
                        }
                    }
                })
                .create(com.qmuiteam.qmui.R.style.QMUI_Dialog).show();
    }
    /**
     * è¾“入编址序号后  ä¸€é”®æ‰§è¡Œæ‰€æœ‰æŒ‡ä»¤
     */
    private void showQuickDialog() {
        final QMUIDialog.EditTextDialogBuilder builder = new QMUIDialog.EditTextDialogBuilder(this);
        builder.setTitle("提示")
                .setPlaceholder("请输入编址号码(1-16)")
                .setInputType(InputType.TYPE_CLASS_TEXT)
                .addAction("取消", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        dialog.dismiss();
                    }
                })
                .addAction("确定", new QMUIDialogAction.ActionListener() {
                    @Override
                    public void onClick(QMUIDialog dialog, int index) {
                        String text = builder.getEditText().getText().toString();
                         Integer addrrNo = -1;
                        try {
                            addrrNo = Integer.parseInt(text);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        if (addrrNo == null || addrrNo < 0 || addrrNo > 16) {
                            Toast("请输入1-16数字");
                        } else {
                            curCmd = 5;
                            item5.setDetailText(text);
                            item6.setText(cmdName[4]);
                            item6.setDetailText(text);
                            dialog.dismiss();
                        }
                    }
                })
                .create(com.qmuiteam.qmui.R.style.QMUI_Dialog).show();
    }
    public void initTitlebar() {
        iv_right.setVisibility(View.VISIBLE);
        iv_left.setImageDrawable(getResources().getDrawable(R.mipmap.icon_back));
        iv_right.setImageDrawable(getResources().getDrawable(R.mipmap.icon_topbar_overflow));
    }
}
app/src/main/java/com/shlb/comb/adapter/BaseArrayListAdapter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,246 @@
/*
 * FileName: BaseArrayListAdapter.java
 * Copyright (C) 2014 Plusub Tech. Co. Ltd. All Rights Reserved <admin@plusub.com>
 *
 * Licensed under the Plusub License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * author  : quhao <blakequ@gmail.com>
 * date     : 2014-6-7 ä¸‹åˆ9:50:52
 * last modify author :
 * version : 1.0
 */
package com.shlb.comb.adapter;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.BaseAdapter;
import java.util.ArrayList;
import java.util.List;
/**
 * æ•°ç»„适配器,只需要实现 {@link #getView(int, View, ViewGroup)}
 * @author blakequ Blakequ@gmail.com
 *
 * @param <T>
 */
public abstract class BaseArrayListAdapter<T> extends BaseAdapter {
    protected Context mContext;
    protected LayoutInflater mInflater;
    private List<T> mDatas;
    /**屏幕的宽度*/
    protected int mScreenWidth;
    /**屏幕高度*/
    protected int mScreenHeight;
    /**屏幕密度*/
    protected float mDensity;
    public BaseArrayListAdapter(Context context){
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mDatas = new ArrayList<T>();
        DisplayMetrics metric = new DisplayMetrics();
        WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        wm.getDefaultDisplay().getMetrics(metric);
        mScreenWidth = metric.widthPixels;
        mScreenHeight = metric.heightPixels;
        mDensity = metric.density;
    }
    public BaseArrayListAdapter(Context context, List<T> datas){
        mContext = context;
        mInflater = LayoutInflater.from(context);
        if (datas != null && datas.size() > 0) {
            mDatas = datas;
        }else{
            mDatas = new ArrayList<T>();
        }
    }
    /**
     * get context
     * <p>Title: getContext
     * <p>Description:
     * @return
     */
    public Context getContext() {
        return mContext;
    }
    public LayoutInflater getInflater() {
        return mInflater;
    }
    /**
     * get screen width
     * <p>Title: getScreenWidth
     * <p>Description:
     * @return
     */
    public int getScreenWidth() {
        return mScreenWidth;
    }
    /**
     * get screen height
     * <p>Title: getScreenHeight
     * <p>Description:
     * @return
     */
    public int getScreenHeight() {
        return mScreenHeight;
    }
    /**
     * return all data
     * <p>Title: getAllData
     * <p>Description:
     * @return
     */
    public List<T> getAllData(){
        return mDatas;
    }
    /**
     * add data to head
     * <p>Title: addHead
     * <p>Description:
     * @param datas
     */
    public void addHead(T datas){
        if (datas != null) {
            mDatas.add(0, datas);
            notifyDataSetChanged();
        }
    }
    /**
     * add data list to head
     * <p>Title: addHead
     * <p>Description:
     * @param datas
     */
    public void addHead(List<T> datas){
        if (datas != null) {
            for (int i = 0; i < datas.size(); i++) {
                mDatas.add(i, datas.get(i));
            }
            notifyDataSetChanged();
        }
    }
    public void add(T datas){
        if (datas != null) {
            mDatas.add(datas);
            notifyDataSetChanged();
        }
    }
    public void add(int position, T datas){
        if (position < 0 || position >= mDatas.size() || datas == null) {
            return;
        }
        mDatas.add(position, datas);
        notifyDataSetChanged();
    }
    /**
     * æ›´æ–°æŒ‡å®šä½ç½®æ•°æ®
     * <p>Title: update
     * <p>Description:
     * @param position
     * @param datas
     */
    public void update(int position, T datas){
        if (position < 0 || position >= mDatas.size() || datas == null) {
            return;
        }
        mDatas.remove(position);
        mDatas.add(position, datas);
        notifyDataSetChanged();
    }
    /**
     * add data to adapter
     * <p>Title: addAll
     * <p>Description:
     * @param datas
     */
    public void addAll(List<T> datas){
        if (datas != null) {
            mDatas.addAll(datas);
            notifyDataSetChanged();
        }
    }
    /**
     * refresh data to adapter, history data will be remove
     * <p>Title: refreshData
     * <p>Description:
     * @param datas
     */
    public void refreshData(List<T> datas){
        if (datas != null) {
            mDatas.clear();
            mDatas.addAll(datas);
            notifyDataSetChanged();
        }
    }
    /**
     * clear all data
     * <p>Title: clear
     * <p>Description:
     */
    public void clear(){
        mDatas.clear();
        notifyDataSetChanged();
    }
    /**
     * åˆ é™¤æŒ‡å®šä½ç½®æ•°æ®
     * <p>Title: delete
     * <p>Description:
     * @param position
     */
    public void delete(int position){
        if (position < 0 || position >= mDatas.size()) {
            return;
        }
        mDatas.remove(position);
        notifyDataSetChanged();
    }
    @Override
    public int getCount() {
        // TODO Auto-generated method stub
        return mDatas.size();
    }
    @Override
    public Object getItem(int position) {
        // TODO Auto-generated method stub
        if (mDatas == null || mDatas.size() <= 0 || position < 0 || position >= mDatas.size()) {
            return null;
        }
        return mDatas.get(position);
    }
    @Override
    public long getItemId(int position) {
        // TODO Auto-generated method stub
        return position;
    }
}
app/src/main/java/com/shlb/comb/adapter/DeviceListAdapter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
package com.shlb.comb.adapter;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.blakequ.bluetooth_manager_lib.device.BeaconType;
import com.blakequ.bluetooth_manager_lib.device.BeaconUtils;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.device.adrecord.AdRecord;
import com.blakequ.bluetooth_manager_lib.device.ibeacon.IBeaconDevice;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import com.shlb.comb.R;
import com.shlb.comb.util.Constants;
public class DeviceListAdapter extends BaseArrayListAdapter<BluetoothLeDevice>{
    private int scrodKey;
    public DeviceListAdapter(Context context) {
        super(context);
    }
    public void updateConfig(int scrodKey){
        this.scrodKey = scrodKey;
    }
    @Override
    public View getView(int position, View view, ViewGroup parent) {
        final ViewHolder viewHolder;
        // General ListView optimization code.
        if (view == null) {
            view = mInflater.inflate(R.layout.dialog_layout_bluetooth_item, null);
            viewHolder = new ViewHolder();
            viewHolder.deviceAddress = (TextView) view.findViewById(R.id.device_address);
            viewHolder.deviceName = (TextView) view.findViewById(R.id.device_name);
            viewHolder.deviceIcon = (ImageView) view.findViewById(R.id.device_icon);
            view.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) view.getTag();
        }
        final BluetoothLeDevice device = (BluetoothLeDevice) getItem(position);
        final String deviceName = device.getName();
        final double rssi = device.getRssi();
        if (deviceName != null && deviceName.length() > 0) {
            viewHolder.deviceName.setText(deviceName);
        } else {
            viewHolder.deviceName.setText(R.string.unknown_device);
        }
        return view;
    }
    public double calculateAccuracy(final int txPower, final double rssi) {
        if (rssi == 0) {
            return 0; // if we cannot determine accuracy, return -1.
        }
        final double ratio = rssi * 1.0 / txPower;
        if (ratio < 1.0) {
            return Math.pow(ratio, 10);
        } else {
            return (0.89976) * Math.pow(ratio, 7.7095) + 0.111;
        }
    }
    static class ViewHolder {
        TextView deviceName;
        TextView deviceAddress;
        ImageView deviceIcon;
    }
}
app/src/main/java/com/shlb/comb/adapter/DeviceListAdapter2.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,137 @@
package com.shlb.comb.adapter;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.blakequ.bluetooth_manager_lib.device.BeaconType;
import com.blakequ.bluetooth_manager_lib.device.BeaconUtils;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.device.adrecord.AdRecord;
import com.blakequ.bluetooth_manager_lib.device.ibeacon.IBeaconDevice;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import com.shlb.comb.R;
import com.shlb.comb.util.Constants;
public class DeviceListAdapter2 extends BaseArrayListAdapter<BluetoothLeDevice>{
    private int scrodKey;
    public DeviceListAdapter2(Context context) {
        super(context);
    }
    public void updateConfig(int scrodKey){
        this.scrodKey = scrodKey;
    }
    @Override
    public View getView(int position, View view, ViewGroup parent) {
        final ViewHolder viewHolder;
        // General ListView optimization code.
        if (view == null) {
            view = mInflater.inflate(R.layout.list_item_device, null);
            viewHolder = new ViewHolder();
            viewHolder.deviceAddress = (TextView) view.findViewById(R.id.device_address);
            viewHolder.deviceName = (TextView) view.findViewById(R.id.device_name);
            viewHolder.deviceRssi = (TextView) view.findViewById(R.id.device_rssi);
            viewHolder.deviceIcon = (ImageView) view.findViewById(R.id.device_icon);
            viewHolder.deviceLastUpdated = (TextView) view.findViewById(R.id.device_last_update);
            viewHolder.ibeaconMajor = (TextView) view.findViewById(R.id.ibeacon_major);
            viewHolder.ibeaconMinor = (TextView) view.findViewById(R.id.ibeacon_minor);
            viewHolder.ibeaconDistance = (TextView) view.findViewById(R.id.ibeacon_distance);
            viewHolder.ibeaconUUID = (TextView) view.findViewById(R.id.ibeacon_uuid);
            viewHolder.ibeaconTxPower = (TextView) view.findViewById(R.id.ibeacon_tx_power);
            viewHolder.ibeaconSection = view.findViewById(R.id.ibeacon_section);
            viewHolder.ibeaconDistanceDescriptor = (TextView) view.findViewById(R.id.ibeacon_distance_descriptor);
            viewHolder.deviceRecord = (TextView) view.findViewById(R.id.ibeacon_scan_record);
            view.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) view.getTag();
        }
        final BluetoothLeDevice device = (BluetoothLeDevice) getItem(position);
        final String deviceName = device.getName();
        final double rssi = device.getRssi();
        if (deviceName != null && deviceName.length() > 0) {
            viewHolder.deviceName.setText(deviceName);
        } else {
            viewHolder.deviceName.setText(R.string.unknown_device);
        }
        if (BeaconUtils.getBeaconType(device) == BeaconType.IBEACON) {
            final IBeaconDevice iBeacon = new IBeaconDevice(device);
            final String accuracy = Constants.DOUBLE_TWO_DIGIT_ACCURACY.format(iBeacon.getAccuracy());
            viewHolder.deviceIcon.setImageResource(R.mipmap.ic_device_ibeacon);
            viewHolder.ibeaconSection.setVisibility(View.VISIBLE);
            viewHolder.ibeaconMajor.setText(String.valueOf(iBeacon.getMajor()));
            viewHolder.ibeaconMinor.setText(String.valueOf(iBeacon.getMinor()));
            viewHolder.ibeaconTxPower.setText(String.valueOf(iBeacon.getCalibratedTxPower()));
            viewHolder.ibeaconUUID.setText(iBeacon.getUUID());
            viewHolder.ibeaconDistance.setText(
                    mContext.getString(R.string.formatter_meters, accuracy));
            viewHolder.ibeaconDistanceDescriptor.setText(iBeacon.getDistanceDescriptor().toString());
        } else {
            viewHolder.deviceIcon.setImageResource(R.mipmap.ic_bluetooth);
            viewHolder.ibeaconSection.setVisibility(View.GONE);
        }
        final String rssiString =
                mContext.getString(R.string.formatter_db, String.valueOf(rssi));
        final String runningAverageRssiString =
                mContext.getString(R.string.formatter_db, String.valueOf(device.getRunningAverageRssi()));
        viewHolder.deviceLastUpdated.setText(
                android.text.format.DateFormat.format(
                        Constants.TIME_FORMAT, new java.util.Date(device.getTimestamp())));
        viewHolder.deviceAddress.setText(device.getAddress());
        viewHolder.deviceRssi.setText(rssiString + " / " + runningAverageRssiString);
        //add for test
        if (scrodKey != -1){
            AdRecord record = device.getAdRecordStore().getRecord(scrodKey);
            if (record != null) {
                viewHolder.deviceRecord.setVisibility(View.VISIBLE);
                viewHolder.deviceRecord.setText(record.getHumanReadableType()+":"+ByteUtils.byteArrayToHexString(record.getData()));
            }else {
                viewHolder.deviceRecord.setVisibility(View.GONE);
            }
        }else {
            viewHolder.deviceRecord.setVisibility(View.GONE);
        }
        return view;
    }
    public double calculateAccuracy(final int txPower, final double rssi) {
        if (rssi == 0) {
            return 0; // if we cannot determine accuracy, return -1.
        }
        final double ratio = rssi * 1.0 / txPower;
        if (ratio < 1.0) {
            return Math.pow(ratio, 10);
        } else {
            return (0.89976) * Math.pow(ratio, 7.7095) + 0.111;
        }
    }
    static class ViewHolder {
        TextView deviceName;
        TextView deviceAddress;
        TextView deviceRssi;
        TextView ibeaconUUID;
        TextView ibeaconMajor;
        TextView ibeaconMinor;
        TextView ibeaconTxPower;
        TextView ibeaconDistance;
        TextView ibeaconDistanceDescriptor;
        TextView deviceLastUpdated;
        View ibeaconSection;
        ImageView deviceIcon;
        TextView deviceRecord;
    }
}
app/src/main/java/com/shlb/comb/adapter/HomeAdapter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.shlb.comb.adapter;
import androidx.annotation.NonNull;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.viewholder.BaseViewHolder;
import com.shlb.comb.R;
import com.shlb.comb.model.HomeItem;
import java.util.List;
public class HomeAdapter extends BaseQuickAdapter<HomeItem, BaseViewHolder> {
    public HomeAdapter(int layoutResId, List data) {
        super(layoutResId, data);
    }
    @Override
    protected void convert(@NonNull BaseViewHolder helper, HomeItem item) {
        helper.setText(R.id.tweetName, item.getTitle());
        //helper.setImageResource(R.id.icon, item.getImageResource());
    }
}
app/src/main/java/com/shlb/comb/adapter/HomeDeviceListAdapter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,240 @@
package com.shlb.comb.adapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanRecordCompat;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import com.shlb.comb.R;
import java.util.List;
import java.util.Map;
public class HomeDeviceListAdapter extends BaseArrayListAdapter<BluetoothLeDevice> {
    private setOnItemButtonClick onItemButtonClick;
    private int scrodKey;
    private int expandedPosition = -1;
    public HomeDeviceListAdapter(Context context, setOnItemButtonClick onItemButtonClick) {
        super(context);
        this.onItemButtonClick = onItemButtonClick;
    }
    public void updateConfig(int scrodKey) {
        this.scrodKey = scrodKey;
    }
    @Override
    public View getView(int position, View view, ViewGroup parent) {
        final ViewHolder viewHolder;
        // General ListView optimization code.
        if (view == null) {
            view = mInflater.inflate(R.layout.home_list_bluetooth_item, null);
            viewHolder = new ViewHolder();
            viewHolder.deviceAddress = (TextView) view.findViewById(R.id.device_address);
            viewHolder.deviceName = (TextView) view.findViewById(R.id.device_name);
            viewHolder.rssi = (TextView) view.findViewById(R.id.rssi);
            viewHolder.deviceIcon = (ImageView) view.findViewById(R.id.device_icon);
            viewHolder.ivDown = (ImageView) view.findViewById(R.id.iv_down);
            viewHolder.llMainContent = (LinearLayout) view.findViewById(R.id.ll_main_content);
            viewHolder.llMoreInfo = (LinearLayout) view.findViewById(R.id.ll_more_info);
            viewHolder.tvDeviceType = (TextView) view.findViewById(R.id.tv_device_type);
            viewHolder.tvAdvType = (TextView) view.findViewById(R.id.tv_adv_type);
            viewHolder.tvFlags = (TextView) view.findViewById(R.id.tv_flags);
            viewHolder.tvLocalName = (TextView) view.findViewById(R.id.tv_local_name);
            viewHolder.tvTxPower = (TextView) view.findViewById(R.id.tv_tx_power);
            viewHolder.tvServiceUuids = (TextView) view.findViewById(R.id.tv_service_uuids);
            viewHolder.tvServiceData = (TextView) view.findViewById(R.id.tv_service_data);
            viewHolder.tvManufacturerData = (TextView) view.findViewById(R.id.tv_manufacturer_data);
            view.findViewById(R.id.conn_btn).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    onItemButtonClick.buttonClick(view, position);
                }
            });
            view.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) view.getTag();
        }
        final BluetoothLeDevice device = (BluetoothLeDevice) getItem(position);
        final String deviceName = device.getName();
        final String deviceAddress = device.getAddress();
        final double rssi = device.getRssi();
        if (deviceName != null && deviceName.length() > 0) {
            viewHolder.deviceName.setText(deviceName);
        } else {
            viewHolder.deviceName.setText(R.string.unknown_device);
        }
        if (deviceAddress != null && deviceAddress.length() > 0) {
            viewHolder.deviceAddress.setText(deviceAddress);
        }
        if (Math.abs(rssi) > 0) {
            viewHolder.rssi.setText("-" + rssi + "dBm");
        }
        if (expandedPosition == position) {
            viewHolder.llMoreInfo.setVisibility(View.VISIBLE);
            viewHolder.ivDown.setRotation(180);
            populateMoreInfo(viewHolder, device);
        } else {
            viewHolder.llMoreInfo.setVisibility(View.GONE);
            viewHolder.ivDown.setRotation(0);
        }
        viewHolder.llMainContent.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (expandedPosition == position) {
                    expandedPosition = -1;
                } else {
                    expandedPosition = position;
                }
                notifyDataSetChanged();
            }
        });
        return view;
    }
    private void populateMoreInfo(ViewHolder holder, BluetoothLeDevice device) {
        holder.tvDeviceType.setText("设备类型: " + getDeviceType(device));
        ScanRecordCompat record = ScanRecordCompat.parseFromBytes(device.getScanRecord());
        if (record != null) {
            holder.tvAdvType.setText("广播类型: Legacy");
            holder.tvFlags.setText("标志 (Flags): " + getFlags(record));
            holder.tvLocalName.setText("完整本地名称: " + (record.getDeviceName() != null ? record.getDeviceName() : "无"));
            int txPower = record.getTxPowerLevel();
            holder.tvTxPower.setText("发射功率: " + (txPower == Integer.MIN_VALUE ? "无" : txPower + " dBm"));
            List<ParcelUuid> uuids = record.getServiceUuids();
            if (uuids != null && !uuids.isEmpty()) {
                StringBuilder sb = new StringBuilder("服务 UUID: ");
                for (ParcelUuid uuid : uuids) {
                    sb.append(uuid.toString()).append("\n");
                }
                holder.tvServiceUuids.setVisibility(View.VISIBLE);
                holder.tvServiceUuids.setText(sb.toString().trim());
            } else {
                holder.tvServiceUuids.setVisibility(View.GONE);
            }
            Map<ParcelUuid, byte[]> serviceData = record.getServiceData();
            if (serviceData != null && !serviceData.isEmpty()) {
                StringBuilder sb = new StringBuilder("服务数据: ");
                for (Map.Entry<ParcelUuid, byte[]> entry : serviceData.entrySet()) {
                    sb.append(entry.getKey().toString()).append(": ")
                      .append(ByteUtils.byteArrayToHexString(entry.getValue())).append("\n");
                }
                holder.tvServiceData.setVisibility(View.VISIBLE);
                holder.tvServiceData.setText(sb.toString().trim());
            } else {
                holder.tvServiceData.setVisibility(View.GONE);
            }
            holder.tvManufacturerData.setText("厂商数据: " + getManufacturerData(record));
        } else {
            holder.tvAdvType.setText("广播类型: æ— ");
            holder.tvFlags.setText("标志 (Flags): æ— ");
            holder.tvLocalName.setText("完整本地名称: æ— ");
            holder.tvTxPower.setText("发射功率: æ— ");
            holder.tvServiceUuids.setVisibility(View.GONE);
            holder.tvServiceData.setVisibility(View.GONE);
            holder.tvManufacturerData.setText("厂商数据: æ— ");
        }
    }
    private String getDeviceType(BluetoothLeDevice device) {
        int type = device.getDevice().getType();
        switch (type) {
            case BluetoothDevice.DEVICE_TYPE_CLASSIC:
                return "仅经典蓝牙 (Classic)";
            case BluetoothDevice.DEVICE_TYPE_LE:
                return "仅低功耗蓝牙 (LE)";
            case BluetoothDevice.DEVICE_TYPE_DUAL:
                return "双模 (Dual Mode)";
            default:
                return "未知";
        }
    }
    private String getFlags(ScanRecordCompat record) {
        int flags = record.getAdvertiseFlags();
        if (flags == -1) return "无";
        StringBuilder sb = new StringBuilder();
        if ((flags & 0x01) != 0) sb.append("LE æœ‰é™å¯å‘现模式, ");
        if ((flags & 0x02) != 0) sb.append("LE æ™®é€šå¯å‘现模式, ");
        if ((flags & 0x04) != 0) sb.append("不支持 BR/EDR, ");
        if ((flags & 0x08) != 0) sb.append("控制器支持 LE å’Œ BR/EDR, ");
        if ((flags & 0x10) != 0) sb.append("主机支持 LE å’Œ BR/EDR, ");
        if (sb.length() > 2) sb.setLength(sb.length() - 2);
        return sb.toString();
    }
    private String getManufacturerData(ScanRecordCompat record) {
        SparseArray<byte[]> data = record.getManufacturerSpecificData();
        if (data == null || data.size() == 0) return "无";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < data.size(); i++) {
            int id = data.keyAt(i);
            byte[] bytes = data.valueAt(i);
            sb.append(String.format("ID: 0x%04X, æ•°æ®: ", id));
            sb.append(ByteUtils.byteArrayToHexString(bytes));
            if (i < data.size() - 1) sb.append("\n");
        }
        return sb.toString();
    }
    public double calculateAccuracy(final int txPower, final double rssi) {
        if (rssi == 0) {
            return 0; // if we cannot determine accuracy, return -1.
        }
        final double ratio = rssi * 1.0 / txPower;
        if (ratio < 1.0) {
            return Math.pow(ratio, 10);
        } else {
            return (0.89976) * Math.pow(ratio, 7.7095) + 0.111;
        }
    }
    static class ViewHolder {
        TextView deviceName;
        TextView deviceAddress;
        TextView rssi;
        ImageView deviceIcon;
        ImageView ivDown;
        LinearLayout llMainContent;
        LinearLayout llMoreInfo;
        TextView tvDeviceType;
        TextView tvAdvType;
        TextView tvFlags;
        TextView tvLocalName;
        TextView tvTxPower;
        TextView tvServiceUuids;
        TextView tvServiceData;
        TextView tvManufacturerData;
    }
    public interface setOnItemButtonClick {
        void buttonClick(View view, int position);
    }
}
app/src/main/java/com/shlb/comb/adapter/WriteAdapter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package com.shlb.comb.adapter;
import android.view.View;
import androidx.annotation.NonNull;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.viewholder.BaseViewHolder;
import com.shlb.comb.R;
import com.shlb.comb.model.HomeItem;
import java.util.List;
import java.util.Map;
public class WriteAdapter extends BaseQuickAdapter<Map<String,Object>, BaseViewHolder> {
    public WriteAdapter(int layoutResId, List data) {
        super(layoutResId, data);
    }
    @Override
    protected void convert(@NonNull BaseViewHolder helper, Map<String,Object> item) {
        helper.setText(R.id.name, item.get("name").toString());
        if("1" == item.get("flag").toString()){
            helper.getView(R.id.logo).setVisibility(View.VISIBLE);
        }else {
            helper.getView(R.id.logo).setVisibility(View.GONE);
        }
    }
}
app/src/main/java/com/shlb/comb/base/BaseActivity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,275 @@
package com.shlb.comb.base;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.blankj.utilcode.util.SnackbarUtils;
import com.blankj.utilcode.util.ToastUtils;
import com.qmuiteam.qmui.widget.dialog.QMUITipDialog;
import com.shlb.comb.R;
import org.greenrobot.eventbus.EventBus;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import pub.devrel.easypermissions.EasyPermissions;
public abstract class BaseActivity extends AppCompatActivity implements View.OnClickListener, EasyPermissions.PermissionCallbacks  {
    public String TAG = this.getClass().getSimpleName();
    //title左边按钮
    protected ImageView iv_left;
    //title标题
    protected TextView tv_center;
    //title右边按钮
    protected ImageView iv_right;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        contentView();
        EventBus.getDefault().register(this);
        init();
    }
    private void init() {
        initView();
        initData();
        initEvent();
    }
    protected void initHead() {
        iv_left = findViewById(R.id.iv_left);
        tv_center = findViewById(R.id.tv_center);
        iv_right = findViewById(R.id.iv_right);
        initHeadEvent();
    }
    private void initHeadEvent() {
        iv_left.setOnClickListener(this);
        iv_right.setOnClickListener(this);
    }
    protected abstract void contentView();
    protected abstract void initView();
    protected abstract void initData();
    protected abstract void initEvent();
    @Override
    public void setTitle(CharSequence title) {
        tv_center.setText(title);
    }
    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.iv_left) {
            finish();
        } else if (id == R.id.iv_right) {
        }
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        EventBus.getDefault().unregister(this);
    }
    QMUITipDialog tipDialog;
    //弹出提示
    public void showTips(String tips) {
        tipDialog = new QMUITipDialog.CustomBuilder(this)
                .setContent(R.layout.tipdialog_custom)
                .create();
        tipDialog.setCanceledOnTouchOutside(true);
        ((TextView) tipDialog.findViewById(R.id.tv_tip)).setText(tips);
        tipDialog.show();
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if (tipDialog != null)tipDialog.dismiss();
            }
        }, 1500);
//        LogUtils.e("TAG","准备");
//        handler.postDelayed(new Runnable() {
//            @Override
//            public void run() {
//                LogUtils.e("TAG","进入");
//                if (tipDialog != null) {
//                    LogUtils.e("TAG","关闭");
//                    tipDialog.dismiss();
//                }
//            }
//        }, 1500);
    }
    QMUITipDialog  loading;
    public void showLoading(){
        showLoading("正在加载");
    }
    public void showLoading(String msg){
        if(loading != null && loading.isShowing()){
            loading.dismiss();
        }
        loading = new QMUITipDialog.Builder(this)
                .setIconType(QMUITipDialog.Builder.ICON_TYPE_LOADING)
                .setTipWord(msg)
                .create();
        loading.show();
    }
    public void hiddeLoading(){
        if (loading != null) {
            loading.dismiss();
        }
    }
    /**
     * å¼ºåˆ¶éšè—é”®ç›˜
     */
    protected void hideKeyborad() {
        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(iv_left.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
    }
    /**
     * æ‰“开至指定的Activity
     *
     * @param pClass
     */
    public void openActivity(Class<?> pClass) {
        openActivity(pClass, null);
    }
    /**
     * æ‰“开至指定的Activity
     *
     * @param pClass
     * @param pBundle ä¼ å€¼
     */
    public void openActivity(Class<?> pClass, Bundle pBundle) {
        Intent intent = new Intent(this, pClass);
        if (pBundle != null) {
            intent.putExtras(pBundle);
        }
        startActivity(intent);
    }
    /**
     * æ‰“å¼€activity
     *
     * @param pAction activity动作
     */
    public void openActivity(String pAction) {
        openActivity(pAction, null);
    }
    /**
     * æ‰“å¼€activity
     *
     * @param pAction activity动作
     * @param pBundle æ•°æ®
     */
    public void openActivity(String pAction, Bundle pBundle) {
        Intent intent = new Intent(pAction);
        if (pBundle != null) {
            intent.putExtras(pBundle);
        }
        startActivity(intent);
    }
    /**
     * è¿”回activity ï¼Œç»‘定数据
     *
     * @param pClass
     */
    public void returnActivity(Class<?> pClass) {
        returnActivity(pClass, null);
    }
    /**
     * è¿”回activity ï¼Œç»‘定数据
     *
     * @param pClass
     * @param pBundle
     */
    public void returnActivity(Class<?> pClass, Bundle pBundle) {
        Intent intent = new Intent(this, pClass);
        if (pBundle != null) {
            intent.putExtras(pBundle);
        }
        intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        startActivity(intent);
    }
    /**
     * è¿”回activity
     *
     * @param pAction activity动作
     */
    public void returnActivity(String pAction) {
        returnActivity(pAction, null);
    }
    /**
     * è¿”回activity
     *
     * @param pAction activity动作
     * @param pBundle æ•°æ®
     */
    public void returnActivity(String pAction, Bundle pBundle) {
        Intent intent = new Intent(pAction);
        if (pBundle != null) {
            intent.putExtras(pBundle);
        }
        intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        startActivity(intent);
    }
    public void Toast(String text) {
        ToastUtils.showShort(text);
//        SnackbarUtils.with(tv_center).setDuration(SnackbarUtils.LENGTH_LONG).setMessage(text).show();
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        // Forward results to EasyPermissions
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }
    @Override
    public void onPermissionsGranted(int requestCode, List<String> list) {
        // Some permissions have been granted
        Toast("权限检测通过!");
    }
    @Override
    public void onPermissionsDenied(int requestCode, List<String> list) {
        // Some permissions have been denied
       Toast("未打开所需权限,程序无法正常运行"+list.toString());
    }
}
app/src/main/java/com/shlb/comb/event/UpdateEvent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
package com.shlb.comb.event;
/**
 */
public class UpdateEvent {
    private Type type;
    private Object obj;
    private String msg;
    private int arg1;
    public UpdateEvent(Type type) {
        this.type = type;
    }
    public UpdateEvent(Type type, Object obj) {
        this.obj = obj;
        this.type = type;
    }
    public UpdateEvent(Type type, Object obj, String msg) {
        this.obj = obj;
        this.type = type;
        this.msg = msg;
    }
    public UpdateEvent(Type type, int arg1) {
        this.type = type;
        this.arg1 = arg1;
    }
    public int getArg1() {
        return arg1;
    }
    public void setArg1(int arg1) {
        this.arg1 = arg1;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public Type getType() {
        return type;
    }
    public void setType(Type type) {
        this.type = type;
    }
    public Object getObj() {
        return obj;
    }
    public void setObj(Object obj) {
        this.obj = obj;
    }
    public static enum Type{
        SCAN_UPDATE,
        BLE_DATA,
        POP_SHOW,
        TAB_SWITCH,
        CONFIG_CHANGE,
        CONN_STATU,
        CONN_SERVICE,
        ONLINE,
        OFFLINE,
        DEVICE_INFO
    }
}
app/src/main/java/com/shlb/comb/fragment/DeviceListFragment.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,358 @@
package com.shlb.comb.fragment;
import android.Manifest;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.scan.BluetoothScanManager;
import com.blakequ.bluetooth_manager_lib.scan.ScanOverListener;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanCallbackCompat;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanResultCompat;
import com.blakequ.bluetooth_manager_lib.util.BluetoothUtils;
import com.blankj.utilcode.util.LogUtils;
import com.qmuiteam.qmui.widget.QMUIEmptyView;
import com.qmuiteam.qmui.widget.dialog.QMUIDialog;
import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction;
import com.shlb.comb.R;
import com.shlb.comb.activity.ConnActivity;
import com.shlb.comb.activity.TestActivity;
import com.shlb.comb.activity.HomeActivity;
import com.shlb.comb.adapter.HomeDeviceListAdapter;
import com.shlb.comb.base.BaseActivity;
import com.shlb.comb.event.UpdateEvent;
import com.shlb.comb.model.BluetoothLeDeviceStore;
import com.shlb.comb.util.Singletion;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
import pub.devrel.easypermissions.AfterPermissionGranted;
import pub.devrel.easypermissions.EasyPermissions;
public class DeviceListFragment extends Fragment implements AdapterView.OnItemClickListener, EasyPermissions.PermissionCallbacks {
    private static final int BLUETOOTH_NEDD_PERMISSIONS = 1011;
    public static final int MODE_RETURN_SETTINGS = 0;
    public static final int MODE_GO_TO_TEST = 1;
    private static int navigationMode = MODE_RETURN_SETTINGS;
    private boolean isSwitchingDevice = false;
    public static void setNavigationMode(int mode) {
        navigationMode = mode;
    }
    private QMUIEmptyView mEmptyView;
    private ListView homeList;
    private BluetoothScanManager scanManager;
    private BluetoothLeDeviceStore mDeviceStore;
    private BluetoothUtils mBluetoothUtils;
    private HomeDeviceListAdapter mLeDeviceListAdapter;
    private com.qmuiteam.qmui.widget.dialog.QMUITipDialog mLoadingDialog;
    private void showLoading(String message) {
        if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
            mLoadingDialog.dismiss();
        }
        mLoadingDialog = new com.qmuiteam.qmui.widget.dialog.QMUITipDialog.Builder(getContext())
                .setIconType(com.qmuiteam.qmui.widget.dialog.QMUITipDialog.Builder.ICON_TYPE_LOADING)
                .setTipWord(message)
                .create();
        mLoadingDialog.show();
    }
    private void dismissLoading() {
        if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
            mLoadingDialog.dismiss();
            mLoadingDialog = null;
        }
    }
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_device_list, container, false);
        mEmptyView = view.findViewById(R.id.emptyView);
        homeList = view.findViewById(R.id.homeList);
        return view;
    }
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        EventBus.getDefault().register(this);
        initView();
        initData();
    }
    private void initView() {
        mEmptyView.show(true, "搜索蓝牙", "", "重新扫描", new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (scanManager != null && !scanManager.isScanning()) {
                    startScan();
                }
            }
        });
    }
    private void initData() {
        if (getActivity() instanceof HomeActivity) {
            HomeActivity activity = (HomeActivity) getActivity();
            mBluetoothUtils = activity.getBluetoothUtils();
            mDeviceStore = activity.getDeviceStore();
            scanManager = activity.getScanManager();
        }
        mLeDeviceListAdapter = new HomeDeviceListAdapter(getContext(), new HomeDeviceListAdapter.setOnItemButtonClick() {
            @Override
            public void buttonClick(View view, int position) {
                final BluetoothLeDevice device = (BluetoothLeDevice) mLeDeviceListAdapter.getItem(position);
                if (device == null) {
                    Toast.makeText(getContext(), "连接设备不能为空!", Toast.LENGTH_SHORT).show();
                    return;
                }
                Singletion.getInstance().mDevice = device;
                if (device.getAddress() == null) {
                    Toast.makeText(getContext(), "连接设备地址不能为空!", Toast.LENGTH_SHORT).show();
                    return;
                }
                if (com.shlb.comb.manager.BleGlobalManager.getInstance().isConnected()) {
                    new QMUIDialog.MessageDialogBuilder(getContext())
                            .setTitle("提示")
                            .setMessage("当前已连接设备,是否断开并连接新设备?")
                            .addAction("取消", (dialog, index) -> dialog.dismiss())
                            .addAction("确定", (dialog, index) -> {
                                dialog.dismiss();
                                isSwitchingDevice = true;
                                com.shlb.comb.manager.BleGlobalManager.getInstance().disconnect();
                                connectToDevice(device);
                            })
                            .create(com.qmuiteam.qmui.R.style.QMUI_Dialog).show();
                } else {
                    connectToDevice(device);
                }
            }
        });
        homeList.setOnItemClickListener(this);
        homeList.setAdapter(mLeDeviceListAdapter);
        scanManager.setScanOverListener(new ScanOverListener() {
            @Override
            public void onScanOver() {
            }
        });
        scanManager.setScanCallbackCompat(new ScanCallbackCompat() {
            @Override
            public void onScanFailed(final int errorCode) {
                super.onScanFailed(errorCode);
                Toast.makeText(getContext(), "扫描失败:" + errorCode, Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onScanResult(int callbackType, ScanResultCompat result) {
                super.onScanResult(callbackType, result);
                String deviceName = result.getScanRecord().getDeviceName();
                LogUtils.i("scan device ", result.getLeDevice().getAddress() + " " + deviceName);
                if (deviceName != null) deviceName = deviceName.toLowerCase();
                if (!TextUtils.isEmpty(deviceName)) {
                    mDeviceStore.addDevice(result.getLeDevice());
                    EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.SCAN_UPDATE));
                }
            }
        });
        startScan();
    }
    private void connectToDevice(BluetoothLeDevice device) {
        if (navigationMode == MODE_RETURN_SETTINGS) {
            com.shlb.comb.manager.BleGlobalManager.getInstance().connect(getContext(), device);
            showLoading("正在连接 " + device.getName() + "...");
        } else {
            Intent intent = new Intent(getContext(), TestActivity.class);
            startActivity(intent);
        }
    }
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
    }
    public void startScan() {
        if (checkPermission()) {
            if (checkIsBleState()) {
                if (mDeviceStore != null && scanManager != null && scanManager.isScanning()) {
                    if (getActivity() instanceof BaseActivity) {
                        ((BaseActivity) getActivity()).Toast("正在扫描中...");
                    }
                    return;
                }
                mDeviceStore.clear();
                EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.SCAN_UPDATE));
               ((BaseActivity) getActivity()).Toast("开始扫描...");
                scanManager.startScanNow();
            }
        }
    }
    public void startScanOnce() {
        if (checkPermission()) {
            if (checkIsBleState()) {
                if (mDeviceStore != null && scanManager != null && scanManager.isScanning()) return;
                mDeviceStore.clear();
                EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.SCAN_UPDATE));
                scanManager.startScanOnce();
            }
        }
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventRefresh(UpdateEvent event) {
        if (event.getType() == UpdateEvent.Type.CONN_STATU) {
             int status = 0;
             if (event.getObj() instanceof Integer) {
                 status = (int) event.getObj();
             }
             if (status == android.bluetooth.BluetoothProfile.STATE_CONNECTED) {
                 dismissLoading();
                 Toast.makeText(getContext(), "连接成功", Toast.LENGTH_SHORT).show();
                 if (getFragmentManager() != null) {
                     getFragmentManager().popBackStack();
                 }
             } else if (status == android.bluetooth.BluetoothProfile.STATE_DISCONNECTED) {
                if (isSwitchingDevice) {
                    isSwitchingDevice = false;
                    return;
                }
                dismissLoading();
                Toast.makeText(getContext(), "连接断开", Toast.LENGTH_SHORT).show();
            }
        }
        if (event.getType() == UpdateEvent.Type.SCAN_UPDATE) {
            if (mLeDeviceListAdapter != null && mDeviceStore != null && mDeviceStore.getDeviceList() != null) {
                mLeDeviceListAdapter.refreshData(mDeviceStore.getDeviceList());
            }
            if (mDeviceStore != null && mDeviceStore.getDeviceList() != null && mDeviceStore.getDeviceList().size() > 0) {
                mEmptyView.hide();
                homeList.setVisibility(View.VISIBLE);
            } else {
                homeList.setVisibility(View.GONE);
                mEmptyView.show(true, "搜索蓝牙", "", "重新扫描", new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        if (scanManager != null && !scanManager.isScanning()) {
                            startScan();
                        }
                    }
                });
            }
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }
    @Override
    public void onPermissionsGranted(int requestCode, @NonNull List<String> perms) {
        startScan();
    }
    @Override
    public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
        Toast.makeText(getContext(), "未获得必要权限,无法扫描", Toast.LENGTH_SHORT).show();
    }
    private boolean checkIsBleState() {
        if (!mBluetoothUtils.isBluetoothLeSupported(getContext())) {
            Toast.makeText(getContext(), "当前手机不支持BLE低功耗蓝牙,无法使用!", Toast.LENGTH_SHORT).show();
        } else if (!mBluetoothUtils.isBluetoothIsEnable()) {
            new QMUIDialog.MessageDialogBuilder(getContext())
                    .setTitle("提示")
                    .setMessage("蓝牙未打开,确定打开蓝牙?")
                    .addAction("取消", new QMUIDialogAction.ActionListener() {
                        @Override
                        public void onClick(QMUIDialog dialog, int index) {
                            dialog.dismiss();
                        }
                    })
                    .addAction("确定", new QMUIDialogAction.ActionListener() {
                        @Override
                        public void onClick(QMUIDialog dialog, int index) {
                            mBluetoothUtils.askUserToEnableBluetoothIfNeeded(getActivity());
                            dialog.dismiss();
                        }
                    })
                    .create(com.qmuiteam.qmui.R.style.QMUI_Dialog).show();
        } else {
            return true;
        }
        return false;
    }
    @AfterPermissionGranted(BLUETOOTH_NEDD_PERMISSIONS)
    private boolean checkPermission() {
        String[] permissionList;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            permissionList = new String[]{
                    Manifest.permission.BLUETOOTH_SCAN,
                    Manifest.permission.BLUETOOTH_CONNECT,
                    Manifest.permission.ACCESS_FINE_LOCATION
            };
        } else {
            permissionList = new String[]{
                    Manifest.permission.BLUETOOTH,
                    Manifest.permission.BLUETOOTH_ADMIN,
                    Manifest.permission.ACCESS_COARSE_LOCATION,
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
            };
        }
        if (EasyPermissions.hasPermissions(getContext(), permissionList)) {
            return true;
        } else {
            EasyPermissions.requestPermissions(this, "程序运行需要权限!",
                    BLUETOOTH_NEDD_PERMISSIONS, permissionList);
            return false;
        }
    }
    @Override
    public void onPause() {
        super.onPause();
        if (scanManager != null && scanManager.isScanning()) {
            scanManager.stopCycleScan();
        }
    }
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        dismissLoading();
        EventBus.getDefault().unregister(this);
        if (scanManager != null) {
            scanManager.setScanCallbackCompat(null);
            scanManager.setScanOverListener(null);
        }
    }
}
app/src/main/java/com/shlb/comb/fragment/SettingsFragment.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,712 @@
package com.shlb.comb.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton;
import com.shlb.comb.manager.BleGlobalManager;
import com.shlb.comb.event.UpdateEvent;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import android.bluetooth.BluetoothProfile;
import com.shlb.comb.R;
import java.util.ArrayList;
import java.util.List;
import com.shlb.comb.util.CMD;
import com.shlb.comb.util.CRCutil;
import com.qmuiteam.qmui.widget.dialog.QMUITipDialog;
import android.text.Html;
import android.content.DialogInterface;
public class SettingsFragment extends Fragment {
    private RecyclerView rvGrid;
    private EditText etLayer;
    private EditText etStation;
    private Spinner spinnerBaud;
    private QMUIRoundButton btnWriteAll;
    private QMUIRoundButton btnReadData;
    private QMUIRoundButton btnReadParam;
    private QMUIRoundButton btnClearLog;
    private TextView tvStatus;
    private TextView tvLog;
    private TextView tvLayerStatus;
    private TextView tvStationStatus;
    private TextView tvBaudStatus;
    private android.widget.ScrollView svLog;
    private QMUITipDialog mLoadingDialog;
    private GridAdapter mAdapter;
    private List<BoxStatus> boxStatusList = new ArrayList<>();
    private StringBuilder logBuilder = new StringBuilder();
    private java.util.Queue<String> cmdQueue = new java.util.LinkedList<>();
    private String currentExecutingCmd = ""; // å½“前正在执行的指令,用于校验响应
    private static class BoxStatus {
        int id;
        boolean isOnline;
        boolean hasGlass;
        public BoxStatus(int id) {
            this.id = id;
            this.isOnline = false;
            this.hasGlass = false;
        }
    }
    private Runnable autoReadRunnable = new Runnable() {
        @Override
        public void run() {
            // è“ç‰™è¿žæŽ¥æˆåŠŸåŽ è‡ªåŠ¨è§¦å‘ç›‘æŽ§è¯¦æƒ…çš„ è¯»å–数据 å’Œ å‚数设定这里的 è¯»å–参数
            if (BleGlobalManager.getInstance().isConnected()) {
                showLoading("正在同步数据...");
                if (btnReadData != null) btnReadData.performClick();
                new android.os.Handler().postDelayed(() -> {
                    if (btnReadParam != null) btnReadParam.performClick();
                    // å‡è®¾å‚数读取触发后 1.5秒 å…³é—­ loading,或者在解析完所有参数后关闭
                    // è¿™é‡Œç®€å•处理,延时关闭
                    new android.os.Handler().postDelayed(() -> {
                         dismissLoading();
                    }, 1500);
                }, 1000); // é—´éš”1秒,避免指令冲突
            }
        }
    };
    private android.os.Handler debounceHandler = new android.os.Handler();
    private static final long DEBOUNCE_DELAY_MS = 1500; // 1.5 seconds debounce
    @Override
    public void onStart() {
        super.onStart();
        if (!EventBus.getDefault().isRegistered(this)) {
            EventBus.getDefault().register(this);
        }
        updateConnectionStatus();
    }
    @Override
    public void onStop() {
        super.onStop();
        if (EventBus.getDefault().isRegistered(this)) {
            EventBus.getDefault().unregister(this);
        }
    }
    private void updateConnectionStatus() {
        if (BleGlobalManager.getInstance().isConnected()) {
            tvStatus.setText("状态:已连接");
        } else {
            tvStatus.setText("状态:未连接");
        }
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventRefresh(UpdateEvent event) {
        if (event.getType() == UpdateEvent.Type.CONN_STATU) {
            // Check if obj is integer
            if (event.getObj() instanceof Integer) {
                 int status = (int) event.getObj();
                 if (status == BluetoothProfile.STATE_CONNECTED) {
                     tvStatus.setText("状态:已连接");
                     Toast.makeText(getContext(), "蓝牙已连接", Toast.LENGTH_SHORT).show();
                     // è“ç‰™è¿žæŽ¥æˆåŠŸåŽ è‡ªåŠ¨è§¦å‘
                     triggerAutoRead();
                 } else {
                     tvStatus.setText("状态:已断开");
                 }
            }
        } else if (event.getType() == UpdateEvent.Type.DEVICE_INFO) {
            // Received data
            String hex = event.getMsg();
            tvStatus.setText("收到数据: " + hex);
            appendLog(hex, false); // false for received (Green)
            parseAndRefresh(hex);
        }
    }
    private void parseAndRefresh(String hex) {
        if (hex == null) return;
        // æ ¹æ®CMD前8位判断指令类型
        if (hex.length() >= 8) {
            // å‚数读取返回 (长度至少12位)
            // è¯»å±‚æ•°: A55A0308...
            // è¯»ç«™å·: A55A0304...
            // è¯»æ³¢ç‰¹çއ: A55A0305...
            if (hex.length() >= 12 && (
                hex.startsWith(CMD.READ_FLOORS.substring(0, 8)) ||
                hex.startsWith(CMD.READ_STATION_NUM.substring(0, 8)) ||
                hex.startsWith(CMD.READ_BAUD_RATE.substring(0, 8)))) {
                parseParamResponse(hex);
                return;
            }
            // æ•°æ®è¯»å–返回 (长度至少32位)
            // è¯»æ•°æ®: A55A0301...
            if (hex.length() >= 32 && hex.startsWith(CMD.READ_DATA.substring(0, 8))) {
                parseDataResponse(hex);
                return;
            }
        }
        // å†™å…¥æŒ‡ä»¤çš„返回 (A55A06开头)
        if (hex.startsWith("A55A06") && hex.length() >= 12) {
            parseWriteResponse(hex);
            return;
        }
    }
    private void setLabelStatus(TextView view, String text, int colorResId) {
        if (view != null && getContext() != null) {
            view.setText(text);
            view.setTextColor(androidx.core.content.ContextCompat.getColor(getContext(), colorResId));
        }
    }
    /**
     * è§£æžå‚数读取返回 (站号、波特率、层数)
     */
    private void parseParamResponse(String hex) {
        try {
            // å‘½ä»¤ç±»åž‹: ç¬¬7-8位
            String cmdType = hex.substring(6, 8);
            // æ•°æ®å†…容: ç¬¬11-12位
            String dataHex = hex.substring(10, 12);
            int value = Integer.parseInt(dataHex, 16);
            if ("04".equals(cmdType)) {
                // ç«™å·
                if (etStation != null) etStation.setText(String.valueOf(value));
                setLabelStatus(tvStationStatus, "(读取值)", R.color.base_color);
                appendLog("读取站号: " + value);
            } else if ("05".equals(cmdType)) {
                // æ³¢ç‰¹çއ
                if (spinnerBaud != null && value >= 0 && value < spinnerBaud.getAdapter().getCount()) {
                    spinnerBaud.setSelection(value);
                }
                setLabelStatus(tvBaudStatus, "(读取值)", R.color.base_color);
                appendLog("读取波特率: " + value);
            } else if ("08".equals(cmdType)) {
                // å±‚æ•°
                if (etLayer != null) etLayer.setText(String.valueOf(value));
                setLabelStatus(tvLayerStatus, "(读取值)", R.color.base_color);
                appendLog("读取层数: " + value);
            }
        } catch (Exception e) {
            e.printStackTrace();
            appendLog("参数解析异常: " + e.getMessage());
            setLabelStatus(tvStationStatus, "(读取失败)", R.color.orange);
            setLabelStatus(tvBaudStatus, "(读取失败)", R.color.orange);
            setLabelStatus(tvLayerStatus, "(读取失败)", R.color.orange);
        }
    }
    /**
     * è§£æžå†™å…¥æŒ‡ä»¤è¿”回
     */
    private void parseWriteResponse(String hex) {
        try {
            // ç»“果状态: ç¬¬11-12位
            String statusHex = hex.substring(10, 12);
            boolean isSuccess = "01".equals(statusHex);
            // åªè¦å½“前有正在执行的指令,且收到了写入回复(A55A06开头),我们就认为是当前指令的回复
            // å› ä¸ºæˆ‘们采用队列机制,必须发一条等一条,所以不会有乱序问题。
            // è¿™ç§æ–¹å¼è§„避了 ENTER å’Œ EXIT å‰8位相同导致无法区分的问题。
            if (!currentExecutingCmd.isEmpty()) {
                if (isSuccess) {
                    // è®°å½•日志:根据当前期待的指令来记录,而不是根据返回的 hex
                    // ä½¿ç”¨å®Œæ•´æŒ‡ä»¤å‰ç¼€åŒ¹é…ï¼Œé¿å… ENTER (3B0101) å’Œ EXIT (3B0100) å‰8位相同导致的误判
                    if (currentExecutingCmd.startsWith(CMD.ENTER_SETTING)) {
                        appendLog("进入设定模式成功");
                    } else if (currentExecutingCmd.startsWith(CMD.WRITE_STATION_NUM)) {
                        appendLog("写入站号成功");
                        setLabelStatus(tvStationStatus, "(写入值)", R.color.base_color_s);
                    } else if (currentExecutingCmd.startsWith(CMD.WRITE_BAUD_RATE)) {
                        appendLog("写入波特率成功");
                        setLabelStatus(tvBaudStatus, "(写入值)", R.color.base_color_s);
                    } else if (currentExecutingCmd.startsWith(CMD.EXIT_SETTING)) {
                        appendLog("退出设定模式成功");
                    }
                    // åªæœ‰æˆåŠŸæ‰ç»§ç»­æ‰§è¡Œä¸‹ä¸€æ¡
                    processNextCmd();
                } else {
                    // æ‰§è¡Œå¤±è´¥
                    cmdQueue.clear();
                    currentExecutingCmd = "";
                    tvStatus.setText("写入失败");
                    if (currentExecutingCmd.startsWith(CMD.WRITE_STATION_NUM)) {
                        setLabelStatus(tvStationStatus, "(写入失败)", R.color.orange);
                    } else if (currentExecutingCmd.startsWith(CMD.WRITE_BAUD_RATE)) {
                        setLabelStatus(tvBaudStatus, "(写入失败)", R.color.orange);
                    }
                    appendLog("写入失败: " + hex);
                    Toast.makeText(getContext(), "写入失败", Toast.LENGTH_SHORT).show();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            cmdQueue.clear();
            currentExecutingCmd = "";
            appendLog("写入响应解析异常: " + e.getMessage());
            // å¼‚常也视为失败
            setLabelStatus(tvStationStatus, "(写入失败)", R.color.orange);
            setLabelStatus(tvBaudStatus, "(写入失败)", R.color.orange);
        }
    }
    private void processNextCmd() {
        if (cmdQueue != null && !cmdQueue.isEmpty()) {
            String nextCmd = cmdQueue.poll();
            currentExecutingCmd = nextCmd;
            // æ›´æ–°çŠ¶æ€æ˜¾ç¤º
            if (nextCmd.startsWith(CMD.ENTER_SETTING.substring(0, 10))) {
                tvStatus.setText("正在进入设定模式...");
            } else if (nextCmd.startsWith(CMD.WRITE_STATION_NUM.substring(0, 10))) {
                tvStatus.setText("正在写入站号...");
            } else if (nextCmd.startsWith(CMD.WRITE_BAUD_RATE.substring(0, 10))) {
                tvStatus.setText("正在写入波特率...");
            } else if (nextCmd.startsWith(CMD.EXIT_SETTING.substring(0, 10))) {
                tvStatus.setText("正在退出设定模式...");
            }
            sendCmdWithCrc(nextCmd);
        } else {
            // é˜Ÿåˆ—为空,全部完成
            currentExecutingCmd = "";
            tvStatus.setText("参数写入完成");
            Toast.makeText(getContext(), "参数写入成功", Toast.LENGTH_SHORT).show();
        }
    }
    /**
     * è§£æžç›‘控数据返回 (是否有玻璃、在线状态)
     */
    private void parseDataResponse(String hex) {
        try {
            // è§£æžæ˜¯å¦æœ‰çŽ»ç’ƒ: ç¬¬11-18位 (8个字符 = 32位)
            // ç´¢å¼• 10-17
            String glassHex = hex.substring(10, 18);
            appendLog("解析玻璃数据: " + glassHex);
            // ç›´æŽ¥è§£æž hex ä¸º long (Big Endian)
            // "00000004" -> 4 -> ...00100 -> Bit 2 -> Box 3
            long glassBits = Long.parseLong(glassHex, 16);
            String onlineHex = hex.substring(18, 26);
            appendLog("解析在线数据: " + onlineHex);
            long onlineBits = Long.parseLong(onlineHex, 16);
            // æ›´æ–°30个格子的状态
            for (BoxStatus box : boxStatusList) {
                int bitIndex = box.id - 1; // å¯¹åº”位索引 0-29
                if (bitIndex >= 0 && bitIndex < 32) {
                    // æ£€æŸ¥å¯¹åº”位是否为1
                    box.hasGlass = ((glassBits >> bitIndex) & 1) == 1;
                    box.isOnline = ((onlineBits >> bitIndex) & 1) == 1;
                }
            }
            // åˆ·æ–°åˆ—表显示
            if (mAdapter != null) {
                mAdapter.notifyDataSetChanged();
            }
        } catch (Exception e) {
            e.printStackTrace();
            tvStatus.setText("数据解析错误: " + e.getMessage());
            appendLog("数据解析错误: " + e.getMessage());
        }
    }
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_settings, container, false);
    }
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // Initialize box status list
        boxStatusList.clear();
        for (int i = 1; i <= 30; i++) {
            boxStatusList.add(new BoxStatus(i));
        }
        initView(view);
        // å¦‚果已经连接,自动触发读取
        if (BleGlobalManager.getInstance().isConnected()) {
            triggerAutoRead();
        }
    }
    private void triggerAutoRead() {
        // ä½¿ç”¨é˜²æŠ–机制,避免短时间内多次触发
        debounceHandler.removeCallbacks(autoReadRunnable);
        debounceHandler.postDelayed(autoReadRunnable, DEBOUNCE_DELAY_MS);
    }
    private void showLoading(String msg) {
        if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
            mLoadingDialog.dismiss();
        }
        mLoadingDialog = new QMUITipDialog.Builder(getContext())
                .setIconType(QMUITipDialog.Builder.ICON_TYPE_LOADING)
                .setTipWord(msg)
                .create();
        // å…è®¸ç‚¹å‡»å¤–部或返回键取消 loading(只是关闭 dialog)
        mLoadingDialog.setCancelable(true);
        mLoadingDialog.setCanceledOnTouchOutside(true);
        mLoadingDialog.show();
    }
    private void dismissLoading() {
        if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
            mLoadingDialog.dismiss();
        }
    }
    private void initView(View view) {
        rvGrid = view.findViewById(R.id.rv_grid);
        etLayer = view.findViewById(R.id.et_layer);
        etStation = view.findViewById(R.id.et_station);
        spinnerBaud = view.findViewById(R.id.spinner_baud);
        btnWriteAll = view.findViewById(R.id.btn_write_all);
        btnReadData = view.findViewById(R.id.btn_read_data);
        btnReadParam = view.findViewById(R.id.btn_read_param);
        btnClearLog = view.findViewById(R.id.btn_clear_log);
        tvStatus = view.findViewById(R.id.tv_status);
        tvLog = view.findViewById(R.id.tv_log);
        svLog = view.findViewById(R.id.sv_log);
        tvLayerStatus = view.findViewById(R.id.tv_layer_status);
        tvStationStatus = view.findViewById(R.id.tv_station_status);
        tvBaudStatus = view.findViewById(R.id.tv_baud_status);
        // è§£å†³æ—¥å¿—区域滑动冲突
        svLog.setOnTouchListener((v, event) -> {
            v.getParent().requestDisallowInterceptTouchEvent(true);
            if ((event.getAction() & android.view.MotionEvent.ACTION_MASK) == android.view.MotionEvent.ACTION_UP) {
                v.getParent().requestDisallowInterceptTouchEvent(false);
            }
            return false;
        });
        // Restore logs if any
        if (logBuilder.length() > 0) {
            tvLog.setText(Html.fromHtml(logBuilder.toString()));
        } else {
            logBuilder.append("日志记录:<br>");
            tvLog.setText(Html.fromHtml(logBuilder.toString()));
        }
        // Grid Setup
        // 10 columns to match the image (10 boxes per row)
        // Since we are in a horizontal scroll view, this will layout correctly
        rvGrid.setLayoutManager(new GridLayoutManager(getContext(), 10));
        mAdapter = new GridAdapter();
        rvGrid.setAdapter(mAdapter);
        // Spinner Setup
        String[] baudRates = new String[]{"156Kbps", "625Kbps", "2.5Mbps", "5Mbps", "10Mbps"};
        ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, baudRates);
        spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        spinnerBaud.setAdapter(spinnerAdapter);
        spinnerBaud.setSelection(0); // Select 156Kbps by default
        // Button Listeners
        btnWriteAll.setOnClickListener(v -> {
            if (!BleGlobalManager.getInstance().isConnected()) {
                Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
                appendLog("错误: è“ç‰™æœªè¿žæŽ¥");
                return;
            }
            String stationStr = etStation.getText().toString().trim();
            if (stationStr.isEmpty()) {
                Toast.makeText(getContext(), "请输入站号", Toast.LENGTH_SHORT).show();
                return;
            }
            try {
                int station = Integer.parseInt(stationStr);
                if (station < 1 || station > 64) {
                    Toast.makeText(getContext(), "站号范围无效(1-64)", Toast.LENGTH_SHORT).show();
                    return;
                }
            } catch (NumberFormatException e) {
                Toast.makeText(getContext(), "请输入有效的数字", Toast.LENGTH_SHORT).show();
                return;
            }
            // æž„造指令队列
            cmdQueue.clear();
            // 1. è¿›å…¥è®¾å®š
            cmdQueue.offer(CMD.ENTER_SETTING);
            // 2. å†™å…¥ç«™å·
            int station = Integer.parseInt(stationStr);
            String stationHex = String.format("%02X", station);
            cmdQueue.offer(CMD.WRITE_STATION_NUM + stationHex);
            // 3. å†™å…¥æ³¢ç‰¹çއ
            int baudIndex = spinnerBaud.getSelectedItemPosition();
            String baudHex = String.format("%02X", baudIndex);
            cmdQueue.offer(CMD.WRITE_BAUD_RATE + baudHex);
            // 4. é€€å‡ºè®¾å®š
            cmdQueue.offer(CMD.EXIT_SETTING);
            // å¼€å§‹æ‰§è¡Œ
            processNextCmd();
        });
        btnReadData.setOnClickListener(v -> {
             if (BleGlobalManager.getInstance().isConnected()) {
                 tvStatus.setText("状态:正在读取数据...");
                 sendCmdWithCrc(CMD.READ_DATA);
             } else {
                 Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
                 appendLog("错误: è“ç‰™æœªè¿žæŽ¥");
             }
        });
        btnReadParam.setOnClickListener(v -> {
             if (BleGlobalManager.getInstance().isConnected()) {
                 tvStatus.setText("状态:正在读取参数...");
                 sendCmdWithCrc(CMD.READ_FLOORS);
                 new android.os.Handler().postDelayed(() -> {
                     sendCmdWithCrc(CMD.READ_STATION_NUM);
                 }, 200);
                 new android.os.Handler().postDelayed(() -> {
                     sendCmdWithCrc(CMD.READ_BAUD_RATE);
                 }, 400);
             } else {
                 Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
                 appendLog("错误: è“ç‰™æœªè¿žæŽ¥");
             }
        });
        btnClearLog.setOnClickListener(v -> {
            logBuilder.setLength(0);
            logBuilder.append("日志记录:<br>");
            tvLog.setText(Html.fromHtml(logBuilder.toString()));
        });
    }
    private void appendLog(String msg) {
        appendLog(msg, null);
    }
    private String getCmdDescription(String hex) {
        if (hex == null) return "";
        // å‘送指令匹配
        if (hex.startsWith(CMD.READ_DATA)) return "-读数据";
        if (hex.startsWith(CMD.READ_FLOORS)) return "-读层数";
        if (hex.startsWith(CMD.READ_STATION_NUM)) return "-读站号";
        if (hex.startsWith(CMD.READ_BAUD_RATE)) return "-读波特率";
        // 3B指令 (Enter/Exit) ç‰¹æ®Šå¤„理
        if (hex.startsWith(CMD.ENTER_SETTING.substring(0, 8))) {
             // 1. ä¸Šä¸‹æ–‡ä¼˜å…ˆ: å¦‚果当前有正在执行的指令,以当前指令为准
             if (!currentExecutingCmd.isEmpty()) {
                 if (currentExecutingCmd.startsWith(CMD.ENTER_SETTING)) return "-进入设定";
                 if (currentExecutingCmd.startsWith(CMD.EXIT_SETTING)) return "-退出设定";
             }
             // 2. ç²¾ç¡®åŒ¹é…é€€å‡ºæŒ‡ä»¤
             if (hex.startsWith(CMD.EXIT_SETTING)) return "-退出设定";
             // 3. é»˜è®¤æƒ…况: ä¸Šä¸‹æ–‡ä¸ºç©ºä¸”非明确退出指令,优先判定为退出设定
             // (修复: ä»Žå‚数设定页返回时,收到的退出响应会被误判为进入设定)
             return "-退出设定";
        }
        if (hex.startsWith(CMD.ENTER_SETTING)) return "-进入设定";
        if (hex.startsWith(CMD.EXIT_SETTING)) return "-退出设定";
        if (hex.startsWith(CMD.WRITE_STATION_NUM)) return "-写站号";
        if (hex.startsWith(CMD.WRITE_BAUD_RATE)) return "-写波特率";
        // æŽ¥æ”¶æ•°æ®åŒ¹é…
        // è¯»æ•°æ®è¿”回: A55A0301...
        if (hex.startsWith(CMD.READ_DATA.substring(0, 8))) return "-读数据";
        // è¯»å‚数返回
        if (hex.length() >= 8) {
             if (hex.startsWith(CMD.READ_FLOORS.substring(0, 8))) return "-读层数";
             if (hex.startsWith(CMD.READ_STATION_NUM.substring(0, 8))) return "-读站号";
             if (hex.startsWith(CMD.READ_BAUD_RATE.substring(0, 8))) return "-读波特率";
        }
        // å†™å…¥è¿”回 (A55A06开头)
        if (hex.startsWith("A55A06") && hex.length() >= 12) {
             return "-写入返回";
        }
        return "";
    }
    private void appendLog(String msg, Boolean isSent) {
        String time = com.blankj.utilcode.util.TimeUtils.getNowString(new java.text.SimpleDateFormat("HH:mm:ss.SSS"));
        String displayMsg = msg;
        String cmdDesc = "";
        // å¦‚果是 hex æŒ‡ä»¤ (纯 0-9 A-F a-f),加空格格式化
        if (msg.matches("^[0-9A-Fa-f]+$")) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < msg.length(); i += 2) {
                if (i + 2 <= msg.length()) {
                    sb.append(msg.substring(i, i + 2)).append(" ");
                } else {
                    sb.append(msg.substring(i));
                }
            }
            displayMsg = sb.toString().trim();
            // èŽ·å–æŒ‡ä»¤æè¿°
            cmdDesc = getCmdDescription(msg);
        }
        String logLine;
        if (isSent != null) {
            String color = isSent ? "#1890ff" : "#05aa87"; // Blue for sent, Green for received
            // æ‹¼æŽ¥åˆ°å‰ç¼€åŽé¢: "发送-读站号: " æˆ– "收到-读站号: "
            String prefix = (isSent ? "发送" : "收到") + cmdDesc + ": ";
            logLine = time + " <font color='" + color + "'>" + prefix + displayMsg + "</font><br>";
        } else {
            // æ™®é€šæ—¥å¿—
            logLine = time + " " + displayMsg + "<br>";
        }
        logBuilder.append(logLine);
        if (tvLog != null) {
            tvLog.setText(Html.fromHtml(logBuilder.toString()));
            if (svLog != null) {
                svLog.post(() -> svLog.fullScroll(View.FOCUS_DOWN));
            }
        }
    }
    private void sendCmdWithCrc(String cmd) {
        if (!BleGlobalManager.getInstance().isConnected()) {
            Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
            appendLog("错误: è“ç‰™æœªè¿žæŽ¥");
            return;
        }
        byte[] cmdBytes = BleGlobalManager.hexStringToBytes(cmd);
        if (cmdBytes != null) {
            String crc = CRCutil.getCRC(cmdBytes);
            // Pad CRC to 4 chars if needed
            while (crc.length() < 4) {
                crc = "0" + crc;
            }
            String fullCmd = cmd + crc.toUpperCase();
            appendLog(fullCmd, true); // true for sent (Blue)
            BleGlobalManager.getInstance().sendCmd(fullCmd);
        } else {
            appendLog("错误: æŒ‡ä»¤è½¬æ¢å¤±è´¥");
        }
    }
    private class GridAdapter extends RecyclerView.Adapter<GridAdapter.ViewHolder> {
        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_grid_box, parent, false);
            return new ViewHolder(view);
        }
        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            // Calculate Box ID based on 3 rows of 10, Right to Left logic as seen in image
            // Row 1 (pos 0-9): 10 ... 1
            // Row 2 (pos 10-19): 20 ... 11
            // Row 3 (pos 20-29): 30 ... 21
            int row = position / 10;
            int col = position % 10;
            int boxId = (row + 1) * 10 - col;
            holder.tvBoxNumber.setText(String.valueOf(boxId));
            // Find status for this box
            BoxStatus status = null;
            for (BoxStatus s : boxStatusList) {
                if (s.id == boxId) {
                    status = s;
                    break;
                }
            }
            if (status != null) {
                // Priority: Online > Glass
                // User requirement: "优先显示是否在线" (Prioritize displaying online status)
                // "19-26为也是16进制,解析成二进制32代码我这30个格子是否在线"
                // Usually this means if offline, show offline color. If online, show state (glass/no glass).
                if (!status.isOnline) {
                    holder.viewBox.setBackgroundResource(R.drawable.bg_box_offline); // Offline (Grey)
                } else {
                    if (status.hasGlass) {
                        holder.viewBox.setBackgroundResource(R.drawable.bg_box_full); // Green
                    } else {
                        holder.viewBox.setBackgroundResource(R.drawable.bg_box_empty); // Online but empty (White)
                    }
                }
            }
        }
        @Override
        public int getItemCount() {
            return 30;
        }
        class ViewHolder extends RecyclerView.ViewHolder {
            TextView tvBoxNumber;
            View viewBox;
            public ViewHolder(@NonNull View itemView) {
                super(itemView);
                tvBoxNumber = itemView.findViewById(R.id.tv_box_number);
                viewBox = itemView.findViewById(R.id.view_box);
            }
        }
    }
}
app/src/main/java/com/shlb/comb/manager/BleGlobalManager.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,268 @@
package com.shlb.comb.manager;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothConnectManager;
import com.blakequ.bluetooth_manager_lib.connect.ConnectState;
import com.blakequ.bluetooth_manager_lib.connect.ConnectStateListener;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.shlb.comb.event.UpdateEvent;
import org.greenrobot.eventbus.EventBus;
import java.util.List;
import java.util.UUID;
public class BleGlobalManager {
    private static volatile BleGlobalManager instance;
    private BluetoothConnectManager connectManager;
    private BluetoothLeDevice mDevice;
    private BluetoothGatt mGatt;
    private BluetoothGattCharacteristic writeCharacteristic;
    private BluetoothGattCharacteristic notifyCharacteristic;
    private Context mContext;
    private Handler mHandler = new Handler(Looper.getMainLooper());
    private BleGlobalManager() {
    }
    public static BleGlobalManager getInstance() {
        if (instance == null) {
            synchronized (BleGlobalManager.class) {
                if (instance == null) {
                    instance = new BleGlobalManager();
                }
            }
        }
        return instance;
    }
    public BluetoothLeDevice getBluetoothLeDevice() {
        return mDevice;
    }
    public void connect(Context context, BluetoothLeDevice device) {
        this.mContext = context;
        if (connectManager == null) {
            connectManager = BluetoothConnectManager.getInstance(mContext);
            connectManager.addConnectStateListener(stateListener);
        }
        // æ¯æ¬¡è¿žæŽ¥éƒ½é‡æ–°è®¾ç½®GattCallback,防止被其他Activity(如TestActivity)覆盖
        connectManager.setBluetoothGattCallback(gattCallback);
        this.mDevice = device;
        connectManager.connect(device.getAddress());
    }
    public void disconnect() {
        if (connectManager != null && mDevice != null) {
            connectManager.disconnect(mDevice.getAddress());
        }
        resetCharacteristics();
    }
    public boolean isConnected() {
        return connectManager != null && connectManager.isConnectDevice();
    }
    private void resetCharacteristics() {
        writeCharacteristic = null;
        notifyCharacteristic = null;
        mGatt = null;
    }
    private ConnectStateListener stateListener = new ConnectStateListener() {
        @Override
        public void onConnectStateChanged(String address, ConnectState state) {
            if (state == ConnectState.CONNECTED) {
                EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.CONN_STATU, BluetoothProfile.STATE_CONNECTED, "conn_statu"));
            } else if (state == ConnectState.NORMAL) {
                EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.CONN_STATU, BluetoothProfile.STATE_DISCONNECTED, "conn_statu"));
            }
            if (state == ConnectState.NORMAL) {
                resetCharacteristics();
            }
        }
    };
    private BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                mGatt = gatt;
            }
        }
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            if (status == BluetoothGatt.GATT_SUCCESS) {
                mHandler.post(() -> findAndEnableCharacteristics(gatt.getServices()));
            }
        }
        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
            // Optional: Broadcast write status
        }
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
            byte[] value = characteristic.getValue();
            String hex = bytesToHexString(value);
            // Broadcast received data
            // You can define a custom event or interface for this
             EventBus.getDefault().post(new UpdateEvent(UpdateEvent.Type.DEVICE_INFO, 0, hex));
        }
    };
    private void findAndEnableCharacteristics(List<BluetoothGattService> services) {
        // Simple logic: Find first Write and first Notify in the same service if possible
        // Or just find any Write and any Notify
        for (BluetoothGattService service : services) {
            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                int props = characteristic.getProperties();
                // Find Write Characteristic
                if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0 ||
                    (props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0) {
                    // TODO   å¾…确认
                    if (writeCharacteristic == null && characteristic.getUuid().toString().startsWith("0000ff") ) {
                        writeCharacteristic = characteristic;
                        // Once we found a write char, let's look for a notify char in the SAME service
                        findNotifyInService(service);
                        if (notifyCharacteristic != null) {
                            // Found both in same service, we are good
                            return;
                        }
                    }
                }
            }
        }
        if (writeCharacteristic != null && notifyCharacteristic == null) {
            // If we didn't find notify in the same service, look everywhere
             for (BluetoothGattService service : services) {
                 findNotifyInService(service);
                 if (notifyCharacteristic != null) break;
             }
        }
        if (writeCharacteristic != null) {
            Toast.makeText(mContext, "服务已就绪 (Write: " + writeCharacteristic.getUuid().toString().substring(0,8) + ")", Toast.LENGTH_SHORT).show();
        } else {
             Toast.makeText(mContext, "未找到可写特征值", Toast.LENGTH_SHORT).show();
        }
    }
    private void findNotifyInService(BluetoothGattService service) {
         for (BluetoothGattCharacteristic c : service.getCharacteristics()) {
            int props = c.getProperties();
            if ((props & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0 ||
                    (props & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
                notifyCharacteristic = c;
                enableNotification(c);
                return;
            }
        }
    }
    private void enableNotification(BluetoothGattCharacteristic characteristic) {
        if (mGatt == null || characteristic == null) return;
        boolean success = mGatt.setCharacteristicNotification(characteristic, true);
        if (success) {
            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
                    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
            if (descriptor != null) {
                int props = characteristic.getProperties();
                byte[] value = null;
                if ((props & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
                    value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
                } else if ((props & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
                    value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
                }
                if (value != null) {
                    descriptor.setValue(value);
                    mGatt.writeDescriptor(descriptor);
                }
            }
        }
    }
    public void sendCmd(String hexCmd) {
        if (writeCharacteristic == null || mGatt == null) {
            Toast.makeText(mContext, "蓝牙未连接或服务未找到", Toast.LENGTH_SHORT).show();
            return;
        }
        byte[] data = hexStringToBytes(hexCmd);
        if (data == null) {
             Toast.makeText(mContext, "指令格式错误", Toast.LENGTH_SHORT).show();
            return;
        }
        writeCharacteristic.setValue(data);
        mGatt.writeCharacteristic(writeCharacteristic);
    }
    public static byte[] hexStringToBytes(String hexString) {
        if (hexString == null || hexString.equals("")) {
            return null;
        }
        hexString = hexString.toUpperCase();
        hexString = hexString.replace(" ", "");
        if (hexString.length() % 2 != 0) {
            hexString = "0" + hexString;
        }
        int length = hexString.length() / 2;
        char[] hexChars = hexString.toCharArray();
        byte[] d = new byte[length];
        for (int i = 0; i < length; i++) {
            int pos = i * 2;
            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
        }
        return d;
    }
    private static byte charToByte(char c) {
        return (byte) "0123456789ABCDEF".indexOf(c);
    }
    public static String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString().toUpperCase();
    }
}
app/src/main/java/com/shlb/comb/model/BluetoothLeDeviceStore.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,150 @@
package com.shlb.comb.model;
import com.blakequ.bluetooth_manager_lib.device.BeaconType;
import com.blakequ.bluetooth_manager_lib.device.BeaconUtils;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.device.ibeacon.IBeaconDevice;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import com.shlb.comb.util.CsvWriterHelper;
import com.shlb.comb.util.TimeFormatter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class BluetoothLeDeviceStore {
    private final Map<String, BluetoothLeDevice> mDeviceMap;
    public BluetoothLeDeviceStore() {
        mDeviceMap = new HashMap<>();
    }
    public void addDevice(final BluetoothLeDevice device) {
        //if (mDeviceMap.containsKey(device.getAddress())) {
        //    mDeviceMap.get(device.getAddress()).updateRssiReading(device.getTimestamp(), device.getRssi());
        //} else {
        //    mDeviceMap.put(device.getAddress(), device);
        //}
        mDeviceMap.put(device.getAddress(), device);
    }
    public void clear() {
        mDeviceMap.clear();
    }
    public List<BluetoothLeDevice> getDeviceList() {
        final List<BluetoothLeDevice> methodResult = new ArrayList<>(mDeviceMap.values());
        if(methodResult==null || methodResult.size()< 1) return  methodResult;
        Collections.sort(methodResult, new Comparator<BluetoothLeDevice>() {
            @Override
            public int compare(final BluetoothLeDevice arg0, final BluetoothLeDevice arg1) {
                return arg0.getAddress().compareToIgnoreCase(arg1.getAddress());
            }
        });
        return methodResult;
    }
    public int size(){
        return mDeviceMap.size();
    }
    private String getListAsCsv() {
        final List<BluetoothLeDevice> list = getDeviceList();
        final StringBuilder sb = new StringBuilder();
        sb.append(CsvWriterHelper.addStuff("mac"));
        sb.append(CsvWriterHelper.addStuff("name"));
        sb.append(CsvWriterHelper.addStuff("firstTimestamp"));
        sb.append(CsvWriterHelper.addStuff("firstRssi"));
        sb.append(CsvWriterHelper.addStuff("currentTimestamp"));
        sb.append(CsvWriterHelper.addStuff("currentRssi"));
        sb.append(CsvWriterHelper.addStuff("adRecord"));
        sb.append(CsvWriterHelper.addStuff("iBeacon"));
        sb.append(CsvWriterHelper.addStuff("uuid"));
        sb.append(CsvWriterHelper.addStuff("major"));
        sb.append(CsvWriterHelper.addStuff("minor"));
        sb.append(CsvWriterHelper.addStuff("txPower"));
        sb.append(CsvWriterHelper.addStuff("distance"));
        sb.append(CsvWriterHelper.addStuff("accuracy"));
        sb.append('\n');
        for (final BluetoothLeDevice device : list) {
            sb.append(CsvWriterHelper.addStuff(device.getAddress()));
            sb.append(CsvWriterHelper.addStuff(device.getName()));
            sb.append(CsvWriterHelper.addStuff(TimeFormatter.getIsoDateTime(device.getFirstTimestamp())));
            sb.append(CsvWriterHelper.addStuff(device.getFirstRssi()));
            sb.append(CsvWriterHelper.addStuff(TimeFormatter.getIsoDateTime(device.getTimestamp())));
            sb.append(CsvWriterHelper.addStuff(device.getRssi()));
            sb.append(CsvWriterHelper.addStuff(ByteUtils.byteArrayToHexString(device.getScanRecord())));
            final boolean isIBeacon = BeaconUtils.getBeaconType(device) == BeaconType.IBEACON;
            final String uuid;
            final String minor;
            final String major;
            final String txPower;
            final String distance;
            final String accuracy;
            if (isIBeacon) {
                final IBeaconDevice beacon = new IBeaconDevice(device);
                uuid = String.valueOf(beacon.getUUID());
                minor = String.valueOf(beacon.getMinor());
                major = String.valueOf(beacon.getMajor());
                txPower = String.valueOf(beacon.getCalibratedTxPower());
                distance = beacon.getDistanceDescriptor().toString().toLowerCase(Locale.US);
                accuracy = String.valueOf(beacon.getAccuracy());
            } else {
                uuid = "";
                minor = "";
                major = "";
                txPower = "";
                distance = "";
                accuracy = "";
            }
            sb.append(CsvWriterHelper.addStuff(isIBeacon));
            sb.append(CsvWriterHelper.addStuff(uuid));
            sb.append(CsvWriterHelper.addStuff(minor));
            sb.append(CsvWriterHelper.addStuff(major));
            sb.append(CsvWriterHelper.addStuff(txPower));
            sb.append(CsvWriterHelper.addStuff(distance));
            sb.append(CsvWriterHelper.addStuff(accuracy));
            sb.append('\n');
        }
        return sb.toString();
    }
    private static FileWriter generateFile(final File file, final String contents) {
        FileWriter writer = null;
        try {
            writer = new FileWriter(file);
            writer.append(contents);
            writer.flush();
        } catch (final IOException e) {
            e.printStackTrace();
        } finally {
            try {
                writer.close();
            } catch (final IOException e) {
                e.printStackTrace();
            }
        }
        return writer;
    }
}
app/src/main/java/com/shlb/comb/model/HomeItem.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package com.shlb.comb.model;
public class HomeItem {
    private String title;
    private Class<?> activity;
    private int imageResource;
    public HomeItem() {
    }
    public HomeItem(String title, Class<?> activity, int imageResource) {
        this.title = title;
        this.activity = activity;
        this.imageResource = imageResource;
    }
    public int getImageResource() {
        return imageResource;
    }
    public void setImageResource(int imageResource) {
        this.imageResource = imageResource;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public Class<?> getActivity() {
        return activity;
    }
    public void setActivity(Class<?> activity) {
        this.activity = activity;
    }
}
app/src/main/java/com/shlb/comb/util/CMD.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package com.shlb.comb.util;
public class CMD {
    //进入
    public final static String  GETINTO  =  "A55A060100000000002F35";
    //清除
    public final static String  CLEAN  =  "A55A060200000000002F06";
    //退出
    public final static String  EXIT  =  "A55A060400000000002F60";
    public final static String  ADDR_PREFIX  =  "A55A060300";
    public final static String  ADDR_SUFFIX  =  "000000";
    //读取前缀
    public final static String  READ_PREFIX  =  "A55A03";
    //写入前缀
    public final static String  WRITE_SUFFIX  =  "A55A06";
    //读数据
    public final static String  READ_DATA  =  READ_PREFIX + "0100";
    //进入设定
    public final static String  ENTER_SETTING  = WRITE_SUFFIX + "3B0101";
    //退出设定
    public final static String  EXIT_SETTING  = WRITE_SUFFIX + "3B0100";
    //读层数
    public final static String  READ_FLOORS  = READ_PREFIX + "080100";
    //读站号
    public final static String  READ_STATION_NUM  = READ_PREFIX + "040100";
    //读波特率
    public final static String  READ_BAUD_RATE  = READ_PREFIX + "050100";
    //读主板版本
    public final static String  READ_BOARD_VERSION  = READ_PREFIX + "0A0100";
    //读传感器版本
    public final static String  READ_SENSOR_VERSION  = READ_PREFIX + "0B0100";
    //写站号
    public final static String  WRITE_STATION_NUM  = WRITE_SUFFIX + "3401";
    //写波特率
    public final static String  WRITE_BAUD_RATE  = WRITE_SUFFIX + "3501";
    //开始编址
    public final static String  WRITE_START_ADDRESS  = WRITE_SUFFIX + "3A0100";
    //结束编址
    public final static String  WRITE_END_ADDRESS  = WRITE_SUFFIX + "3A0101";
    //开始自检
    public final static String  WRITE_START_CHECK  = WRITE_SUFFIX + "390100";
    //结束自检
    public final static String  WRITE_END_CHECK  = WRITE_SUFFIX + "390101";
    // é˜ˆå€¼è®¾å®š
    public final static String  WRITE_THRESHOLD_SETTING  = WRITE_SUFFIX + "3202";
    //灵敏设定
    public final static String  WRITE_SENSITIVITY_SETTING  = WRITE_SUFFIX + "3302";
    //主板复位
    public final static String  WRITE_BOARD_RESET  = WRITE_SUFFIX + "360100";
    //传感器复位
    public final static String  WRITE_SENSOR_RESET  = WRITE_SUFFIX + "360101";
}
app/src/main/java/com/shlb/comb/util/CRCutil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.shlb.comb.util;
public class CRCutil {
    /**
     * è®¡ç®—CRC16校验码
     *
     * @param bytes
     * @return
     */
    public static String getCRC(byte[] bytes) {
        int CRC = 0xFFFF;
        int POLYNOMIAL = 0xA001;
        int i, j,m,n;
        for (i = 0; i < bytes.length; i++) {
            CRC ^= ((int) bytes[i] & 0x000000ff);
            for (j = 0; j < 8; j++) {
                if ((CRC & 0x00000001) != 0) {
                    CRC >>= 1;
                    CRC ^= POLYNOMIAL;
                } else {
                    CRC >>= 1;
                }
            }
        }
        m = CRC /256;
        n = CRC%256;
        CRC = n * 256 +m;
        return Integer.toHexString(CRC);
    }
}
app/src/main/java/com/shlb/comb/util/Constants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.shlb.comb.util;
import java.text.DecimalFormat;
public class Constants {
    public static final DecimalFormat DOUBLE_TWO_DIGIT_ACCURACY = new DecimalFormat("#.##");
    public static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String FILTER_SWITCH = "switch";
    public static final String FILTER_NAME = "name";
    public static final String FILTER_RSSI = "rssi";
    public static final String SCAN_PERIOD = "scan";
    public static final String PAUSE_PERIOD = "pause";
    public static final String SHOW_SPINNER = "spinner";
}
app/src/main/java/com/shlb/comb/util/CsvWriterHelper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.shlb.comb.util;
public class CsvWriterHelper {
    private static final String QUOTE = "\"";
    public static String addStuff(final Integer text) {
        return QUOTE + text + QUOTE + ",";
    }
    public static String addStuff(final Long text) {
        return QUOTE + text + QUOTE + ",";
    }
    public static String addStuff(final boolean value) {
        return QUOTE + value + QUOTE + ",";
    }
    public static String addStuff(String text) {
        if (text == null) {
            text = "<blank>";
        }
        text = text.replace(QUOTE, "'");
        return QUOTE + text.trim() + QUOTE + ",";
    }
}
app/src/main/java/com/shlb/comb/util/Singletion.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package com.shlb.comb.util;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
 * ç”¨äºŽå­˜å‚¨å·²è¿žæŽ¥è“ç‰™æ•°æ®
 */
public class Singletion {
    private static final Singletion mInstance = new Singletion();
    private Singletion() {
    }
    public static Singletion getInstance() {
        return mInstance;
    }
    //当前连接蓝牙的的可写服务
    public  List<Map<String,String>>  mbluetoothServices = new ArrayList<>();
    //当前连接蓝牙
    public BluetoothLeDevice mDevice;
    public boolean mConnected;
    public String uuid;
    public void clean(){
        mbluetoothServices.clear();
        mDevice = null;
    }
}
app/src/main/java/com/shlb/comb/util/TimeFormatter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.shlb.comb.util;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class TimeFormatter {
    private final static String FORMAT = "yyyy-MM-dd HH:mm:ss zzz";
    private final static SimpleDateFormat FORMATTER = new SimpleDateFormat(FORMAT, Locale.CHINA);
    public static String getIsoDateTime(final Date date) {
        return FORMATTER.format(date);
    }
    public static String getIsoDateTime(final long millis) {
        return getIsoDateTime(new Date(millis));
    }
}
app/src/main/java/com/shlb/comb/view/ParticleView.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,379 @@
package com.shlb.comb.view;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;
import com.shlb.comb.R;
import com.shlb.comb.view.particleview.LineEvaluator;
import com.shlb.comb.view.particleview.Particle;
import java.util.ArrayList;
import java.util.Collection;
/**
 * ä½œè€…: å·´æŽŒ on 16/8/27 11:29
 * Github: https://github.com/JeasonWong
 */
public class ParticleView extends View {
    private final int STATUS_MOTIONLESS = 0;
    private final int STATUS_PARTICLE_GATHER = 1;
    private final int STATUS_TEXT_MOVING = 2;
    private final int ROW_NUM = 10;
    private final int COLUMN_NUM = 10;
    private final int DEFAULT_MAX_TEXT_SIZE = sp2px(80);
    private final int DEFAULT_MIN_TEXT_SIZE = sp2px(30);
    public final int DEFAULT_TEXT_ANIM_TIME = 1000;
    public final int DEFAULT_SPREAD_ANIM_TIME = 300;
    public final int DEFAULT_HOST_TEXT_ANIM_TIME = 800;
    private Paint mHostTextPaint;
    private Paint mParticleTextPaint;
    private Paint mCirclePaint;
    private Paint mHostBgPaint;
    private int mWidth, mHeight;
    private Particle[][] mParticles = new Particle[ROW_NUM][COLUMN_NUM];
    private Particle[][] mMinParticles = new Particle[ROW_NUM][COLUMN_NUM];
    //背景色
    private int mBgColor;
    //粒子色
    private int mParticleColor;
    //默认粒子文案大小
    private int mParticleTextSize = DEFAULT_MIN_TEXT_SIZE;
    private int mStatus = STATUS_MOTIONLESS;
    private ParticleAnimListener mParticleAnimListener;
    //粒子文案
    private String mParticleText;
    //主文案
    private String mHostText;
    //扩散宽度
    private float mSpreadWidth;
    //Host文字展现宽度
    private float mHostRectWidth;
    //粒子文案的x坐标
    private float mParticleTextX;
    //Host文字的x坐标
    private float mHostTextX;
    //Text anim time in milliseconds
    private int mTextAnimTime;
    //Spread anim time in milliseconds
    private int mSpreadAnimTime;
    //HostText anim time in milliseconds
    private int mHostTextAnimTime;
    private PointF mStartMaxP, mEndMaxP;
    private PointF mStartMinP, mEndMinP;
    public ParticleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public ParticleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(attrs);
    }
    private void initView(AttributeSet attrs) {
        TypedArray typeArray = getContext().obtainStyledAttributes(attrs, R.styleable.ParticleView);
        mHostText = null == typeArray.getString(R.styleable.ParticleView_pv_host_text) ? "" : typeArray.getString(R.styleable.ParticleView_pv_host_text);
        mParticleText = null == typeArray.getString(R.styleable.ParticleView_pv_particle_text) ? "" : typeArray.getString(R.styleable.ParticleView_pv_particle_text);
        mParticleTextSize = (int) typeArray.getDimension(R.styleable.ParticleView_pv_particle_text_size, DEFAULT_MIN_TEXT_SIZE);
        int hostTextSize = (int) typeArray.getDimension(R.styleable.ParticleView_pv_host_text_size, DEFAULT_MIN_TEXT_SIZE);
        mBgColor = typeArray.getColor(R.styleable.ParticleView_pv_background_color, 0xFF0867AB);
        mParticleColor = typeArray.getColor(R.styleable.ParticleView_pv_text_color, 0xFFCEF4FD);
        mTextAnimTime = typeArray.getInt(R.styleable.ParticleView_pv_text_anim_time, DEFAULT_TEXT_ANIM_TIME);
        mSpreadAnimTime = typeArray.getInt(R.styleable.ParticleView_pv_text_anim_time, DEFAULT_SPREAD_ANIM_TIME);
        mHostTextAnimTime = typeArray.getInt(R.styleable.ParticleView_pv_text_anim_time, DEFAULT_HOST_TEXT_ANIM_TIME);
        typeArray.recycle();
        mHostTextPaint = new Paint();
        mHostTextPaint.setAntiAlias(true);
        mHostTextPaint.setTextSize(hostTextSize);
        mParticleTextPaint = new Paint();
        mParticleTextPaint.setAntiAlias(true);
        mCirclePaint = new Paint();
        mCirclePaint.setAntiAlias(true);
        mHostBgPaint = new Paint();
        mHostBgPaint.setAntiAlias(true);
        mHostBgPaint.setTextSize(hostTextSize);
        mParticleTextPaint.setTextSize(mParticleTextSize);
        mCirclePaint.setTextSize(mParticleTextSize);
        mParticleTextPaint.setColor(mBgColor);
        mHostTextPaint.setColor(mBgColor);
        mCirclePaint.setColor(mParticleColor);
        mHostBgPaint.setColor(mParticleColor);
    }
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        mStartMinP = new PointF(mWidth / 2 - getTextWidth(mParticleText, mParticleTextPaint) / 2f - dip2px(4), mHeight / 2 + getTextHeight(mHostText, mHostTextPaint) / 2 - getTextHeight(mParticleText, mParticleTextPaint) / 0.7f);
        mEndMinP = new PointF(mWidth / 2 + getTextWidth(mParticleText, mParticleTextPaint) / 2f + dip2px(10), mHeight / 2 + getTextHeight(mHostText, mHostTextPaint) / 2);
        for (int i = 0; i < ROW_NUM; i++) {
            for (int j = 0; j < COLUMN_NUM; j++) {
                mMinParticles[i][j] = new Particle(mStartMinP.x + (mEndMinP.x - mStartMinP.x) / COLUMN_NUM * j, mStartMinP.y + (mEndMinP.y - mStartMinP.y) / ROW_NUM * i, dip2px(0.8f));
            }
        }
        mStartMaxP = new PointF(mWidth / 2 - DEFAULT_MAX_TEXT_SIZE, mHeight / 2 - DEFAULT_MAX_TEXT_SIZE);
        mEndMaxP = new PointF(mWidth / 2 + DEFAULT_MAX_TEXT_SIZE, mHeight / 2 + DEFAULT_MAX_TEXT_SIZE);
        for (int i = 0; i < ROW_NUM; i++) {
            for (int j = 0; j < COLUMN_NUM; j++) {
                mParticles[i][j] = new Particle(mStartMaxP.x + (mEndMaxP.x - mStartMaxP.x) / COLUMN_NUM * j, mStartMaxP.y + (mEndMaxP.y - mStartMaxP.y) / ROW_NUM * i, getTextWidth(mHostText + mParticleText, mParticleTextPaint) / (COLUMN_NUM * 1.8f));
            }
        }
        Shader linearGradient = new LinearGradient(mWidth / 2 - getTextWidth(mParticleText, mCirclePaint) / 2f,
                mHeight / 2 - getTextHeight(mParticleText, mCirclePaint) / 2,
                mWidth / 2 - getTextWidth(mParticleText, mCirclePaint) / 2,
                mHeight / 2 + getTextHeight(mParticleText, mCirclePaint) / 2,
                new int[]{mParticleColor, Color.argb(120, getR(mParticleColor), getG(mParticleColor), getB(mParticleColor))}, null, Shader.TileMode.CLAMP);
        mCirclePaint.setShader(linearGradient);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mStatus == STATUS_PARTICLE_GATHER) {
            for (int i = 0; i < ROW_NUM; i++) {
                for (int j = 0; j < COLUMN_NUM; j++) {
                    canvas.drawCircle(mParticles[i][j].x, mParticles[i][j].y, mParticles[i][j].radius, mCirclePaint);
                }
            }
        }
        if (mStatus == STATUS_TEXT_MOVING) {
            canvas.drawText(mHostText, mHostTextX, mHeight / 2 + getTextHeight(mHostText, mHostBgPaint) / 2, mHostBgPaint);
            canvas.drawRect(mHostTextX + mHostRectWidth, mHeight / 2 - getTextHeight(mHostText, mHostBgPaint) / 1.2f, mHostTextX + getTextWidth(mHostText, mHostTextPaint), mHeight / 2 + getTextHeight(mHostText, mHostBgPaint) / 1.2f, mHostTextPaint);
        }
        if (mStatus == STATUS_PARTICLE_GATHER) {
            canvas.drawRoundRect(new RectF(mWidth / 2 - mSpreadWidth, mStartMinP.y, mWidth / 2 + mSpreadWidth, mEndMinP.y), dip2px(2), dip2px(2), mHostBgPaint);
            canvas.drawText(mParticleText, mWidth / 2 - getTextWidth(mParticleText, mParticleTextPaint) / 2, mStartMinP.y + (mEndMinP.y - mStartMinP.y) / 2 + getTextHeight(mParticleText, mParticleTextPaint) / 2, mParticleTextPaint);
        } else if (mStatus == STATUS_TEXT_MOVING) {
            canvas.drawRoundRect(new RectF(mParticleTextX - dip2px(4), mStartMinP.y, mParticleTextX + getTextWidth(mParticleText, mParticleTextPaint) + dip2px(4), mEndMinP.y), dip2px(2), dip2px(2), mHostBgPaint);
            canvas.drawText(mParticleText, mParticleTextX, mStartMinP.y + (mEndMinP.y - mStartMinP.y) / 2 + getTextHeight(mParticleText, mParticleTextPaint) / 2, mParticleTextPaint);
        }
    }
    private void startParticleAnim() {
        mStatus = STATUS_PARTICLE_GATHER;
        Collection<Animator> animList = new ArrayList<>();
        ValueAnimator textAnim = ValueAnimator.ofInt(DEFAULT_MAX_TEXT_SIZE, mParticleTextSize);
        textAnim.setDuration((int) (mTextAnimTime * 0.8f));
        textAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int textSize = (int) valueAnimator.getAnimatedValue();
                mParticleTextPaint.setTextSize(textSize);
            }
        });
        animList.add(textAnim);
        for (int i = 0; i < ROW_NUM; i++) {
            for (int j = 0; j < COLUMN_NUM; j++) {
                final int tempI = i;
                final int tempJ = j;
                ValueAnimator animator = ValueAnimator.ofObject(new LineEvaluator(), mParticles[i][j], mMinParticles[i][j]);
                animator.setDuration(mTextAnimTime + ((int) (mTextAnimTime * 0.02f)) * i + ((int) (mTextAnimTime * 0.03f)) * j);
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        mParticles[tempI][tempJ] = (Particle) animation.getAnimatedValue();
                        if (tempI == ROW_NUM - 1 && tempJ == COLUMN_NUM - 1) {
                            invalidate();
                        }
                    }
                });
                animList.add(animator);
            }
        }
        AnimatorSet set = new AnimatorSet();
        set.playTogether(animList);
        set.start();
        set.addListener(new AnimListener() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startSpreadAnim();
            }
        });
    }
    private void startSpreadAnim() {
        ValueAnimator animator = ValueAnimator.ofFloat(0, getTextWidth(mParticleText, mParticleTextPaint) / 2 + dip2px(4));
        animator.setDuration(mSpreadAnimTime);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mSpreadWidth = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.addListener(new AnimListener() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startHostTextAnim();
            }
        });
        animator.start();
    }
    private void startHostTextAnim() {
        mStatus = STATUS_TEXT_MOVING;
        Collection<Animator> animList = new ArrayList<>();
        ValueAnimator particleTextXAnim = ValueAnimator.ofFloat(mStartMinP.x + dip2px(4), mWidth / 2 - (getTextWidth(mHostText, mHostTextPaint) + getTextWidth(mParticleText, mParticleTextPaint)) / 2 + getTextWidth(mHostText, mHostTextPaint));
        particleTextXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mParticleTextX = (float) animation.getAnimatedValue();
            }
        });
        animList.add(particleTextXAnim);
        ValueAnimator animator = ValueAnimator.ofFloat(0, getTextWidth(mHostText, mHostTextPaint));
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mHostRectWidth = (float) animation.getAnimatedValue();
            }
        });
        animList.add(animator);
        ValueAnimator hostTextXAnim = ValueAnimator.ofFloat(mStartMinP.x, mWidth / 2 - (getTextWidth(mHostText, mHostTextPaint) + getTextWidth(mParticleText, mParticleTextPaint) + dip2px(20)) / 2);
        hostTextXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mHostTextX = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animList.add(hostTextXAnim);
        AnimatorSet set = new AnimatorSet();
        set.playTogether(animList);
        set.setDuration(mHostTextAnimTime);
        set.addListener(new AnimListener() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (null != mParticleAnimListener) {
                    mParticleAnimListener.onAnimationEnd();
                }
            }
        });
        set.start();
    }
    public void startAnim() {
        post(new Runnable() {
            @Override
            public void run() {
                startParticleAnim();
            }
        });
    }
    private abstract class AnimListener implements Animator.AnimatorListener {
        @Override
        public void onAnimationStart(Animator animation) {
        }
        @Override
        public void onAnimationCancel(Animator animation) {
        }
        @Override
        public void onAnimationRepeat(Animator animation) {
        }
    }
    public void setOnParticleAnimListener(ParticleAnimListener particleAnimListener) {
        mParticleAnimListener = particleAnimListener;
    }
    public interface ParticleAnimListener {
        void onAnimationEnd();
    }
    private int dip2px(float dipValue) {
        final float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }
    private int sp2px(float spValue) {
        final float fontScale = getContext().getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
    private float getTextHeight(String text, Paint paint) {
        Rect rect = new Rect();
        paint.getTextBounds(text, 0, text.length(), rect);
        return rect.height() / 1.1f;
    }
    private float getTextWidth(String text, Paint paint) {
        return paint.measureText(text);
    }
    private int getR(int color) {
        int r = (color >> 16) & 0xFF;
        return r;
    }
    private int getG(int color) {
        int g = (color >> 8) & 0xFF;
        return g;
    }
    private int getB(int color) {
        int b = color & 0xFF;
        return b;
    }
}
app/src/main/java/com/shlb/comb/view/particleview/LineEvaluator.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.shlb.comb.view.particleview;
import android.animation.TypeEvaluator;
public class LineEvaluator implements TypeEvaluator<Particle> {
    @Override
    public Particle evaluate(float fraction, Particle startValue, Particle endValue) {
        Particle particle = new Particle();
        particle.x = startValue.x + (endValue.x - startValue.x) * fraction;
        particle.y = startValue.y + (endValue.y - startValue.y) * fraction;
        particle.radius = startValue.radius + (endValue.radius - startValue.radius) * fraction;
        return particle;
    }
}
app/src/main/java/com/shlb/comb/view/particleview/Particle.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package com.shlb.comb.view.particleview;
public class Particle {
    public float x;
    public float y;
    public float radius;
    public Particle() {
    }
    public Particle(float x, float y, float radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }
}
app/src/main/java/com/shlb/comb/view/t.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
package com.shlb.comb.view;
public class t {
}
app/src/main/res/color/s_app_color_blue_2.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
 Tencent is pleased to support the open source community by making QMUI_Android available.
 Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
 Licensed under the MIT License (the "License"); you may not use this file except in
 compliance with the License. You may obtain a copy of the License at
 http://opensource.org/licenses/MIT
 Unless required by applicable law or agreed to in writing, software distributed under the License is
 distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 either express or implied. See the License for the specific language governing permissions and
 limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/app_color_blue_2_pressed" android:state_pressed="true"/>
    <item android:color="@color/app_color_blue_2"/>
</selector>
app/src/main/res/color/selector_drawer_icon.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#1890ff" android:state_selected="true" />
    <item android:color="#1890ff" android:state_pressed="true" />
    <item android:color="#757575" />
</selector>
app/src/main/res/color/selector_drawer_text.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#1890ff" android:state_selected="true" />
    <item android:color="#1890ff" android:state_pressed="true" />
    <item android:color="#333333" />
</selector>
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
        <aapt:attr name="android:fillColor">
            <gradient
                android:endX="85.84757"
                android:endY="92.4963"
                android:startX="42.9492"
                android:startY="49.59793"
                android:type="linear">
                <item
                    android:color="#44000000"
                    android:offset="0.0" />
                <item
                    android:color="#00000000"
                    android:offset="1.0" />
            </gradient>
        </aapt:attr>
    </path>
    <path
        android:fillColor="#FFFFFF"
        android:fillType="nonZero"
        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
        android:strokeWidth="1"
        android:strokeColor="#00000000" />
</vector>
app/src/main/res/drawable/bg_box_empty.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FFFFFF"/>
    <corners android:radius="6dp"/>
    <stroke android:width="1dp" android:color="#E0E0E0"/>
</shape>
app/src/main/res/drawable/bg_box_error.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#F44336"/>
    <corners android:radius="6dp"/>
</shape>
app/src/main/res/drawable/bg_box_full.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#4CAF50"/>
    <corners android:radius="6dp"/>
</shape>
app/src/main/res/drawable/bg_box_offline.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#9E9E9E"/>
    <corners android:radius="6dp"/>
</shape>
app/src/main/res/drawable/bg_card.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/white"/>
    <corners android:radius="12dp"/>
</shape>
app/src/main/res/drawable/bg_drawer_header_gradient.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="135"
        android:startColor="#1890ff"
        android:endColor="#0050b3"
        android:type="linear" />
</shape>
app/src/main/res/drawable/bg_drawer_item.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="#201890ff">
    <item>
        <selector>
            <item android:state_selected="true">
                <shape android:shape="rectangle">
                    <solid android:color="#E6F7FF" />
                    <corners android:radius="8dp" />
                </shape>
            </item>
            <item android:state_pressed="true">
                <shape android:shape="rectangle">
                    <solid android:color="#F5F5F5" />
                    <corners android:radius="8dp" />
                </shape>
            </item>
            <item>
                <shape android:shape="rectangle">
                    <solid android:color="@android:color/transparent" />
                    <corners android:radius="8dp" />
                </shape>
            </item>
        </selector>
    </item>
</ripple>
app/src/main/res/drawable/bg_edittext.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/white"/>
    <corners android:radius="4dp"/>
    <stroke android:width="1dp" android:color="@color/gray_line"/>
</shape>
app/src/main/res/drawable/bg_input_field.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#F5F6FA"/>
    <corners android:radius="8dp"/>
    <stroke android:width="0dp" android:color="@color/gray_line"/>
</shape>
app/src/main/res/drawable/bg_round.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle"
    >
    <solid
        android:color="@color/white"
        />
    <corners
        android:radius="3dp"
        />
</shape>
app/src/main/res/drawable/btn_gray.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape>
            <solid android:color="#D0D0D0"/>
            <corners android:radius="4dp"/>
            <stroke android:width="1dp" android:color="#A0A0A0"/>
        </shape>
    </item>
    <item>
        <shape>
            <solid android:color="#E0E0E0"/>
            <corners android:radius="4dp"/>
            <stroke android:width="1dp" android:color="#B0B0B0"/>
        </shape>
    </item>
</selector>
app/src/main/res/drawable/ic_launcher_background.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path
        android:fillColor="#3DDC84"
        android:pathData="M0,0h108v108h-108z" />
    <path
        android:fillColor="#00000000"
        android:pathData="M9,0L9,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,0L19,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,0L29,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,0L39,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,0L49,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,0L59,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,0L69,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,0L79,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M89,0L89,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M99,0L99,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,9L108,9"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,19L108,19"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,29L108,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,39L108,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,49L108,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,59L108,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,69L108,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,79L108,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,89L108,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,99L108,99"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,29L89,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,39L89,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,49L89,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,59L89,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,69L89,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,79L89,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,19L29,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,19L39,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,19L49,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,19L59,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,19L69,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,19L79,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
</vector>
app/src/main/res/drawable/item_flag_view.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle"
    >
    <solid
        android:color="@color/colorPrimary"
        />
    <corners
        android:radius="1dp"
        />
</shape>
app/src/main/res/drawable/radius_button_bg.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
 Tencent is pleased to support the open source community by making QMUI_Android available.
 Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
 Licensed under the MIT License (the "License"); you may not use this file except in
 compliance with the License. You may obtain a copy of the License at
 http://opensource.org/licenses/MIT
 Unless required by applicable law or agreed to in writing, software distributed under the License is
 distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 either express or implied. See the License for the specific language governing permissions and
 limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
    <!-- rectangle è¡¨ç¤ºä¸ºçŸ©å½¢ -->
    <!-- å¡«å……的颜色 -->
    <solid android:color="#FFEBF9FF"/>
    <!-- è¾¹æ¡†çš„颜色和粗细 -->
    <stroke
        android:width="1px"
        android:color="@color/app_color_blue_2"/>
    <!-- android:radius åœ†è§’的半径 -->
    <corners android:radius="4dp"/>
</shape>
app/src/main/res/drawable/radius_button_bg_pressed.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
 Tencent is pleased to support the open source community by making QMUI_Android available.
 Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
 Licensed under the MIT License (the "License"); you may not use this file except in
 compliance with the License. You may obtain a copy of the License at
 http://opensource.org/licenses/MIT
 Unless required by applicable law or agreed to in writing, software distributed under the License is
 distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 either express or implied. See the License for the specific language governing permissions and
 limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
    <!-- rectangle è¡¨ç¤ºä¸ºçŸ©å½¢ -->
    <!-- å¡«å……的颜色 -->
    <solid android:color="#7FEBF9FF"/>
    <!-- è¾¹æ¡†çš„颜色和粗细 -->
    <stroke
        android:width="1px"
        android:color="@color/app_color_blue_2_pressed"/>
    <!-- android:radius åœ†è§’的半径 -->
    <corners android:radius="4dp"/>
</shape>
app/src/main/res/drawable/s_radius_button_bg.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
 Tencent is pleased to support the open source community by making QMUI_Android available.
 Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
 Licensed under the MIT License (the "License"); you may not use this file except in
 compliance with the License. You may obtain a copy of the License at
 http://opensource.org/licenses/MIT
 Unless required by applicable law or agreed to in writing, software distributed under the License is
 distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 either express or implied. See the License for the specific language governing permissions and
 limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/radius_button_bg_pressed" android:state_pressed="true"/>
    <item android:drawable="@drawable/radius_button_bg"/>
</selector>
app/src/main/res/layout/activity_advanced_setting.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,280 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/gray_bg"
    android:orientation="vertical">
    <include layout="@layout/include_head" />
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="15dp">
            <!-- Advanced Settings Section -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/bg_card"
                android:orientation="vertical"
                android:padding="20dp">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="20dp"
                    android:text="高级设置"
                    android:textColor="@color/base_text"
                    android:textSize="18sp"
                    android:textStyle="bold" />
                <!-- Mainboard Version Row -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:layout_marginBottom="25dp">
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal"
                        android:layout_marginBottom="10dp">
                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="主板版本"
                            android:textColor="@color/base_text"
                            android:textSize="15sp"
                            android:textStyle="bold"/>
                        <TextView
                            android:id="@+id/tv_mainboard_status"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text=""
                            android:textSize="14sp"
                            android:layout_marginLeft="10dp"/>
                    </LinearLayout>
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal">
                        <EditText
                            android:id="@+id/et_mainboard_version"
                            android:layout_width="0dp"
                            android:layout_height="48dp"
                            android:layout_weight="1"
                            android:background="@drawable/bg_input_field"
                            android:paddingStart="10dp"
                            android:paddingEnd="10dp"
                            android:hint="10-10-1961-01"
                            android:enabled="false"
                            android:textColor="@color/base_text"
                            android:textSize="14sp"
                            android:layout_marginRight="10dp"/>
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_mainboard_read"
                            android:layout_width="80dp"
                            android:layout_height="45dp"
                            android:text="读取"
                            android:textColor="@color/white"
                            android:textSize="14sp"
                            app:qmui_backgroundColor="@color/base_color"
                            app:qmui_radius="8dp"/>
                    </LinearLayout>
                </LinearLayout>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:background="@color/gray_line"
                    android:layout_marginBottom="20dp"/>
                <!-- Display Version Row -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:layout_marginBottom="25dp">
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal"
                        android:layout_marginBottom="10dp">
                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="显示屏版本"
                            android:textColor="@color/base_text"
                            android:textSize="15sp"
                            android:textStyle="bold"/>
                        <TextView
                            android:id="@+id/tv_sensor_status"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text=""
                            android:textSize="14sp"
                            android:layout_marginLeft="10dp"/>
                    </LinearLayout>
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal">
                        <EditText
                            android:id="@+id/et_display_version"
                            android:layout_width="0dp"
                            android:layout_height="48dp"
                            android:layout_weight="1"
                            android:background="@drawable/bg_input_field"
                            android:paddingStart="10dp"
                            android:paddingEnd="10dp"
                            android:hint="10-10-1962-01"
                            android:enabled="false"
                            android:textColor="@android:color/black"
                            android:textSize="14sp"
                            android:layout_marginRight="10dp"/>
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_display_read"
                            android:layout_width="80dp"
                            android:layout_height="45dp"
                            android:text="读取"
                            android:textColor="@color/white"
                            android:textSize="14sp"
                            app:qmui_backgroundColor="@color/base_color"
                            app:qmui_radius="8dp"/>
                    </LinearLayout>
                </LinearLayout>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:background="@color/gray_line"
                    android:layout_marginBottom="20dp"/>
                <!-- Addressing Buttons -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:gravity="center">
                    <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                        android:id="@+id/btn_start_addressing"
                        android:layout_width="0dp"
                        android:layout_height="45dp"
                        android:layout_weight="1"
                        android:text="开始编址"
                        android:textColor="@color/white"
                        android:textSize="14sp"
                        app:qmui_backgroundColor="@color/base_color"
                        app:qmui_radius="8dp"
                        android:layout_marginRight="15dp"/>
                    <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                        android:id="@+id/btn_end_addressing"
                        android:layout_width="0dp"
                        android:layout_height="45dp"
                        android:layout_weight="1"
                        android:text="结束编址"
                        android:textColor="@color/white"
                        android:textSize="14sp"
                        app:qmui_backgroundColor="@color/base_color"
                        app:qmui_radius="8dp"/>
                </LinearLayout>
            </LinearLayout>
            <!-- Log Display -->
            <androidx.cardview.widget.CardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:cardCornerRadius="12dp"
                app:cardElevation="2dp"
                app:cardBackgroundColor="@color/white"
                android:layout_marginTop="16dp"
                android:layout_marginBottom="24dp">
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">
                    <!-- Card Header -->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal"
                        android:gravity="center_vertical"
                        android:padding="16dp"
                        android:background="#FAFAFA">
                        <View
                            android:layout_width="4dp"
                            android:layout_height="16dp"
                            android:background="@color/base_color"
                            android:layout_marginEnd="8dp"/>
                        <TextView
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_weight="1"
                            android:text="操作记录"
                            android:textSize="16sp"
                            android:textStyle="bold"
                            android:textColor="#333333" />
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_clear_log"
                            android:layout_width="wrap_content"
                            android:layout_height="32dp"
                            android:text="清空"
                            android:textColor="@color/white"
                            android:textSize="12sp"
                            android:paddingStart="12dp"
                            android:paddingEnd="12dp"
                            app:qmui_backgroundColor="#FF5252"
                            app:qmui_radius="4dp"/>
                    </LinearLayout>
                    <View
                        android:layout_width="match_parent"
                        android:layout_height="1dp"
                        android:background="#EEEEEE"/>
                    <ScrollView
                        android:layout_width="match_parent"
                        android:layout_height="200dp"
                        android:padding="12dp"
                        android:background="#FAFAFA">
                        <TextView
                            android:id="@+id/tv_log"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:text="等待操作..."
                            android:textColor="#333333"
                            android:textSize="12sp"
                            android:fontFamily="monospace"
                            android:lineSpacingExtra="4dp"/>
                    </ScrollView>
                </LinearLayout>
            </androidx.cardview.widget.CardView>
        </LinearLayout>
    </ScrollView>
</LinearLayout>
app/src/main/res/layout/activity_base.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
<?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="match_parent"
    xmlns:titlebar="http://schemas.android.com/apk/res-auto"
    android:id="@+id/ll_root"
    android:orientation="vertical">
 <include
     android:id="@+id/include_head"
     layout="@layout/include_head"/>
    <RelativeLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--加载失败-->
        <LinearLayout
            android:id="@+id/ll_error_refresh"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:orientation="vertical"
            android:visibility="gone">
            <ImageView
                android:id="@+id/img_err"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@mipmap/load_err" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="15dp"
                android:text="加载失败,点击重试"
                android:textSize="15sp" />
        </LinearLayout>
        <!--加载中..-->
        <ViewStub
            android:id="@+id/vs_loading"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:inflatedId="@+id/panel_import"
            android:layout="@layout/layout_loading_view" />
    </RelativeLayout>
</LinearLayout>
app/src/main/res/layout/activity_conn.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/qmui_config_color_background"
    android:orientation="vertical"
    tools:context=".activity.HomeActivity">
    <include layout="@layout/include_head"/>
    <com.qmuiteam.qmui.widget.QMUIEmptyView
        android:id="@+id/emptyView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
    <com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView
        android:id="@+id/groupListView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout >
app/src/main/res/layout/activity_current_device.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
<?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="match_parent"
    android:orientation="vertical">
    <include layout="@layout/include_head" />
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#F5F5F5"
        android:scrollbars="none">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingBottom="24dp">
            <!-- ç¡¬ä»¶ä¿¡æ¯ -->
            <com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView
                android:id="@+id/group_hardware"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp" />
            <!-- ä½ŽåŠŸè€—è“ç‰™ -->
            <com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView
                android:id="@+id/group_ble"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp" />
            <!-- è“ç‰™éŸ³é¢‘ -->
            <com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView
                android:id="@+id/group_audio"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp" />
            <!-- å±å¹•信息 -->
            <com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView
                android:id="@+id/group_screen"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp" />
        </LinearLayout>
    </ScrollView>
</LinearLayout>
app/src/main/res/layout/activity_home.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,257 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="false"
    tools:context=".activity.HomeActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="false"
        android:orientation="vertical">
        <View
            android:layout_width="match_parent"
            android:layout_height="25dp"
            android:background="@color/base_color" />
        <include layout="@layout/include_head_home" />
        <FrameLayout
            android:id="@+id/fragment_container"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/dimension_10"
            android:layout_marginBottom="@dimen/dimension_20"
            android:gravity="center"
            android:text="上海兰宝传感科技股份有限公司"
            android:textColor="@color/dark_hint" />
    </LinearLayout>
    <LinearLayout
        android:id="@+id/nav_view"
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="@color/white"
        android:clickable="true"
        android:focusable="true"
        android:orientation="vertical">
        <!-- æŠ½å±‰å¤´éƒ¨ (左右结构,使用主题色) -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="160dp"
            android:background="@color/base_color"
            android:gravity="bottom"
            android:orientation="horizontal"
            android:padding="20dp">
            <ImageView
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:src="@mipmap/ic_launcher" />
            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="60dp"
                android:layout_marginLeft="15dp"
                android:gravity="center_vertical"
                android:orientation="vertical">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="COMB"
                    android:textColor="@color/white"
                    android:textSize="22sp"
                    android:textStyle="bold" />
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Shanghai Lanbao Sensing Technology Co., Ltd."
                    android:textColor="@color/white"
                    android:textSize="11sp"
                    android:letterSpacing="0.1" />
            </LinearLayout>
        </LinearLayout>
        <!-- èœå•列表 -->
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:paddingTop="5dp">
                <!-- å®žæ—¶ç›‘控 -->
                <LinearLayout
                    android:id="@+id/menu_monitor"
                    android:layout_width="match_parent"
                    android:layout_height="48dp"
                    android:gravity="center_vertical"
                    android:orientation="horizontal"
                    android:paddingLeft="20dp"
                    android:paddingRight="20dp">
                    <ImageView
                        android:layout_width="22dp"
                        android:layout_height="22dp"
                        android:src="@mipmap/icon_record"
                        app:tint="@color/drawer_item_text_gray" />
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="25dp"
                        android:text="实时监控"
                        android:textColor="@color/drawer_item_text_gray"
                        android:textSize="15sp" />
                </LinearLayout>
                <!-- è“ç‰™è®¾å¤‡ -->
                <LinearLayout
                    android:id="@+id/menu_devices"
                    android:layout_width="match_parent"
                    android:layout_height="48dp"
                    android:gravity="center_vertical"
                    android:orientation="horizontal"
                    android:paddingLeft="20dp"
                    android:paddingRight="20dp">
                    <ImageView
                        android:layout_width="22dp"
                        android:layout_height="22dp"
                        android:src="@mipmap/ic_bluetooth"
                        app:tint="@color/drawer_item_text_gray" />
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="25dp"
                        android:text="蓝牙设备"
                        android:textColor="@color/drawer_item_text_gray"
                        android:textSize="15sp" />
                </LinearLayout>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:layout_marginTop="5dp"
                    android:layout_marginBottom="5dp"
                    android:background="@color/drawer_divider" />
                <!-- å‚数设定 -->
                <LinearLayout
                    android:id="@+id/menu_settings"
                    android:layout_width="match_parent"
                    android:layout_height="48dp"
                    android:gravity="center_vertical"
                    android:orientation="horizontal"
                    android:paddingLeft="20dp"
                    android:paddingRight="20dp">
                    <ImageView
                        android:layout_width="22dp"
                        android:layout_height="22dp"
                        android:src="@mipmap/icon_topbar_overflow"
                        app:tint="@color/drawer_item_text_gray" />
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="25dp"
                        android:text="参数设定"
                        android:textColor="@color/drawer_item_text_gray"
                        android:textSize="15sp" />
                </LinearLayout>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:layout_marginTop="5dp"
                    android:layout_marginBottom="5dp"
                    android:background="@color/drawer_divider" />
                <!-- å½“前设备 -->
                <LinearLayout
                    android:id="@+id/menu_current_device"
                    android:layout_width="match_parent"
                    android:layout_height="48dp"
                    android:gravity="center_vertical"
                    android:orientation="horizontal"
                    android:paddingLeft="20dp"
                    android:paddingRight="20dp">
                    <ImageView
                        android:layout_width="22dp"
                        android:layout_height="22dp"
                        android:src="@mipmap/icon_topbar_about"
                        app:tint="@color/drawer_item_text_gray" />
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="25dp"
                        android:text="当前设备"
                        android:textColor="@color/drawer_item_text_gray"
                        android:textSize="15sp" />
                </LinearLayout>
                <!-- é«˜çº§è®¾ç½® -->
                <LinearLayout
                    android:id="@+id/menu_advanced_settings"
                    android:layout_width="match_parent"
                    android:layout_height="48dp"
                    android:gravity="center_vertical"
                    android:orientation="horizontal"
                    android:paddingLeft="20dp"
                    android:paddingRight="20dp">
                    <ImageView
                        android:layout_width="22dp"
                        android:layout_height="22dp"
                        android:src="@mipmap/icon_topbar_overflow"
                        app:tint="@color/drawer_item_text_gray" />
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="25dp"
                        android:text="高级设置"
                        android:textColor="@color/drawer_item_text_gray"
                        android:textSize="15sp" />
                </LinearLayout>
            </LinearLayout>
        </ScrollView>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="20dp"
            android:gravity="center"
            android:id="@+id/tv_version"
            android:text="1.0.0"
            android:textColor="@color/dark_hint"
            android:textSize="11sp"
            android:letterSpacing="0.1" />
    </LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>
app/src/main/res/layout/activity_parameter_setting.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,458 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/gray_bg"
    android:orientation="vertical">
    <include layout="@layout/include_head" />
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="15dp">
            <!-- Monitoring Details Section -->
            <androidx.cardview.widget.CardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:cardCornerRadius="12dp"
                app:cardElevation="2dp"
                app:cardBackgroundColor="@color/white"
                android:layout_marginBottom="24dp">
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">
                    <!-- Card Header -->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal"
                        android:gravity="center_vertical"
                        android:padding="16dp"
                        android:background="#FAFAFA">
                        <View
                            android:layout_width="4dp"
                            android:layout_height="16dp"
                            android:background="@color/base_color"
                            android:layout_marginEnd="8dp"/>
                        <TextView
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_weight="1"
                            android:text="自检监测"
                            android:textSize="16sp"
                            android:textStyle="bold"
                            android:textColor="#333333" />
                    </LinearLayout>
                    <View
                        android:layout_width="match_parent"
                        android:layout_height="1dp"
                        android:background="#EEEEEE"/>
                    <!-- Card Content -->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="vertical"
                        android:padding="16dp">
                        <HorizontalScrollView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:scrollbars="none">
                            <androidx.recyclerview.widget.RecyclerView
                                android:id="@+id/rv_grid"
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content" />
                        </HorizontalScrollView>
                        <View
                            android:layout_width="match_parent"
                            android:layout_height="1dp"
                            android:background="#EEEEEE"
                            android:layout_marginTop="16dp"
                            android:layout_marginBottom="12dp"/>
                        <!-- Legend -->
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:orientation="horizontal"
                            android:gravity="center_vertical"
                            android:paddingStart="4dp">
                            <View
                                android:layout_width="12dp"
                                android:layout_height="12dp"
                                android:background="@drawable/bg_box_full"
                                android:layout_marginEnd="6dp"/>
                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:text="自检成功"
                                android:textSize="12sp"
                                android:textColor="#666666"
                                android:layout_marginEnd="16dp"/>
                            <View
                                android:layout_width="12dp"
                                android:layout_height="12dp"
                                android:background="@drawable/bg_box_error"
                                android:layout_marginEnd="6dp"/>
                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:text="自检失败"
                                android:textSize="12sp"
                                android:textColor="#666666"
                                android:layout_marginEnd="16dp"/>
                        </LinearLayout>
                        <!-- Start/End Check Buttons -->
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:orientation="horizontal"
                            android:layout_marginTop="20dp">
                            <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                                android:id="@+id/btn_start_check"
                                android:layout_width="0dp"
                                android:layout_height="48dp"
                                android:layout_weight="1"
                                android:text="开始自检"
                                android:textColor="@color/white"
                                android:textSize="14sp"
                                app:qmui_backgroundColor="@color/base_color"
                                app:qmui_radius="8dp"
                                android:layout_marginRight="10dp"/>
                            <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                                android:id="@+id/btn_end_check"
                                android:layout_width="0dp"
                                android:layout_height="48dp"
                                android:layout_weight="1"
                                android:text="结束自检"
                                android:textColor="@color/base_color"
                                android:textSize="14sp"
                                app:qmui_backgroundColor="@color/white"
                                app:qmui_borderColor="@color/base_color"
                                app:qmui_borderWidth="1dp"
                                app:qmui_radius="8dp"
                                android:layout_marginLeft="10dp"/>
                        </LinearLayout>
                    </LinearLayout>
                </LinearLayout>
            </androidx.cardview.widget.CardView>
            <!-- Customer Use Section -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/bg_card"
                android:orientation="vertical"
                android:padding="20dp">
                <!-- Threshold Row -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:layout_marginBottom="25dp">
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="阈值"
                        android:textColor="@color/base_text"
                        android:textSize="15sp"
                        android:textStyle="bold"
                        android:layout_marginBottom="10dp"/>
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal">
                        <Spinner
                            android:id="@+id/sp_threshold_mode"
                            android:layout_width="0dp"
                            android:layout_height="48dp"
                            android:layout_weight="1.5"
                            android:entries="@array/threshold_modes"
                            android:background="@drawable/bg_input_field"
                            android:paddingStart="10dp"
                            android:layout_marginRight="10dp" />
                        <EditText
                            android:id="@+id/et_threshold_value"
                            android:layout_width="0dp"
                            android:layout_height="48dp"
                            android:layout_weight="1"
                            android:background="@drawable/bg_input_field"
                            android:gravity="center"
                            android:hint="1-30"
                            android:inputType="number"
                            android:textSize="14sp"/>
                    </LinearLayout>
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="10dp"
                        android:orientation="horizontal">
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_threshold_read"
                            android:layout_width="0dp"
                            android:layout_height="45dp"
                            android:layout_weight="1"
                            android:text="读取"
                            android:textColor="@color/white"
                            android:textSize="14sp"
                            app:qmui_backgroundColor="@color/base_color"
                            app:qmui_radius="8dp"
                            android:layout_marginRight="10dp"/>
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_threshold_write"
                            android:layout_width="0dp"
                            android:layout_height="45dp"
                            android:layout_weight="1"
                            android:text="写入"
                            android:textColor="@color/white"
                            android:textSize="14sp"
                            app:qmui_backgroundColor="@color/base_color_s"
                            app:qmui_radius="8dp"/>
                    </LinearLayout>
                </LinearLayout>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:background="@color/gray_line"
                    android:layout_marginBottom="20dp"/>
                <!-- Sensitivity Row -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:layout_marginBottom="25dp">
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="灵敏度"
                        android:textColor="@color/base_text"
                        android:textSize="15sp"
                        android:textStyle="bold"
                        android:layout_marginBottom="10dp"/>
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal">
                        <Spinner
                            android:id="@+id/sp_sensitivity_mode"
                            android:layout_width="0dp"
                            android:layout_height="48dp"
                            android:layout_weight="1.5"
                            android:entries="@array/sensitivity_modes"
                            android:background="@drawable/bg_input_field"
                            android:paddingStart="10dp"
                            android:layout_marginRight="10dp" />
                        <EditText
                            android:id="@+id/et_sensitivity_value"
                            android:layout_width="0dp"
                            android:layout_height="48dp"
                            android:layout_weight="1"
                            android:background="@drawable/bg_input_field"
                            android:gravity="center"
                            android:hint="1-30"
                            android:inputType="number"
                            android:textSize="14sp"/>
                    </LinearLayout>
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="10dp"
                        android:orientation="horizontal">
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_sensitivity_read"
                            android:layout_width="0dp"
                            android:layout_height="45dp"
                            android:layout_weight="1"
                            android:text="读取"
                            android:textColor="@color/white"
                            android:textSize="14sp"
                            app:qmui_backgroundColor="@color/base_color"
                            app:qmui_radius="8dp"
                            android:layout_marginRight="10dp"/>
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_sensitivity_write"
                            android:layout_width="0dp"
                            android:layout_height="45dp"
                            android:layout_weight="1"
                            android:text="写入"
                            android:textColor="@color/white"
                            android:textSize="14sp"
                            app:qmui_backgroundColor="@color/base_color_s"
                            app:qmui_radius="8dp"/>
                    </LinearLayout>
                </LinearLayout>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:background="@color/gray_line"
                    android:layout_marginBottom="20dp"/>
                <!-- Control Buttons -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="操作"
                        android:textColor="@color/base_text"
                        android:textSize="15sp"
                        android:textStyle="bold"
                        android:layout_marginBottom="10dp"/>
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal">
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_reset_mainboard"
                            android:layout_width="0dp"
                            android:layout_height="48dp"
                            android:layout_weight="1"
                            android:text="主板复位"
                            android:textColor="@color/white"
                            android:textSize="14sp"
                            app:qmui_backgroundColor="@color/orange"
                            app:qmui_radius="8dp"
                            android:layout_marginRight="10dp"/>
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_reset_sensor"
                            android:layout_width="0dp"
                            android:layout_height="48dp"
                            android:layout_weight="1"
                            android:text="传感复位"
                            android:textColor="@color/white"
                            android:textSize="14sp"
                            app:qmui_backgroundColor="@color/orange"
                            app:qmui_radius="8dp"
                            android:layout_marginLeft="10dp"/>
                    </LinearLayout>
                </LinearLayout>
            </LinearLayout>
            <!-- Log Display -->
            <androidx.cardview.widget.CardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:cardCornerRadius="12dp"
                app:cardElevation="2dp"
                app:cardBackgroundColor="@color/white"
                android:layout_marginTop="16dp"
                android:layout_marginBottom="24dp">
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">
                    <!-- Card Header -->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal"
                        android:gravity="center_vertical"
                        android:padding="16dp"
                        android:background="#FAFAFA">
                        <View
                            android:layout_width="4dp"
                            android:layout_height="16dp"
                            android:background="@color/base_color"
                            android:layout_marginEnd="8dp"/>
                        <TextView
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_weight="1"
                            android:text="日志"
                            android:textSize="16sp"
                            android:textStyle="bold"
                            android:textColor="#333333" />
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_clear_log"
                            android:layout_width="wrap_content"
                            android:layout_height="28dp"
                            android:text="清空"
                            android:textColor="@color/white"
                            android:textSize="12sp"
                            android:paddingStart="12dp"
                            android:paddingEnd="12dp"
                            app:qmui_backgroundColor="#FF5252"
                            app:qmui_radius="4dp"/>
                    </LinearLayout>
                    <View
                        android:layout_width="match_parent"
                        android:layout_height="1dp"
                        android:background="#EEEEEE"/>
                    <ScrollView
                        android:id="@+id/sv_log"
                        android:layout_width="match_parent"
                        android:layout_height="200dp"
                        android:padding="12dp"
                        android:scrollbars="vertical"
                        android:background="#FAFAFA">
                        <TextView
                            android:id="@+id/tv_log"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:text="日志记录:\n"
                            android:textSize="12sp"
                            android:fontFamily="monospace"
                            android:textColor="#333333"
                            android:lineSpacingExtra="4dp"/>
                    </ScrollView>
                </LinearLayout>
            </androidx.cardview.widget.CardView>
        </LinearLayout>
    </ScrollView>
</LinearLayout>
app/src/main/res/layout/activity_splash.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:pv="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.shlb.comb.view.ParticleView
        android:id="@+id/ptview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"
        pv:pv_background_color="@color/colorPrimary"
        pv:pv_host_text="COMB"
        pv:pv_host_text_anim_time="600"
        pv:pv_particle_text="移动平台"
        pv:pv_particle_text_size="14sp"
        pv:pv_spread_anim_time="1000"
        pv:pv_text_anim_time="600"
        pv:pv_text_color="#FFF" />
    <TextView
        android:textColor="@color/white"
        android:layout_marginBottom="@dimen/dimension_20"
        android:layout_centerHorizontal="true"
        android:layout_alignParentBottom="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="上海兰宝传感科技股份有限公司"
        />
</RelativeLayout>
app/src/main/res/layout/activity_test.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<?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="match_parent"
    android:background="@color/qmui_config_color_background"
    android:orientation="vertical">
    <include layout="@layout/include_head"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">
        <TextView
            android:id="@+id/tv_status"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="状态: æœªè¿žæŽ¥"
            android:textSize="16sp"
            android:textStyle="bold"
            android:textColor="@android:color/black"/>
        <TextView
            android:id="@+id/tv_selected_service"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="当前选择服务: æ— "
            android:layout_marginTop="10dp"
            android:textColor="@color/base_color"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="可用服务列表:"
            android:layout_marginTop="10dp"/>
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="250dp"
            android:layout_marginTop="5dp"
            android:background="@drawable/bg_round">
            <com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView
                android:id="@+id/groupListView"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
        </ScrollView>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center_vertical"
            android:layout_marginTop="10dp">
            <EditText
                android:id="@+id/et_command"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:background="@drawable/bg_edittext"
                android:padding="10dp"
                android:textSize="14sp"
                android:hint="请输入Hex指令 (如 A55A...)"/>
            <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                android:id="@+id/btn_send"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="10dp"
                android:text="发送"
                android:textColor="@color/white"
                android:layout_marginLeft="10dp"/>
        </LinearLayout>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="日志:"
            android:layout_marginTop="10dp"/>
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:background="#FAFAFA"
            android:layout_marginTop="5dp">
            <TextView
                android:id="@+id/tv_log"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="5dp"
                android:textSize="12sp"
                android:fontFamily="monospace"/>
        </ScrollView>
    </LinearLayout>
</LinearLayout>
app/src/main/res/layout/activity_write.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,315 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/qmui_config_color_background"
    android:orientation="vertical"
    tools:context=".activity.HomeActivity">
    <include layout="@layout/include_head" />
    <com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView
        android:id="@+id/groupListView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone" />
    <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
        android:id="@+id/send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/dimension_15"
        android:clickable="true"
        android:padding="10dp"
        android:text="发送指令"
        android:visibility="gone"
        app:qmui_radius="4dp" />
    <TextView
        android:textSize="@dimen/dimension_40"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:id="@+id/sendNum"
        />
    <LinearLayout
        android:id="@+id/ll_root"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <Button
            android:visibility="gone"
            android:id="@+id/btnInto"
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:layout_marginLeft="@dimen/dimension_10"
            android:layout_marginTop="@dimen/dimension_10"
            android:layout_marginRight="10dp"
            android:gravity="center"
            android:text="进入编址"
            android:textStyle="bold" />
        <Button
            android:visibility="gone"
            android:id="@+id/btnClean"
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:layout_marginLeft="@dimen/dimension_10"
            android:layout_marginTop="@dimen/dimension_10"
            android:layout_marginRight="10dp"
            android:gravity="center"
            android:text="清除地址"
            android:textStyle="bold" />
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="@dimen/dimension_10"
            android:paddingTop="@dimen/dimension_10"
            android:paddingRight="@dimen/dimension_10">
            <Button
                android:id="@+id/btn1"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="1"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn2"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="2"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn3"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="3"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn4"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="4"
                android:textStyle="bold" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="@dimen/dimension_10"
            android:paddingTop="@dimen/dimension_10"
            android:paddingRight="@dimen/dimension_10"
            >
            <Button
                android:id="@+id/btn5"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="5"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn6"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="6"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn7"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="7"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn8"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="8"
                android:textStyle="bold" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="@dimen/dimension_10"
            android:paddingTop="@dimen/dimension_10"
            android:paddingRight="@dimen/dimension_10"
            >
            <Button
                android:id="@+id/btn9"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="9"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn10"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="10"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn11"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="11"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn12"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="12"
                android:textStyle="bold" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="@dimen/dimension_10"
            android:paddingTop="@dimen/dimension_10"
            android:paddingRight="@dimen/dimension_10"
            >
            <Button
                android:id="@+id/btn13"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="13"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn14"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="14"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn15"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="15"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn16"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="16"
                android:textStyle="bold" />
        </LinearLayout>
        <Button
            android:visibility="gone"
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:layout_marginLeft="@dimen/dimension_10"
            android:layout_marginTop="@dimen/dimension_10"
            android:layout_marginRight="10dp"
            android:id="@+id/btnExit"
            android:gravity="center"
            android:text="退出编址"
            android:textStyle="bold" />
    </LinearLayout>
</LinearLayout>
app/src/main/res/layout/activity_write2.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/qmui_config_color_background"
    android:orientation="vertical"
    tools:context=".activity.HomeActivity">
    <include layout="@layout/include_head"/>
    <com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView
        android:id="@+id/groupListView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
        android:layout_marginTop="@dimen/dimension_15"
        android:id="@+id/send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:layout_gravity="center"
        android:padding="10dp"
        android:text="发送指令"
        app:qmui_radius="4dp"/>
</LinearLayout >
app/src/main/res/layout/dialog_layout_bluetooth_item.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
<?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:orientation="horizontal">
    <RelativeLayout
        android:layout_width="@dimen/dimension_60"
        android:layout_height="50dp"
        >
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:orientation="vertical">
            <ImageView
                android:id="@+id/device_icon"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:src="@mipmap/ic_sanjiao" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="-dBm"
                android:layout_gravity="center"
                android:textColor="@color/qmui_config_color_gray_6"
                android:textSize="@dimen/dimension_10" />
        </LinearLayout>
    </RelativeLayout>
    <RelativeLayout
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="50dp"
         >
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:orientation="vertical"
            >
            <TextView
                android:id="@+id/device_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="JDY-123-456"
                />
            <TextView
                android:id="@+id/device_address"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="24:6d:12:dd:ad:df:cd"
                android:textSize="@dimen/dimension_10"
                android:textColor="@color/qmui_config_color_gray_6"
                />
        </LinearLayout>
    </RelativeLayout>
    <RelativeLayout
        android:layout_width="130dp"
        android:layout_height="50dp"
        >
        <Button
            android:layout_width="@dimen/dimension_80"
            android:layout_height="40dp"
            android:layout_centerVertical="true"
            android:layout_marginRight="@dimen/dimension_5"
            android:layout_toLeftOf="@id/iv_down"
            android:focusable="false"
            android:text="CONNECT"
            android:textSize="@dimen/dimension_10" />
        <ImageView
            android:layout_marginRight="20dp"
            android:id="@+id/iv_down"
            android:layout_alignParentRight="true"
            android:src="@mipmap/ic_down"
            android:layout_centerVertical="true"
            android:layout_width="20dp"
            android:layout_height="20dp"/>
    </RelativeLayout>
</LinearLayout>
app/src/main/res/layout/dialog_layout_bluetooth_list.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
<?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="match_parent"
    android:background="@drawable/bg_round"
    android:orientation="vertical"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:orientation="vertical">
        <ListView
            android:id="@android:id/list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </LinearLayout>
</LinearLayout>
app/src/main/res/layout/dialog_layout_write_item.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/dimension_30"
    android:orientation="vertical"
    >
    <TextView
        android:layout_marginLeft="@dimen/dimension_15"
        android:text="测试"
        android:textSize="@dimen/dimension_12"
        android:layout_centerVertical="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/name"
        />
    <ImageView
        android:layout_marginRight="@dimen/dimension_15"
        android:layout_alignParentRight="true"
        android:id="@+id/logo"
        android:layout_centerVertical="true"
        android:layout_width="@dimen/dimension_20"
        android:layout_height="@dimen/dimension_20"
        android:src="@mipmap/about_logo"
        />
</RelativeLayout>
app/src/main/res/layout/dialog_layout_write_list.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
<?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="match_parent"
    android:background="@drawable/bg_round"
    android:orientation="vertical"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:orientation="vertical">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="@dimen/dimension_40">
            <TextView
                android:layout_marginLeft="15dp"
                android:text="选择命令"
                android:textSize="@dimen/dimension_12"
                android:layout_centerVertical="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
            <ImageView
                android:id="@+id/iv_close"
                android:layout_width="@dimen/dimension_20"
                android:layout_height="@dimen/dimension_20"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:layout_marginRight="@dimen/dimension_20"
                android:src="@mipmap/icon_colse" />
        </RelativeLayout>
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/write"
            android:layout_width="match_parent"
            android:layout_height="200dp"/>
        <TextView
            android:id="@+id/selectedtv"
            android:layout_marginLeft="@dimen/dimension_15"
            android:layout_marginTop="@dimen/dimension_10"
            android:textSize="@dimen/dimension_12"
            android:layout_width="wrap_content"
            android:layout_height="@dimen/dimension_20"
            android:text="当前选择:"
            />
        <TextView
            android:id="@+id/restv"
            android:layout_marginLeft="@dimen/dimension_15"
            android:layout_marginTop="@dimen/dimension_10"
            android:textSize="@dimen/dimension_12"
            android:layout_width="wrap_content"
            android:layout_height="@dimen/dimension_20"
            android:text="返回信息:"
            />
        <EditText
            android:id="@+id/writeed"
            android:layout_marginLeft="@dimen/dimension_15"
            android:layout_marginRight="@dimen/dimension_15"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/dimension_10"
            />
        <Button
            android:id="@+id/writebtn"
            android:layout_gravity="center"
            android:textSize="@dimen/dimension_12"
            android:layout_width="@dimen/dimension_90"
            android:layout_height="@dimen/dimension_40"
            android:text="send"
            />
    </LinearLayout>
</LinearLayout>
app/src/main/res/layout/fragment_device_list.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
<?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="match_parent"
    android:orientation="vertical">
    <com.qmuiteam.qmui.widget.QMUIEmptyView
        android:id="@+id/emptyView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <ListView
        android:id="@+id/homeList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@color/gray_line"
        android:dividerHeight="1dp"
        android:visibility="gone" />
</LinearLayout>
app/src/main/res/layout/fragment_settings.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,465 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#F5F6FA"
    android:fillViewport="true">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">
        <!-- Card 1: Grid Status -->
        <androidx.cardview.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:cardCornerRadius="12dp"
            app:cardElevation="2dp"
            app:cardBackgroundColor="@color/white"
            android:layout_marginBottom="24dp">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <!-- Card Header -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:gravity="center_vertical"
                    android:padding="16dp"
                    android:background="#FAFAFA">
                    <View
                        android:layout_width="4dp"
                        android:layout_height="16dp"
                        android:background="@color/base_color"
                        android:layout_marginEnd="8dp"/>
                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:text="监控详情"
                        android:textSize="16sp"
                        android:textStyle="bold"
                        android:textColor="#333333" />
                    <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                        android:id="@+id/btn_read_data"
                        android:layout_width="wrap_content"
                        android:layout_height="32dp"
                        android:text="读取数据"
                        android:textColor="@color/white"
                        android:textSize="12sp"
                        android:paddingStart="12dp"
                        android:paddingEnd="12dp"
                        app:qmui_backgroundColor="@color/base_color"
                        app:qmui_radius="4dp"/>
                </LinearLayout>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:background="#EEEEEE"/>
                <!-- Card Content -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:padding="16dp">
                    <HorizontalScrollView
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:scrollbars="none">
                        <androidx.recyclerview.widget.RecyclerView
                            android:id="@+id/rv_grid"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content" />
                    </HorizontalScrollView>
                    <View
                        android:layout_width="match_parent"
                        android:layout_height="1dp"
                        android:background="#EEEEEE"
                        android:layout_marginTop="16dp"
                        android:layout_marginBottom="12dp"/>
                    <!-- Legend -->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal"
                        android:gravity="center_vertical"
                        android:paddingStart="4dp">
                        <View
                            android:layout_width="12dp"
                            android:layout_height="12dp"
                            android:background="@drawable/bg_box_full"
                            android:layout_marginEnd="6dp"/>
                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="有玻璃"
                            android:textSize="12sp"
                            android:textColor="#666666"
                            android:layout_marginEnd="16dp"/>
                        <View
                            android:layout_width="12dp"
                            android:layout_height="12dp"
                            android:background="@drawable/bg_box_empty"
                            android:layout_marginEnd="6dp"/>
                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="无玻璃"
                            android:textSize="12sp"
                            android:textColor="#666666"
                            android:layout_marginEnd="16dp"/>
                        <View
                            android:layout_width="12dp"
                            android:layout_height="12dp"
                            android:background="@drawable/bg_box_offline"
                            android:layout_marginEnd="6dp"/>
                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="离线"
                            android:textSize="12sp"
                            android:textColor="#666666"/>
                    </LinearLayout>
                </LinearLayout>
            </LinearLayout>
        </androidx.cardview.widget.CardView>
        <!-- Card 2: Controls -->
        <androidx.cardview.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:cardCornerRadius="12dp"
            app:cardElevation="2dp"
            app:cardBackgroundColor="@color/white">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <!-- Card Header -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:gravity="center_vertical"
                    android:padding="16dp"
                    android:background="#FAFAFA">
                    <View
                        android:layout_width="4dp"
                        android:layout_height="16dp"
                        android:background="@color/base_color"
                        android:layout_marginEnd="8dp"/>
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="参数"
                        android:textSize="16sp"
                        android:textStyle="bold"
                        android:textColor="#333333" />
                </LinearLayout>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:background="#EEEEEE"/>
                <!-- Card Content -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:padding="16dp">
                    <!-- Layer Input -->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="vertical"
                        android:layout_marginBottom="16dp">
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:orientation="horizontal"
                            android:gravity="center_vertical"
                            android:layout_marginBottom="8dp">
                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:text="层数"
                                android:textSize="14sp"
                                android:textColor="#666666"/>
                            <TextView
                                android:id="@+id/tv_layer_status"
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textSize="10sp"
                                android:layout_marginStart="8dp"/>
                        </LinearLayout>
                        <EditText
                            android:id="@+id/et_layer"
                            android:layout_width="match_parent"
                            android:layout_height="48dp"
                            android:background="@drawable/bg_input_field"
                            android:paddingStart="16dp"
                            android:paddingEnd="16dp"
                            android:hint="层数 (1-30)"
                            android:textColorHint="#CCCCCC"
                            android:textSize="14sp"
                            android:focusable="false"
                            android:focusableInTouchMode="false"
                            android:inputType="none"/>
                    </LinearLayout>
                    <!-- Station Input -->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="vertical"
                        android:layout_marginBottom="16dp">
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:orientation="horizontal"
                            android:gravity="center_vertical"
                            android:layout_marginBottom="8dp">
                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:text="站号"
                                android:textSize="14sp"
                                android:textColor="#666666"/>
                            <TextView
                                android:id="@+id/tv_station_status"
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textSize="10sp"
                                android:layout_marginStart="8dp"/>
                        </LinearLayout>
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:orientation="horizontal">
                            <EditText
                                android:id="@+id/et_station"
                                android:layout_width="0dp"
                                android:layout_height="48dp"
                                android:layout_weight="1"
                                android:background="@drawable/bg_input_field"
                                android:paddingStart="16dp"
                                android:paddingEnd="16dp"
                                android:hint="请输入站号 (1-64)"
                                android:textColorHint="#CCCCCC"
                                android:textSize="14sp"
                                android:inputType="number"/>
                        </LinearLayout>
                    </LinearLayout>
                    <!-- Baud Rate -->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="vertical">
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:orientation="horizontal"
                            android:gravity="center_vertical"
                            android:layout_marginBottom="8dp">
                            <TextView
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:text="波特率"
                                android:textSize="14sp"
                                android:textColor="#666666"/>
                            <TextView
                                android:id="@+id/tv_baud_status"
                                android:layout_width="wrap_content"
                                android:layout_height="wrap_content"
                                android:textSize="10sp"
                                android:layout_marginStart="8dp"/>
                        </LinearLayout>
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:orientation="horizontal">
                            <FrameLayout
                                android:layout_width="0dp"
                                android:layout_height="48dp"
                                android:layout_weight="1"
                                android:background="@drawable/bg_input_field">
                                <Spinner
                                    android:id="@+id/spinner_baud"
                                    android:layout_width="match_parent"
                                    android:layout_height="match_parent"
                                    android:background="@null"
                                    android:paddingStart="8dp"/>
                            </FrameLayout>
                        </LinearLayout>
                    </LinearLayout>
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal"
                        android:layout_marginTop="24dp">
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_read_param"
                            android:layout_width="0dp"
                            android:layout_height="48dp"
                            android:layout_weight="1"
                            android:layout_marginEnd="8dp"
                            android:text="读取参数"
                            android:textColor="@color/white"
                            android:textSize="16sp"
                            android:textStyle="bold"
                            app:qmui_backgroundColor="@color/base_color"
                            app:qmui_radius="8dp"/>
                        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                            android:id="@+id/btn_write_all"
                            android:layout_width="0dp"
                            android:layout_height="48dp"
                            android:layout_weight="1"
                            android:layout_marginStart="8dp"
                            android:text="写入参数"
                            android:textColor="@color/white"
                            android:textSize="16sp"
                            android:textStyle="bold"
                            app:qmui_backgroundColor="@color/base_color_s"
                            app:qmui_radius="8dp"/>
                    </LinearLayout>
                </LinearLayout>
            </LinearLayout>
        </androidx.cardview.widget.CardView>
        <TextView
            android:id="@+id/tv_status"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="当前状态:等待操作..."
            android:gravity="center"
            android:textSize="12sp"
            android:textColor="#999999"
            android:layout_marginTop="16dp"/>
        <!-- Log Display -->
        <androidx.cardview.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:cardCornerRadius="12dp"
            app:cardElevation="2dp"
            app:cardBackgroundColor="@color/white"
            android:layout_marginTop="16dp"
            android:layout_marginBottom="24dp">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <!-- Card Header -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:gravity="center_vertical"
                    android:padding="16dp"
                    android:background="#FAFAFA">
                    <View
                        android:layout_width="4dp"
                        android:layout_height="16dp"
                        android:background="@color/base_color"
                        android:layout_marginEnd="8dp"/>
                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:text="日志"
                        android:textSize="16sp"
                        android:textStyle="bold"
                        android:textColor="#333333" />
                    <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
                        android:id="@+id/btn_clear_log"
                        android:layout_width="wrap_content"
                        android:layout_height="32dp"
                        android:text="清空"
                        android:textColor="@color/white"
                        android:textSize="12sp"
                        android:paddingStart="12dp"
                        android:paddingEnd="12dp"
                        app:qmui_backgroundColor="#FF5252"
                        app:qmui_radius="4dp"/>
                </LinearLayout>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:background="#EEEEEE"/>
                <ScrollView
                    android:id="@+id/sv_log"
                    android:layout_width="match_parent"
                    android:layout_height="200dp"
                    android:padding="12dp"
                    android:scrollbars="vertical"
                    android:background="#FAFAFA">
                    <TextView
                        android:id="@+id/tv_log"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:text="日志记录:\n"
                        android:textSize="12sp"
                        android:fontFamily="monospace"
                        android:textColor="#333333"
                        android:lineSpacingExtra="4dp"/>
                </ScrollView>
            </LinearLayout>
        </androidx.cardview.widget.CardView>
    </LinearLayout>
</androidx.core.widget.NestedScrollView>
app/src/main/res/layout/home_item_view.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
    android:id="@+id/card_view"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_marginLeft="5dp"
    android:layout_marginRight="5dp"
    android:foreground="?android:attr/selectableItemBackground"
    card_view:cardBackgroundColor="@color/white"
    card_view:cardCornerRadius="4dp"
    card_view:cardUseCompatPadding="true">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        >
        <ImageView
            android:id="@+id/img"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_alignParentTop="true"
            android:scaleType="centerCrop"
            android:src="@mipmap/animation_img1"/>
        <TextView
            android:id="@+id/tweetName"
            android:paddingLeft="@dimen/dp_10"
            style="@style/tweetName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Chad"
            android:layout_marginTop="@dimen/dp_10"
            android:layout_below="@+id/img"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"/>
        <TextView
            android:id="@+id/tweetText"
            android:paddingLeft="@dimen/dp_10"
            style="@style/tweetText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="传感器编址"
            android:textColor="@android:color/darker_gray"
            android:layout_below="@+id/tweetName"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:paddingRight="@dimen/dp_10"
            android:paddingBottom="@dimen/dp_10"
            android:layout_alignParentRight="true"
            android:layout_alignParentEnd="true"/>
        <TextView
            android:id="@+id/tweetDate"
            style="@style/tweetDate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text=""
            android:paddingRight="@dimen/dp_10"
            android:layout_alignBaseline="@+id/tweetName"
            android:layout_alignBottom="@+id/tweetName"
            android:layout_alignParentRight="true"
            android:layout_alignParentEnd="true"/>
    </RelativeLayout>
</androidx.cardview.widget.CardView>
app/src/main/res/layout/home_list_bluetooth_item.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,176 @@
<?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:orientation="vertical">
    <LinearLayout
        android:id="@+id/ll_main_content"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dimension_70"
        android:orientation="horizontal">
        <RelativeLayout
            android:layout_width="@dimen/dimension_80"
            android:layout_height="@dimen/dimension_70">
            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:orientation="vertical">
                <ImageView
                    android:id="@+id/device_icon"
                    android:layout_width="30dp"
                    android:layout_height="30dp"
                    android:src="@mipmap/ic_sanjiao" />
                <TextView
                    android:id="@+id/rssi"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:layout_marginTop="@dimen/dimension_5"
                    android:text="-dBm"
                    android:textColor="@color/qmui_config_color_gray_6"
                    android:textSize="@dimen/dimension_10" />
            </LinearLayout>
        </RelativeLayout>
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="@dimen/dimension_70"
            android:layout_weight="1">
            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:orientation="vertical">
                <TextView
                    android:id="@+id/device_name"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:maxLines="1" />
                <TextView
                    android:id="@+id/device_address"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="@dimen/dimension_5"
                    android:maxLines="1"
                    android:textColor="@color/qmui_config_color_gray_6"
                    android:textSize="@dimen/dimension_10" />
            </LinearLayout>
        </RelativeLayout>
        <RelativeLayout
            android:layout_width="130dp"
            android:layout_height="@dimen/dimension_70">
            <Button
                android:id="@+id/conn_btn"
                android:layout_width="@dimen/dimension_80"
                android:layout_height="40dp"
                android:layout_centerVertical="true"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_toLeftOf="@id/iv_down"
                android:focusable="false"
                android:text="CONNECT"
                android:textSize="@dimen/dimension_10" />
            <ImageView
                android:id="@+id/iv_down"
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:layout_marginRight="20dp"
                android:src="@mipmap/ic_down" />
        </RelativeLayout>
    </LinearLayout>
    <LinearLayout
        android:id="@+id/ll_more_info"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:paddingBottom="10dp"
        android:visibility="gone">
        <TextView
            android:id="@+id/tv_device_type"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/qmui_config_color_gray_6"
            android:textSize="12sp"
            android:layout_marginTop="2dp"/>
        <TextView
            android:id="@+id/tv_adv_type"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/qmui_config_color_gray_6"
            android:textSize="12sp"
            android:layout_marginTop="2dp"/>
        <TextView
            android:id="@+id/tv_flags"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/qmui_config_color_gray_6"
            android:textSize="12sp"
            android:layout_marginTop="2dp"/>
        <TextView
            android:id="@+id/tv_local_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/qmui_config_color_gray_6"
            android:textSize="12sp"
            android:layout_marginTop="2dp"/>
        <TextView
            android:id="@+id/tv_tx_power"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/qmui_config_color_gray_6"
            android:textSize="12sp"
            android:layout_marginTop="2dp"/>
        <TextView
            android:id="@+id/tv_service_uuids"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/qmui_config_color_gray_6"
            android:textSize="12sp"
            android:layout_marginTop="2dp"/>
        <TextView
            android:id="@+id/tv_service_data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/qmui_config_color_gray_6"
            android:textSize="12sp"
            android:layout_marginTop="2dp"/>
        <TextView
            android:id="@+id/tv_manufacturer_data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/qmui_config_color_gray_6"
            android:textSize="12sp"
            android:layout_marginTop="2dp"/>
    </LinearLayout>
</LinearLayout>
app/src/main/res/layout/include_head.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/head"
    android:layout_width="match_parent"
    android:layout_height="@dimen/dimension_50"
    android:background="@color/base_color"
    android:gravity="center_vertical">
    <ImageView
        android:id="@+id/iv_left"
        android:layout_width="@dimen/dimension_40"
        android:layout_height="@dimen/dimension_40"
        android:layout_centerVertical="true"
        android:padding="@dimen/dimension_10"
        android:src="@mipmap/icon_menu"
          />
    <TextView
        android:textColor="@color/white"
        android:textSize="@dimen/dimension_15"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:id="@+id/tv_center"
        />
    <TextView
        android:id="@+id/tv_right_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:paddingRight="16dp"
        android:text="未连接"
        android:textColor="@color/white"
        android:textSize="14sp"/>
    <ImageView
        android:visibility="gone"
        android:padding="@dimen/dimension_10"
        android:layout_alignParentRight="true"
        android:id="@+id/iv_right"
        android:layout_centerVertical="true"
        android:layout_width="@dimen/dimension_40"
        android:layout_height="@dimen/dimension_40"
        android:src="@mipmap/icon_topbar_right"
        />
</RelativeLayout>
app/src/main/res/layout/include_head_home.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/head"
    android:layout_width="match_parent"
    android:layout_height="@dimen/dimension_60"
    android:background="@color/base_color"
    android:paddingTop="@dimen/dimension_10"
    android:gravity="center_vertical">
    <ImageView
        android:id="@+id/iv_left"
        android:layout_width="@dimen/dimension_40"
        android:layout_height="@dimen/dimension_40"
        android:layout_centerVertical="true"
        android:padding="@dimen/dimension_10"
        android:src="@mipmap/icon_menu" />
    <TextView
        android:id="@+id/tv_center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toEndOf="@id/iv_left"
        android:layout_toRightOf="@id/iv_left"
        android:textColor="@color/white"
        android:textSize="@dimen/fontsize_18"
        android:textStyle="bold"
        android:layout_marginLeft="@dimen/dimension_10"
        android:layout_marginStart="@dimen/dimension_10" />
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:gravity="center_vertical"
        android:orientation="horizontal">
        <TextView
            android:id="@+id/tv_scan"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="@dimen/dimension_10"
            android:text="SCAN"
            android:textColor="@color/white"
            android:textSize="@dimen/fontsize_15" />
        <ImageView
            android:id="@+id/iv_right"
            android:layout_width="@dimen/dimension_40"
            android:layout_height="@dimen/dimension_40"
            android:padding="@dimen/dimension_10"
            android:src="@mipmap/icon_topbar_right"
            android:visibility="visible" />
    </LinearLayout>
</RelativeLayout>
app/src/main/res/layout/item_grid_box.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
<?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:gravity="center"
    android:orientation="vertical"
    android:padding="2dp">
    <TextView
        android:id="@+id/tv_box_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="9sp"
        android:textColor="#999999"
        android:layout_marginBottom="2dp"/>
    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <View
            android:id="@+id/view_box"
            android:layout_width="28dp"
            android:layout_height="28dp"
            android:background="@drawable/bg_box_empty" />
        <!-- Add a subtle shadow or overlay effect if needed in future -->
    </FrameLayout>
</LinearLayout>
app/src/main/res/layout/layout_loading_view.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_progress_bar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/img_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
         />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_toRightOf="@+id/img_progress"
        android:text="努力加载中..."
        android:textColor="#585858"
        android:textSize="14sp" />
</RelativeLayout>
app/src/main/res/layout/list_item_device.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:gravity="top"
              android:orientation="horizontal"
    tools:ignore="MissingDefaultResource">
    <ImageView
        android:id="@+id/device_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingRight="5dp"
        android:paddingTop="5dp"
        android:src="@mipmap/ic_bluetooth"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:id="@+id/device_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="24sp"/>
        <GridLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:columnCount="2">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:text="@string/label_mac"
                android:textSize="12sp"
                android:textStyle="bold"/>
            <TextView
                android:id="@+id/device_address"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="12sp"
                android:typeface="monospace"/>
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:text="@string/label_updated"
                android:textSize="12sp"
                android:textStyle="bold"/>
            <TextView
                android:id="@+id/device_last_update"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:textSize="12sp"/>
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:text="@string/label_rssi"
                android:textSize="12sp"
                android:textStyle="bold"/>
            <TextView
                android:id="@+id/device_rssi"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:textSize="12sp"/>
        </GridLayout>
        <TextView
            android:id="@+id/ibeacon_scan_record"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingRight="5dp"
            android:visibility="gone"
            android:textSize="12sp"/>
        <GridLayout
            android:id="@+id/ibeacon_section"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:background="#66e0e0e0"
            android:columnCount="4">
            <!-- ROW 1 -->
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:text="@string/label_uuid"
                android:textSize="12sp"
                android:textStyle="bold"/>
            <TextView
                android:id="@+id/ibeacon_uuid"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_columnSpan="3"
                android:paddingRight="5dp"
                android:textSize="12sp"/>
            <!-- ROW 2 -->
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:text="@string/label_major"
                android:textSize="12sp"
                android:textStyle="bold"/>
            <TextView
                android:id="@+id/ibeacon_major"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:textSize="12sp"/>
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingLeft="5dp"
                android:paddingRight="5dp"
                android:text="@string/label_minor"
                android:textSize="12sp"
                android:textStyle="bold"/>
            <TextView
                android:id="@+id/ibeacon_minor"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:textSize="12sp"/>
            <!-- ROW 3 -->
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:text="@string/label_tx_power"
                android:textSize="12sp"
                android:textStyle="bold"/>
            <TextView
                android:id="@+id/ibeacon_tx_power"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:textSize="12sp"/>
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingLeft="5dp"
                android:paddingRight="5dp"
                android:text="@string/label_distance"
                android:textSize="12sp"
                android:textStyle="bold"/>
            <TextView
                android:id="@+id/ibeacon_distance"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:textSize="12sp"/>
            <!-- ROW 4 -->
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="5dp"
                android:text="@string/label_decriptor"
                android:textSize="12sp"
                android:textStyle="bold"/>
            <TextView
                android:id="@+id/ibeacon_distance_descriptor"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_columnSpan="3"
                android:paddingRight="5dp"
                android:textSize="12sp"/>
        </GridLayout>
    </LinearLayout>
</LinearLayout>
app/src/main/res/layout/normal_activity_write.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,304 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/qmui_config_color_background"
    android:orientation="vertical"
    tools:context=".activity.HomeActivity">
    <include layout="@layout/include_head" />
    <com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView
        android:id="@+id/groupListView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
       />
    <Button
        android:id="@+id/send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/dimension_15"
        android:clickable="true"
        android:padding="10dp"
        android:text="发送指令"
        />
    <LinearLayout
        android:visibility="gone"
        android:id="@+id/ll_root"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <Button
            android:id="@+id/btnInto"
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:layout_marginLeft="@dimen/dimension_10"
            android:layout_marginTop="@dimen/dimension_10"
            android:layout_marginRight="10dp"
            android:gravity="center"
            android:text="进入编址"
            android:textStyle="bold" />
        <Button
            android:id="@+id/btnClean"
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:layout_marginLeft="@dimen/dimension_10"
            android:layout_marginTop="@dimen/dimension_10"
            android:layout_marginRight="10dp"
            android:gravity="center"
            android:text="清除地址"
            android:textStyle="bold" />
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="@dimen/dimension_10"
            android:paddingTop="@dimen/dimension_10"
            android:paddingRight="@dimen/dimension_10">
            <Button
                android:id="@+id/btn1"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="1"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn2"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="2"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn3"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="3"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn4"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="4"
                android:textStyle="bold" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="@dimen/dimension_10"
            android:paddingTop="@dimen/dimension_10"
            android:paddingRight="@dimen/dimension_10"
            >
            <Button
                android:id="@+id/btn5"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="5"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn6"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="6"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn7"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="7"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn8"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="8"
                android:textStyle="bold" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="@dimen/dimension_10"
            android:paddingTop="@dimen/dimension_10"
            android:paddingRight="@dimen/dimension_10"
            >
            <Button
                android:id="@+id/btn9"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="9"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn10"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="10"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn11"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="11"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn12"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="12"
                android:textStyle="bold" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="@dimen/dimension_10"
            android:paddingTop="@dimen/dimension_10"
            android:paddingRight="@dimen/dimension_10"
            >
            <Button
                android:id="@+id/btn13"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="13"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn14"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="14"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn15"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_marginRight="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="15"
                android:textStyle="bold" />
            <Button
                android:id="@+id/btn16"
                android:layout_width="0dp"
                android:layout_height="45dp"
                android:layout_marginLeft="@dimen/dimension_5"
                android:layout_weight="1"
                android:gravity="center"
                android:text="16"
                android:textStyle="bold" />
        </LinearLayout>
        <Button
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:layout_marginLeft="@dimen/dimension_10"
            android:layout_marginTop="@dimen/dimension_10"
            android:layout_marginRight="10dp"
            android:id="@+id/btnExit"
            android:gravity="center"
            android:text="退出编址"
            android:textStyle="bold" />
    </LinearLayout>
</LinearLayout>
app/src/main/res/layout/tipdialog_custom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:gravity="center"
              android:orientation="horizontal"
              tools:ignore="UseCompoundDrawables">
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="自定义 icon"
        android:src="@drawable/qmui_icon_notify_info"/>
    <TextView
        android:id="@+id/tv_tip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:text="自定义结构"
        android:textColor="@color/qmui_config_color_white"
        android:textSize="16sp"/>
</LinearLayout>
app/src/main/res/layout/top_view.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:layout_width="match_parent"
             android:layout_height="wrap_content">
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@mipmap/top_background"/>
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right|bottom"
        android:src="@mipmap/monkey"/>
</FrameLayout>
app/src/main/res/menu/menu_main.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.shlb.comb.MainActivity">
    <item
        android:id="@+id/action_settings"
        android:orderInCategory="100"
        android:title="@string/action_settings"
        app:showAsAction="never" />
</menu>
app/src/main/res/mipmap-hdpi/ic_launcher.png
app/src/main/res/mipmap-mdpi/ic_launcher.png
app/src/main/res/mipmap-xhdpi/about_logo.png
app/src/main/res/mipmap-xhdpi/animation_img1.png
app/src/main/res/mipmap-xhdpi/example_image0.png
app/src/main/res/mipmap-xhdpi/ic_bluetooth.png
app/src/main/res/mipmap-xhdpi/ic_device_ibeacon.png
app/src/main/res/mipmap-xhdpi/ic_down.png
app/src/main/res/mipmap-xhdpi/ic_launcher.png
app/src/main/res/mipmap-xhdpi/ic_sanjiao.png
app/src/main/res/mipmap-xhdpi/ic_sport_bg.png
app/src/main/res/mipmap-xhdpi/icon_back.png
app/src/main/res/mipmap-xhdpi/icon_colse.png
app/src/main/res/mipmap-xhdpi/icon_head.png
app/src/main/res/mipmap-xhdpi/icon_menu.png
app/src/main/res/mipmap-xhdpi/icon_record.png
app/src/main/res/mipmap-xhdpi/icon_topbar_about.png
app/src/main/res/mipmap-xhdpi/icon_topbar_overflow.png
app/src/main/res/mipmap-xhdpi/icon_topbar_right.png
app/src/main/res/mipmap-xhdpi/load_err.png
app/src/main/res/mipmap-xhdpi/monkey.png
app/src/main/res/mipmap-xhdpi/top_background.png
app/src/main/res/mipmap-xxhdpi/animation_img1.png
app/src/main/res/mipmap-xxhdpi/animation_img2.png
app/src/main/res/mipmap-xxhdpi/animation_img3.png
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
app/src/main/res/mipmap-xxhdpi/icon_topbar_about.png
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
app/src/main/res/values-land/dimens.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
<resources>
    <dimen name="fab_margin">48dp</dimen>
</resources>
app/src/main/res/values-w1240dp/dimens.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
<resources>
    <dimen name="fab_margin">200dp</dimen>
</resources>
app/src/main/res/values-w600dp/dimens.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
<resources>
    <dimen name="fab_margin">48dp</dimen>
</resources>
app/src/main/res/values/arrays.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="threshold_modes">
        <item>一键设定</item>
        <item>单层设定</item>
    </string-array>
    <string-array name="sensitivity_modes">
        <item>批量增加</item>
        <item>批量减少</item>
        <item>单层增加</item>
        <item>单层减少</item>
    </string-array>
</resources>
app/src/main/res/values/colors.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#1890ff</color>
    <color name="colorPrimaryDark">#1890ff</color>
    <color name="colorAccent">#1890ff</color>
    <color name="white">#ffffff</color>
    <color name="base_color">#1890ff</color>
    <color name="base_color_s">#05aa87</color>
    <color name="gray_line">#e5e5e5</color>
    <color name="gray_bg">#f7f7f7</color>
    <color name="dark_line">#C2C2C2</color>
    <color name="dark_hint">#898989</color>
    <color name="dark_border">#979797</color>
    <color name="dark_big_line">#727272</color>
    <color name="dark_line2">#9E9E9E</color>
    <color name="bg_green">#2AD9A4</color>
    <color name="bg_title">#EFEEF5</color>
    <color name="base_line">#F4F5F7</color>
    <color name="app_color_blue_2">#31BDF3</color>
    <color name="app_color_blue_2_disabled">#7F31BDF3</color>
    <color name="app_color_blue_2_pressed">#7F31BDF3</color>
    <color name="app_color_theme_1">#EF5362</color> <!-- Grapefruit -->
    <color name="app_color_theme_2">#FE6D4B</color> <!-- Bittersweet -->
    <color name="app_color_theme_3">#FFCF47</color> <!-- Sunflower -->
    <color name="app_color_theme_4">#9FD661</color> <!-- Grass -->
    <color name="app_color_theme_5">#3FD0AD</color> <!-- Mint -->
    <color name="app_color_theme_6">#2BBDF3</color> <!-- Aqua -->
    <color name="app_color_theme_7">#5A9AEF</color> <!-- Blue Jeans -->
    <color name="app_color_theme_8">#AC8FEF</color> <!-- Lavender -->
    <color name="app_color_theme_9">#EE85C1</color> <!-- Pink Rose -->
    <color name="e5">#F7F7F7</color>
    <color name="base_text">#000</color>
    <color name="bg">#10858C96</color>
    <color name="bg_table">#F0F0F0</color>
    <color name="blue_">#196CA0</color>
    <color name="orange">#FAA02A</color>
    <color name="green">#00FF00</color>
    <color name="blue">#EE9A00</color>
    <color name="red">#EE0000</color>
    <color name="content_background">#f2f2f2</color>
    <color name="red_normal">#FF0000</color>
    <color name="red_pressed">#FF6666</color>
    <color name="shape1">#5d8df7</color>
    <color name="shape2">#5EB9F8</color>
    <color name="shape3">#5CC2F8</color>
    <color name="title1">#1890ff</color>
    <color name="title2">#1890ff</color>
    <color name="title3">#1890ff</color>
    <color name="title4">#1890ff</color>
    <color name="bar1">#C0FF8C</color>
    <color name="bar2">#FFF78C</color>
    <color name="bar3">#FFD08C</color>
    <color name="bar4">#8CEAFF</color>
    <color name="bar5">#FF8C9D</color>
    <color name="drawer_header_teal">#008299</color>
    <color name="drawer_item_selected_blue">#00B2D4</color>
    <color name="drawer_item_text_gray">#666666</color>
    <color name="drawer_divider">#EEEEEE</color>
    <color name="drawer_badge_red">#EE0000</color>
    <integer-array name="gradient_arc_color">
        <item>@color/green</item>
        <item>@color/blue</item>
        <item>@color/red</item>
    </integer-array>
    <integer-array name="titlecolor">
        <item>@color/title1</item>
        <item>@color/title2</item>
        <item>@color/title3</item>
    </integer-array>
</resources>
app/src/main/res/values/dimens.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
    <dimen name="padding_top">0dp</dimen>
    <!-- å­—体大小 -->
    <dimen name="fontsize_8">8sp</dimen>
    <dimen name="fontsize_10">10sp</dimen>
    <dimen name="fontsize_12">12sp</dimen>
    <dimen name="fontsize_13">13sp</dimen>
    <dimen name="fontsize_14">14sp</dimen>
    <dimen name="fontsize_15">15sp</dimen>
    <dimen name="fontsize_16">16sp</dimen>
    <dimen name="fontsize_17">17sp</dimen>
    <dimen name="fontsize_18">18sp</dimen>
    <dimen name="fontsize_20">20sp</dimen>
    <dimen name="fontsize_22">22sp</dimen>
    <dimen name="fontsize_24">24sp</dimen>
    <dimen name="fontsize_26">26sp</dimen>
    <dimen name="fontsize_28">28sp</dimen>
    <dimen name="fontsize_34">34sp</dimen>
    <dimen name="fontsize_40">40sp</dimen>
    <!-- å°ºå¯¸ -->
    <dimen name="dimension_0">0dp</dimen>
    <dimen name="dimension_dp_litter">0.25dp</dimen>
    <dimen name="dimension_dp_half">0.5dp</dimen>
    <dimen name="dimension_1">1dp</dimen>
    <dimen name="dimension_2">2dp</dimen>
    <dimen name="dimension_3">3dp</dimen>
    <dimen name="dimension_4">4dp</dimen>
    <dimen name="dimension_5">5dp</dimen>
    <dimen name="dimension_6">6dp</dimen>
    <dimen name="dimension_7">7dp</dimen>
    <dimen name="dimension_8">8dp</dimen>
    <dimen name="dimension_10">10dp</dimen>
    <dimen name="dimension_11">11dp</dimen>
    <dimen name="dimension_12">12dp</dimen>
    <dimen name="dimension_13">13dp</dimen>
    <dimen name="dimension_14">14dp</dimen>
    <dimen name="dimension_15">15dp</dimen>
    <dimen name="dimension_16">16dp</dimen>
    <dimen name="dimension_17">17dp</dimen>
    <dimen name="dimension_18">18dp</dimen>
    <dimen name="dimension_20">20dp</dimen>
    <dimen name="dimension_21">21dp</dimen>
    <dimen name="dimension_23">23dp</dimen>
    <dimen name="dimension_24">24dp</dimen>
    <dimen name="dimension_25">25dp</dimen>
    <dimen name="dimension_28">28dp</dimen>
    <dimen name="dimension_30">30dp</dimen>
    <dimen name="dimension_32">32dp</dimen>
    <dimen name="dimension_34">34dp</dimen>
    <dimen name="dimension_35">35dp</dimen>
    <dimen name="dimension_36">36dp</dimen>
    <dimen name="dimension_38">38dp</dimen>
    <dimen name="dimension_40">40dp</dimen>
    <dimen name="dimension_41">41dp</dimen>
    <dimen name="dimension_43">43dp</dimen>
    <dimen name="dimension_39">39dp</dimen>
    <dimen name="dimension_42">42dp</dimen>
    <dimen name="dimension_44">44dp</dimen>
    <dimen name="dimension_45">45dp</dimen>
    <dimen name="dimension_47">47dp</dimen>
    <dimen name="dimension_48">48dp</dimen>
    <dimen name="dimension_50">50dp</dimen>
    <dimen name="dimension_52">52dp</dimen>
    <dimen name="dimension_55">55dp</dimen>
    <dimen name="dimension_60">60dp</dimen>
    <dimen name="dimension_65">65dp</dimen>
    <dimen name="dimension_70">70dp</dimen>
    <dimen name="dimension_75">75dp</dimen>
    <dimen name="dimension_77">77dp</dimen>
    <dimen name="dimension_80">80dp</dimen>
    <dimen name="dimension_85">85dp</dimen>
    <dimen name="dimension_90">90dp</dimen>
    <dimen name="dimension_95">95dp</dimen>
    <dimen name="dimension_100">100dp</dimen>
    <dimen name="dimension_120">120dp</dimen>
    <dimen name="dimension_135">135dp</dimen>
    <dimen name="dimension_140">140dp</dimen>
    <dimen name="dimension_160">160dp</dimen>
    <dimen name="dimension_165">165dp</dimen>
    <dimen name="dimension_170">170dp</dimen>
    <dimen name="dimension_150">150dp</dimen>
    <dimen name="dimension_180">180dp</dimen>
    <dimen name="dimension_200">200dp</dimen>
    <dimen name="dimension_220">220dp</dimen>
    <dimen name="dimension_250">250dp</dimen>
    <dimen name="dimension_260">260dp</dimen>
    <dimen name="dimension_290">290dp</dimen>
    <dimen name="dimension_300">300dp</dimen>
    <dimen name="dimension_px_half">0.5px</dimen>
    <dimen name="dimension_px_1">1px</dimen>
    <dimen name="dimension_dp_15">15dp</dimen>
    <dimen name="dimension_px_30">30px</dimen>
    <dimen name="dimension_px_100">100px</dimen>
    <dimen name="status_bar_height">0dp</dimen>
</resources>
app/src/main/res/values/ids.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
 Tencent is pleased to support the open source community by making QMUI_Android available.
 Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
 Licensed under the MIT License (the "License"); you may not use this file except in
 compliance with the License. You may obtain a copy of the License at
 http://opensource.org/licenses/MIT
 Unless required by applicable law or agreed to in writing, software distributed under the License is
 distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 either express or implied. See the License for the specific language governing permissions and
 limitations under the License.
-->
<resources>
    <item name="qmuidemo" type="id"/>
    <item name="main_container" type="id"/>
    <item name="empty_button" type="id"/>
    <item name="topbar_right_about_button" type="id"/>
    <item name="topbar_right_change_button" type="id"/>
    <item name="layout_for_test" type="id"/>
    <!--main-->
    <item name="ble_connec" type="id"/>
    <item name="ble_address" type="id"/>
    <!--address-->
    <item name="address_write_list" type="id"/>
    <item name="address_ble_name" type="id"/>
    <item name="address_ble_statu" type="id"/>
    <item name="address_ble_mac" type="id"/>
</resources>
app/src/main/res/values/strings.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
<resources>
    <string name="app_name">comb</string>
    <string name="action_settings">Settings</string>
    <!-- Strings used for fragments for navigation -->
    <string name="first_fragment_label">First Fragment</string>
    <string name="second_fragment_label">Second Fragment</string>
    <string name="next">Next</string>
    <string name="previous">Previous</string>
    <string name="hello_first_fragment">Hello first fragment</string>
    <string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
    <!-- Labels -->
    <string name="about_dialog_text">This is a sample application using the Bluetooth LE Library.\n\nGithub: https://github.com/alt236/Bluetooth-LE-Library---Android\n\nCopyright: Alexandros Schillings</string>
    <string name="header_device_info">Device Info</string>
    <string name="header_ibeacon_data">iBeacon Data</string>
    <string name="header_raw_ad_records">Raw Ad Records</string>
    <string name="header_rssi_info">RSSI Info</string>
    <string name="header_scan_record">Scan Record</string>
    <string name="label_advertisement">Advertisement:</string>
    <string name="label_as_array">As Array:</string>
    <string name="label_as_string">As String:</string>
    <string name="label_bluetooth_le_status">Bluetooth LE:</string>
    <string name="label_bluetooth_status">Bluetooth:</string>
    <string name="label_bluetooth_filter">Filter:</string>
    <string name="label_bluetooth_period">Period:</string>
    <string name="label_bonding_state">Bonding State:</string>
    <string name="label_company_id">Company ID:</string>
    <string name="label_data">Data:</string>
    <string name="label_desc">Desc:</string>
    <string name="label_device_address">Device address:</string>
    <string name="label_device_class">Device Class:</string>
    <string name="label_device_major_class">Major Class:</string>
    <string name="label_device_services">Services:</string>
    <string name="label_device_name">Device Name:</string>
    <string name="label_distance">Distance:</string>
    <string name="label_first_rssi">First RSSI:</string>
    <string name="label_first_timestamp">First Timestamp:</string>
    <string name="label_last_rssi">Last RSSI:</string>
    <string name="label_last_timestamp">Last Timestamp:</string>
    <string name="label_mac">MAC:</string>
    <string name="label_major">Major:</string>
    <string name="label_minor">Minor:</string>
    <string name="label_rssi">RSSI:</string>
    <string name="label_running_average_rssi">Running Average RSSI:</string>
    <string name="label_state">State:</string>
    <string name="label_tx_power">TX Power:</string>
    <string name="label_uuid">UUID:</string>
    <string name="label_updated">Updated:</string>
    <string name="label_decriptor">Descriptor:</string>
    <string name="label_record">Record:</string>
    <string name="connected">Connected</string>
    <string name="disconnected">Disconnected</string>
    <string name="invalid_device_data">Invalid Device Data!</string>
    <string name="no_data">No data</string>
    <string name="not_supported">Not supported</string>
    <string name="off">Off</string>
    <string name="period">scan:10s, pause:5s</string>
    <string name="on">On</string>
    <string name="supported">Supported</string>
    <string name="unknown">unknown</string>
    <string name="unknown_characteristic">Unknown characteristic</string>
    <string name="unknown_device">Unknown device</string>
    <string name="unknown_service">Unknown service</string>
    <string name="no_known_services">No known services</string>
    <!-- Formatters -->
    <string name="formatter_meters">%sm</string>
    <string name="formatter_db">%sdb</string>
    <string name="formatter_item_count">Items: %s</string>
</resources>
app/src/main/res/values/themes.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="AppTheme"  parent="QMUI.Compat.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:windowBackground">@color/gray_bg</item>
        <item name="QMUITopBarStyle">@style/QDTopBar</item>
        <item name="android:navigationBarColor">@android:color/transparent</item>
    </style>
    <!--SplashActivity Theme-->
    <style name="SplashTheme" parent="AppTheme">
        <item name="android:windowIsTranslucent">false</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowBackground">@color/colorPrimary</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:navigationBarColor">@color/colorPrimary</item>
        <item name="android:statusBarColor">@color/colorPrimary</item>
    </style>
    <style name="QDTopBar" parent="QMUI.TopBar">
        <item name="qmui_topbar_title_color">@color/qmui_config_color_white</item>
        <item name="qmui_topbar_subtitle_color">@color/qmui_config_color_white</item>
        <item name="qmui_topbar_height">48dp</item>
        <item name="qmui_topbar_title_text_size">15dp</item>
        <item name="qmui_topbar_image_btn_height">48dp</item>
    </style>
    <style name="tweetName">
        <item name="android:textColor">#48495f</item>
        <item name="android:textStyle">bold</item>
        <item name="android:textSize">18sp</item>
        <!--<item name="android:textStyle">bold</item>-->
        <item name="android:shadowColor">#44000000</item>
        <item name="android:shadowDx">1</item>
        <item name="android:shadowDy">1</item>
        <item name="android:shadowRadius">1</item>
    </style>
    <style name="tweetText">
        <item name="android:textSize">13sp</item>
        <item name="android:textColorLink">#888888</item>
        <item name="android:lineSpacingExtra">4dp</item>
    </style>
    <style name="tweetDate">
        <item name="android:textSize">10sp</item>
        <item name="android:textColor">#FFCCCCCC</item>
    </style>
    <style name="ButtonBase" parent="android:Widget.Holo.Light.Button"/>
    <style name="Button" parent="ButtonBase"/>
    <style name="CustomButton" parent="@style/Button">
        <item name="android:layout_height">40dp</item>
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_marginTop">10dp</item>
        <item name="android:layout_gravity">center</item>
        <item name="android:paddingLeft">?attr/qmui_content_spacing_horizontal</item>
        <item name="android:paddingRight">?attr/qmui_content_spacing_horizontal</item>
        <item name="android:background">@drawable/s_radius_button_bg</item>
        <item name="android:textColor">@color/s_app_color_blue_2</item>
        <item name="android:textSize">12sp</item>
        <item name="android:textStyle">bold</item>
        <item name="android:gravity">center</item>
    </style>
</resources>
app/src/main/res/values/values.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ParticleView"><attr format="string" name="pv_host_text"/><attr format="dimension" name="pv_host_text_size"/><attr format="string" name="pv_particle_text"/><attr format="dimension" name="pv_particle_text_size"/><attr format="color" name="pv_text_color"/><attr format="color" name="pv_background_color"/><attr format="integer" name="pv_text_anim_time"/><attr format="integer" name="pv_spread_anim_time"/><attr format="integer" name="pv_host_text_anim_time"/></declare-styleable>
</resources>
app/src/main/res/xml/backup_rules.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
   Sample backup rules file; uncomment and customize as necessary.
   See https://developer.android.com/guide/topics/data/autobackup
   for details.
   Note: This file is ignored for devices older that API 31
   See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
    <!--
   <include domain="sharedpref" path="."/>
   <exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
app/src/main/res/xml/data_extraction_rules.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
   Sample data extraction rules file; uncomment and customize as necessary.
   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
   for details.
-->
<data-extraction-rules>
    <cloud-backup>
        <!-- TODO: Use <include> and <exclude> to control what is backed up.
        <include .../>
        <exclude .../>
        -->
    </cloud-backup>
    <!--
    <device-transfer>
        <include .../>
        <exclude .../>
    </device-transfer>
    -->
</data-extraction-rules>
bluetooth-manager-lib/.gitignore
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
/build
bluetooth-manager-lib/build.gradle
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
apply plugin: 'com.android.library'
android {
    namespace "com.blakequ.bluetooth_manager_lib"
    compileSdkVersion 28
    defaultConfig {
        minSdk 21
        targetSdk 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    lintOptions {
        abortOnError false
    }
}
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    testImplementation 'junit:junit:4.12'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    api 'com.blankj:utilcodex:1.31.0'
}
bluetooth-manager-lib/proguard-rules.pro
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in D:\studio_sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}
bluetooth-manager-lib/src/main/AndroidManifest.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.blakequ.bluetooth_manager_lib">
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <application
        android:allowBackup="true">
    </application>
</manifest>
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/BleManager.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
package com.blakequ.bluetooth_manager_lib;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import android.util.Log;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothConnectManager;
import com.blakequ.bluetooth_manager_lib.connect.ConnectConfig;
import com.blakequ.bluetooth_manager_lib.connect.multiple.MultiConnectManager;
import com.blakequ.bluetooth_manager_lib.scan.BluetoothScanManager;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/17 11:02 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description: scan and connect ble device
 */
@TargetApi(18)
public final class BleManager {
    private static BleParamsOptions configOptions;
    private BleManager(){
    }
    public static BleParamsOptions getBleParamsOptions(){
        if (configOptions == null){
            setBleParamsOptions(BleParamsOptions.createDefault());
        }
        return configOptions;
    }
    /**
     * set config of ble connect and scan
     * @param options
     */
    public static void setBleParamsOptions(@NonNull BleParamsOptions options){
        if (options != null){
            configOptions = options;
            ConnectConfig.maxConnectDeviceNum = options.getMaxConnectDeviceNum();
        }
    }
    /**
     * get connect manager, only connect one device
     * @param context
     * @return
     * @see #getMultiConnectManager(Context)
     */
    public static BluetoothConnectManager getConnectManager(@NonNull Context context){
        return BluetoothConnectManager.getInstance(context);
    }
    /**
     * get scan bluetooth manager
     * @param context
     * @return
     */
    public static BluetoothScanManager getScanManager(@NonNull Context context){
        return BluetoothScanManager.getInstance(context);
    }
    /**
     * get multi bluetooth device connect manager
     * @param context
     * @return
     * @see #getConnectManager(Context)
     */
    public static MultiConnectManager getMultiConnectManager(@NonNull Context context){
        return MultiConnectManager.getInstance(context);
    }
    /**
     * Check if Bluetooth LE is supported by this Android device, and if so, make sure it is enabled.
     *
     * @return false if it is supported and not enabled
     * @throws BleNotAvailableException if Bluetooth LE is not supported.  (Note: The Android emulator will do this)
     */
    public static boolean checkAvailability(Context context) throws BleNotAvailableException {
        if (isSDKAvailable()) {
            if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
                throw new BleNotAvailableException("Bluetooth LE not supported by this device");
            } else {
                if (((BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter().isEnabled()) {
                    return true;
                }
            }
        }
        return false;
    }
    public static boolean isSDKAvailable(){
        if (android.os.Build.VERSION.SDK_INT < 18) {
            throw new BleNotAvailableException("Bluetooth LE not supported by this device");
        }
        return true;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/BleNotAvailableException.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
/**
 * Radius Networks, Inc.
 * http://www.radiusnetworks.com
 *
 * @author David G. Young
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.blakequ.bluetooth_manager_lib;
/**
 * Indicates that Bluetooth Low Energy is not available on this device
 * @see ScanManager#checkAvailability()
 * @author David G. Young
 *
 */
public class BleNotAvailableException extends RuntimeException {
    private static final long serialVersionUID = 2242747823097637729L;
    public BleNotAvailableException(String message) {
            super(message);
        }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/BleParamsOptions.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,285 @@
package com.blakequ.bluetooth_manager_lib;
import androidx.annotation.IntRange;
import com.blakequ.bluetooth_manager_lib.connect.ConnectConfig;
import static com.blakequ.bluetooth_manager_lib.scan.BackgroundPowerSaver.DEFAULT_BACKGROUND_BETWEEN_SCAN_PERIOD;
import static com.blakequ.bluetooth_manager_lib.scan.BackgroundPowerSaver.DEFAULT_BACKGROUND_SCAN_PERIOD;
import static com.blakequ.bluetooth_manager_lib.scan.BackgroundPowerSaver.DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD;
import static com.blakequ.bluetooth_manager_lib.scan.BackgroundPowerSaver.DEFAULT_FOREGROUND_SCAN_PERIOD;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/12/1 14:28 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description: config of ble connect and scan
 */
public class BleParamsOptions {
    private final boolean isDebugMode;
    //using for scan
    private final long foregroundScanPeriod;
    private final long foregroundBetweenScanPeriod;
    private final long backgroundScanPeriod;
    private final long backgroundBetweenScanPeriod;
    //using for connect
    private final int maxConnectDeviceNum; //一次最大连接设备个数
    private final int reconnectStrategy; //重连策略
    private final int reconnectMaxTimes; //最大重连次数
    private final long reconnectBaseSpaceTime; //重连基础时间间隔ms
    private final int reconnectedLineToExponentTimes; //快速重连的次数(线性到指数)
    private final int connectTimeOutTimes; //连接超时时间
    public boolean isDebugMode() {
        return isDebugMode;
    }
    public long getForegroundScanPeriod() {
        return foregroundScanPeriod;
    }
    public long getForegroundBetweenScanPeriod() {
        return foregroundBetweenScanPeriod;
    }
    public long getBackgroundScanPeriod() {
        return backgroundScanPeriod;
    }
    public long getBackgroundBetweenScanPeriod() {
        return backgroundBetweenScanPeriod;
    }
    public int getMaxConnectDeviceNum() {
        return maxConnectDeviceNum;
    }
    public int getReconnectStrategy() {
        return reconnectStrategy;
    }
    public int getReconnectMaxTimes() {
        return reconnectMaxTimes;
    }
    public long getReconnectBaseSpaceTime() {
        return reconnectBaseSpaceTime;
    }
    public int getReconnectedLineToExponentTimes() {
        return reconnectedLineToExponentTimes;
    }
    public int getConnectTimeOutTimes() {
        return connectTimeOutTimes;
    }
    private BleParamsOptions(Builder builder){
        this.isDebugMode = builder.isDebugMode;
        this.foregroundScanPeriod = builder.foregroundScanPeriod;
        this.foregroundBetweenScanPeriod = builder.foregroundBetweenScanPeriod;
        this.backgroundScanPeriod = builder.backgroundScanPeriod;
        this.backgroundBetweenScanPeriod = builder.backgroundBetweenScanPeriod;
        this.maxConnectDeviceNum = builder.maxConnectDeviceNum;
        this.reconnectStrategy = builder.reconnectStrategy;
        this.reconnectMaxTimes = builder.reconnectMaxTimes;
        this.reconnectBaseSpaceTime = builder.reconnectBaseSpaceTime;
        this.reconnectedLineToExponentTimes = builder.reconnectedLineToExponentTimes;
        this.connectTimeOutTimes = builder.connectTimeOutTimes;
    }
    public static class Builder {
        private boolean isDebugMode = true;
        private long foregroundScanPeriod = DEFAULT_FOREGROUND_SCAN_PERIOD;
        private long foregroundBetweenScanPeriod = DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD;
        private long backgroundScanPeriod = DEFAULT_BACKGROUND_SCAN_PERIOD;
        private long backgroundBetweenScanPeriod = DEFAULT_BACKGROUND_BETWEEN_SCAN_PERIOD;
        private int maxConnectDeviceNum = 5;//一次最大连接设备个数
        private int reconnectStrategy = ConnectConfig.RECONNECT_LINE_EXPONENT; //重连策略
        private int reconnectMaxTimes = Integer.MAX_VALUE; //最大重连次数
        private long reconnectBaseSpaceTime = 8000; //重连基础时间间隔ms
        private int reconnectedLineToExponentTimes = 5; //快速重连的次数(线性到指数)
        private int connectTimeOutTimes = 15000; //连接超时时间15s
        /**
         * setting is debug mode, if false then the log will disable
         * @param isDebugMode you can set by BuildConfig.DEBUG, default is true
         * @return
         */
        public Builder setDebugMode(boolean isDebugMode){
            this.isDebugMode = isDebugMode;
            return this;
        }
        /**
         * Sets the duration in milliseconds of each Bluetooth LE scan cycle to look for beacons.
         * This function is used to setup the period when switching
         * between background/foreground. To have it effect on an already running scan (when the next
         * cycle starts)
         *
         * @param foregroundScanPeriod defalut is 10 seconds, you should using milliseconds
         * @return
         */
        public Builder setForegroundScanPeriod(@IntRange(from = 0) long foregroundScanPeriod) {
            if (foregroundScanPeriod < 0){
                throw new IllegalArgumentException("Period time must > 0, now is "+foregroundScanPeriod);
            }
            this.foregroundScanPeriod = foregroundScanPeriod;
            return this;
        }
        /**
         * Sets the duration in milliseconds between each Bluetooth LE scan cycle to look for beacons.
         * This function is used to setup the period when switching
         * between background/foreground. To have it effect on an already running scan (when the next
         * cycle starts)
         * @param foregroundBetweenScanPeriod defalut is 5 seconds, you should using milliseconds
         * @return
         */
        public Builder setForegroundBetweenScanPeriod(@IntRange(from = 0) long foregroundBetweenScanPeriod) {
            if (foregroundBetweenScanPeriod < 0){
                throw new IllegalArgumentException("Period time must > 0, now is "+foregroundBetweenScanPeriod);
            }
            this.foregroundBetweenScanPeriod = foregroundBetweenScanPeriod;
            return this;
        }
        /**
         * Sets the duration in milliseconds of each Bluetooth LE scan cycle to look for beacons.
         * This function is used to setup the period when switching
         * between background/foreground. To have it effect on an already running scan (when the next
         * cycle starts)
         * @param backgroundScanPeriod default is 10 seconds, you should using milliseconds
         * @return
         */
        public Builder setBackgroundScanPeriod(@IntRange(from = 0) long backgroundScanPeriod) {
            if (backgroundScanPeriod < 0){
                throw new IllegalArgumentException("Period time must > 0, now is "+backgroundScanPeriod);
            }
            this.backgroundScanPeriod = backgroundScanPeriod;
            return this;
        }
        /**
         * Sets the duration in milliseconds spent not scanning between each Bluetooth LE scan cycle when no ranging/monitoring clients are in the foreground
         * @param backgroundBetweenScanPeriod default is 5 minutes, you should using milliseconds
         * @return
         */
        public Builder setBackgroundBetweenScanPeriod(@IntRange(from = 0) long backgroundBetweenScanPeriod) {
            if (backgroundBetweenScanPeriod < 0){
                throw new IllegalArgumentException("Period time must > 0, now is "+backgroundBetweenScanPeriod);
            }
            this.backgroundBetweenScanPeriod = backgroundBetweenScanPeriod;
            return this;
        }
        /**
         * max number of connect ble device
         * @param maxConnectDeviceNum default is 5
         * @return
         */
        public Builder setMaxConnectDeviceNum(@IntRange(from = 1) int maxConnectDeviceNum) {
            if (maxConnectDeviceNum < 1){
                throw new IllegalArgumentException("Connect device number must > 1, now is "+maxConnectDeviceNum);
            }
            this.maxConnectDeviceNum = maxConnectDeviceNum;
            return this;
        }
        /**
         * how to reconnect, you have choose reconnectStrategy:
         * <ol>
         * <li>1. ConnectConfig.RECONNECT_LINEAR, each reconnection interval time is the same</li>
         * <li>2. ConnectConfig.RECONNECT_EXPONENT, the reconnect time interval is exponential growth</li>
         * <li>3. ConnectConfig.RECONNECT_LINE_EXPONENT, Start time interval is the same, after reconnectedLineToExponentTimes times then use exponential growth<li/>
         * </ol>
         * @param reconnectStrategy
         * @return
         */
        public Builder setReconnectStrategy(@IntRange(from = 1, to = 4) int reconnectStrategy) {
            if (reconnectStrategy < 1 || reconnectStrategy > 4){
                throw new IllegalArgumentException("reconnectStrategy range is 1 to 4");
            }
            this.reconnectStrategy = reconnectStrategy;
            return this;
        }
        /**
         * usable only the reconnect strategy is ConnectConfig.RECONNECT_LINE_EXPONENT
         * @param reconnectedLineToExponentTimes the times from linear to exponential, default is 5 times
         * @return
         */
        public Builder setReconnectedLineToExponentTimes(@IntRange(from = 1) int reconnectedLineToExponentTimes) {
            if (reconnectedLineToExponentTimes < 1){
                throw new IllegalArgumentException("reconnectedLineToExponentTimes value must >= 1, now is "+reconnectedLineToExponentTimes);
            }
            this.reconnectedLineToExponentTimes = reconnectedLineToExponentTimes;
            return this;
        }
        /**
         * max reconnect times, if you set 0 will not reconnect
         * @param reconnectMaxTimes default is Integer.MAX_VALUE
         * @return
         */
        public Builder setReconnectMaxTimes(@IntRange(from = 0) int reconnectMaxTimes) {
            if (reconnectMaxTimes < 0){
                throw new IllegalArgumentException("Reconnect max times must > 0, now is "+reconnectMaxTimes);
            }
            this.reconnectMaxTimes = reconnectMaxTimes;
            return this;
        }
        /**
         * reconnect interval time(>=1000ms)
         * @param reconnectBaseSpaceTime default is 8 seconds
         * @return
         */
        public Builder setReconnectBaseSpaceTime(@IntRange(from = 1000) long reconnectBaseSpaceTime) {
            if (reconnectBaseSpaceTime < 1000){
                throw new IllegalArgumentException("reconnectBaseSpaceTime must >= 1000ms, now is "+reconnectBaseSpaceTime);
            }
            this.reconnectBaseSpaceTime = reconnectBaseSpaceTime;
            return this;
        }
        /**
         * time out of connect device(after this time will check bluetooth state, if bluetooth not available will close all connect)
         * @param connectTimeOutTimes default is 15 seconds(must > 1 seconds)
         * @return
         */
        public Builder setConnectTimeOutTimes(@IntRange(from = 1000) int connectTimeOutTimes){
            if (connectTimeOutTimes < 1000){
                throw new IllegalArgumentException("connectTimeOutTimes must >= 1000ms, now is "+connectTimeOutTimes);
            }
            this.connectTimeOutTimes = connectTimeOutTimes;
            return this;
        }
        /** Builds configured {@link BleParamsOptions} object */
        public BleParamsOptions build() {
            return new BleParamsOptions(this);
        }
    }
    public static BleParamsOptions createDefault() {
        return new Builder().build();
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/BluetoothConnectInterface.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,398 @@
package com.blakequ.bluetooth_manager_lib.connect;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import com.blakequ.bluetooth_manager_lib.util.BluetoothUtils;
import com.blankj.utilcode.util.LogUtils;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Queue;
import java.util.UUID;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/19 11:16 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description:bluetooth connect interface
 */
public abstract class BluetoothConnectInterface {
    protected static final String TAG = "BluetoothConnectInterface";
    protected Context context;
    private BluetoothOperatorQueue mOpratorQueue;
    private Handler mHandler = new Handler(Looper.getMainLooper());
    public BluetoothConnectInterface(Context context){
        this.context = context;
        mOpratorQueue = new BluetoothOperatorQueue();
    }
    /**
     * release resource
     */
    public abstract void release();
    /**
     * get gatt connect callback
     * @return
     */
    protected abstract BluetoothGattCallback getBluetoothGattCallback();
    protected abstract String getServiceUUID();
    /**
     * get device gatt service, if not will return null
     * @param address
     * @return null if not find gatt service
     */
    public abstract BluetoothGatt getBluetoothGatt(String address);
    /**
     * get the list of subscribe
     * @return
     */
    protected abstract Queue<BluetoothSubScribeData> getSubscribeDataQueue();
    /**
     * invoke when bluetooth disconnect
     * @param gatt
     */
    protected abstract void onDeviceDisconnect(BluetoothGatt gatt, int errorState);
    /**
     * invoke when bluetooth connected
     * @param gatt
     */
    protected abstract void onDeviceConnected(BluetoothGatt gatt);
    /**
     * invoke when fail to discover service
     * @param gatt
     */
    protected abstract void onDiscoverServicesFail(BluetoothGatt gatt);
    /**
     * invoke when success to discover service
     * @param gatt
     */
    protected abstract void onDiscoverServicesSuccess(BluetoothGatt gatt);
    /**
     * Runs the specified action on the UI thread. If the current thread is the UI
     * thread, then the action is executed immediately. If the current thread is
     * not the UI thread, the action is posted to the event queue of the UI thread.
     *
     * @param action the action to run on the UI thread
     */
    public final void runOnUiThread(Runnable action) {
        if (Thread.currentThread() != Looper.getMainLooper().getThread()) {
            mHandler.post(action);
        } else {
            action.run();
        }
    }
    protected Handler getMainLooperHandler(){
        return mHandler;
    }
    protected BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
            if (getBluetoothGattCallback() != null) getBluetoothGattCallback().onCharacteristicChanged(gatt, characteristic);
        }
        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicRead(gatt, characteristic, status);
            LogUtils.i("onCharacteristicRead data status:" + GattError.parseConnectionError(status) + " " + characteristic.getUuid().toString());
            mOpratorQueue.nextOperator();
            if (getBluetoothGattCallback() != null) getBluetoothGattCallback().onCharacteristicRead(gatt, characteristic, status);
        }
        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
            LogUtils.i("onCharacteristicWrite write status:" + GattError.parseConnectionError(status));
            mOpratorQueue.nextOperator();
            if (getBluetoothGattCallback() != null) getBluetoothGattCallback().onCharacteristicWrite(gatt, characteristic, status);
        }
        @Override
        public void onConnectionStateChange(final BluetoothGatt gatt, int status, final int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            //status=133是GATT_ERROR错误http://stackoverflow.com/questions/25330938/android-bluetoothgatt-status-133-register-callback
            //http://www.loverobots.cn/android-ble-connection-solution-bluetoothgatt-status-133.html
            LogUtils.i("onConnectionStateChange gattStatus=" + GattError.parseConnectionError(status) + " newStatus="
                    + (newState == BluetoothProfile.STATE_CONNECTED ? "CONNECTED" : "DISCONNECTED"));
            //不同的手机当蓝牙关闭,设备断开(重启,远离)返回的状态不一样,newState都一样是DISCONNECTED,设备切换不会产生影响
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {//调用connect会调用
                    LogUtils.i("Connected to GATT server");
                    // Attempts to discover services after successful connection.
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            onDeviceConnected(gatt);
                            if (gatt != null && !gatt.discoverServices()) {
                                LogUtils.e("onConnectionStateChange start service discovery fail! Thread:" + Thread.currentThread());
                            }
                        }
                    });
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {//调用disconnect会调用,设备断开或蓝牙关闭会进入
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            onDeviceDisconnect(gatt, newState);
                        }
                    });
                }
            } else{ //调用connect和disconnect出错后会进入,设备断开或蓝牙关闭会进入
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        onDeviceDisconnect(gatt, newState);
                    }
                });
            }
            if (getBluetoothGattCallback() != null) getBluetoothGattCallback().onConnectionStateChange(gatt, status, newState);
        }
        @Override
        public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorRead(gatt, descriptor, status);
            LogUtils.i("onDescriptorRead status=" + GattError.parseConnectionError(status));
            mOpratorQueue.nextOperator();
            if (getBluetoothGattCallback() != null) getBluetoothGattCallback().onDescriptorRead(gatt, descriptor, status);
        }
        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorWrite(gatt, descriptor, status);
            LogUtils.i("onDescriptorWrite status=" + GattError.parseConnectionError(status));
            mOpratorQueue.nextOperator();
            if (getBluetoothGattCallback() != null) getBluetoothGattCallback().onDescriptorWrite(gatt, descriptor, status);
        }
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
            super.onMtuChanged(gatt, mtu, status);
            if (getBluetoothGattCallback() != null) getBluetoothGattCallback().onMtuChanged(gatt, mtu, status);
        }
        @Override
        public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
            super.onReadRemoteRssi(gatt, rssi, status);
            if (getBluetoothGattCallback() != null) getBluetoothGattCallback().onReadRemoteRssi(gatt, rssi, status);
        }
        @Override
        public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
            super.onReliableWriteCompleted(gatt, status);
            if (getBluetoothGattCallback() != null) getBluetoothGattCallback().onReliableWriteCompleted(gatt, status);
        }
        @Override
        public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
            LogUtils.i("onServicesDiscovered status=" + GattError.parseConnectionError(status));
            if (status == BluetoothGatt.GATT_SUCCESS) {
                //start subscribe data
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        onDiscoverServicesSuccess(gatt);
                        if (gatt != null){
                            startSubscribe(gatt);
                        }
                    }
                });
            }else {
                LogUtils.e("onServicesDiscovered fail!");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        onDiscoverServicesFail(gatt);
                    }
                });
            }
            if (getBluetoothGattCallback() != null) getBluetoothGattCallback().onServicesDiscovered(gatt, status);
        }
    };
    /**
     * start subscribe data, add data to subscribe list before invoke this method
     * <p>You should invoke this method after onServicesDiscovered, otherwise can not find service<p/>
     * @param bluetoothGatt
     * @return boolean is success start read or write character
     */
    public boolean startSubscribe(BluetoothGatt bluetoothGatt){
        if (bluetoothGatt == null){
            LogUtils.e("Fail to subscribe, BluetoothGatt is null");
            return false;
        }
        boolean isSuccess = subscribe(bluetoothGatt.getDevice().getAddress());
        mOpratorQueue.start(bluetoothGatt);
        return isSuccess;
    }
    /**
     * è®¢é˜…蓝牙设备通知及读写数据
     * @return
     */
    protected boolean subscribe(String address){
        BluetoothGatt mBluetoothGatt = getBluetoothGatt(address);
        if (mBluetoothGatt == null){
            LogUtils.e("can not subscribe to ble device info "+address);
            return false;
        }
        mOpratorQueue.clean();
        if (isEmpty(getServiceUUID())){
            LogUtils.e("Service UUID is null");
            return false;
        }
        //check subscribe list
        if (getSubscribeDataQueue() == null && getSubscribeDataQueue().size() > 0){
            LogUtils.e("Subscribe BLE data is null, you must invoke addBluetoothSubscribeData to add data");
            return false;
        }
        BluetoothGattService gattService = mBluetoothGatt.getService(UUID.fromString(getServiceUUID()));
        if (gattService != null){
            for (BluetoothSubScribeData data:getSubscribeDataQueue()){
                final BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(data.getCharacteristicUUID());
                if (characteristic != null){
                    switch (data.getOperatorType()){
                        case CHAR_WIRTE:
                            if (BluetoothUtils.isCharacteristicWrite(characteristic.getProperties())){
                                characteristic.setValue(data.getCharacteristicValue());
                                mOpratorQueue.addOperator(characteristic, true);
                            }else{
                                LogUtils.e("Fail to write characteristic, not have write property , uuid:"+characteristic.getUuid()+" ,property:"+characteristic.getProperties());
                            }
                            break;
                        case CHAR_READ:
                            //bug fix:samsung phone bug, can not read value
                            if (checkIsSamsung()){
                                setProperty(characteristic);
                            }
                            if(BluetoothUtils.isCharacteristicRead(characteristic.getProperties())){
                                mOpratorQueue.addOperator(characteristic, false);
                            }else{
                                LogUtils.e("Fail to read characteristic, not have read property , uuid:" + characteristic.getUuid() + " ,property:" + characteristic.getProperties());
                            }
                            break;
                        case DESC_READ:
                            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(data.getDescriptorUUID());
                            if (descriptor != null){
                                mOpratorQueue.addOperator(descriptor, false);
                            }else {
                                LogUtils.e("Fail to get descriptor read uuid:"+data.getDescriptorUUID());
                            }
                            break;
                        case DESC_WRITE:
                            BluetoothGattDescriptor descriptor2 = characteristic.getDescriptor(data.getDescriptorUUID());
                            if (descriptor2 != null){
                                descriptor2.setValue(data.getDescriptorValue());
                                mOpratorQueue.addOperator(descriptor2, true);
                            }else {
                                LogUtils.e("Fail to get descriptor write uuid:"+data.getDescriptorUUID());
                            }
                            break;
                        case NOTIFY:
                            if(BluetoothUtils.isCharacteristicNotify(characteristic.getProperties())){
                                mBluetoothGatt.setCharacteristicNotification(characteristic, true);
                                BluetoothGattDescriptor descriptor3 = characteristic.getDescriptor(data.getDescriptorUUID());
                                if (descriptor3 != null){
                                    if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0){
                                        descriptor3.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                                    }else if((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0){
                                        //两个都是通知的意思,notify和indication的区别在于,notify只是将你要发的数据发送给手机,没有确认机制,
                                        //不会保证数据发送是否到达。而indication的方式在手机收到数据时会主动回一个ack回来。即有确认机制,只有收
                                        //到这个ack你才能继续发送下一个数据。这保证了数据的正确到达,也起到了流控的作用。所以在打开通知的时候,需要设置一下。
                                        descriptor3.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
                                    }
                                    mOpratorQueue.addOperator(descriptor3, true);
                                }else {
                                    LogUtils.e("Fail to get notify descriptor uuid:"+data.getDescriptorUUID());
                                }
                            }else{
                                LogUtils.e("Fail to notify characteristic, not have notify property , uuid:" + characteristic.getUuid() + " ,property:" + characteristic.getProperties());
                            }
                            break;
                    }
                }else {
                    LogUtils.e("Fail to get characteristic service uuid:"+data.getCharacteristicUUID());
                }
            }
        }else {
            LogUtils.e("Can not get gatt service uuid:"+getServiceUUID());
            return false;
        }
        return true;
    }
    /**
     * è®¾ç½®å±žæ€§,设置读权限
     * @param flagReadChar
     */
    @SuppressLint("SoonBlockedPrivateApi")
    private void setProperty(BluetoothGattCharacteristic flagReadChar){
        Field properField = null;
        try {
            properField = flagReadChar.getClass().getDeclaredField("mProperties");
            properField.setAccessible(true);
            properField.set(flagReadChar, 10);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    /**
     * åˆ¤æ–­æ‰‹æœºç±»åž‹
     * @return
     */
    protected boolean checkIsSamsung() {
        String brand = android.os.Build.BRAND;
        if (brand.toLowerCase().equals("samsung")) {
            return true;
        }
        return false;
    }
    public boolean isEmpty(String str) {
        return str == null || str.length() == 0;
    }
    public <V> boolean isEmpty(List<V> sourceList) {
        return sourceList == null || sourceList.size() == 0;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/BluetoothConnectManager.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,475 @@
package com.blakequ.bluetooth_manager_lib.connect;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Looper;
import android.os.SystemClock;
import com.blakequ.bluetooth_manager_lib.BleManager;
import com.blakequ.bluetooth_manager_lib.BleParamsOptions;
import com.blakequ.bluetooth_manager_lib.util.BluetoothUtils;
import com.blankj.utilcode.util.LogUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/18 11:29 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description: ç”¨äºŽå½“前蓝牙的连接管理,负责连接的建立断开,使用时需要设置回调{@link #setBluetoothGattCallback(BluetoothGattCallback)},
 * è¿žæŽ¥{@link #connect(String)},断开连接{@link #disconnect(String)}, å…³é—­è¿žæŽ¥{@link #close(String)}.
 * æ³¨æ„ï¼š<br>
 * 1.在进行蓝牙断开和连接调用的时候,需要在主线程执行,否则在三星手机会出现许多异常错误或无法连接的情况<br>
 * 2.该连接管理只能连接一个设备,不支持同时连接多个设备<br>
 * 3.可以自定义断开后重连次数和重连的间隔时间<br>
 * 4.如果要订阅服务数据(read,write,notify)Service UUID {@link #setServiceUUID(String)}, å¦åˆ™ä¸èƒ½è‡ªåŠ¨çš„è¿›è¡Œé€šçŸ¥å’Œchar和desc的读写操作(还需要{@link #addBluetoothSubscribeData(BluetoothSubScribeData)})<br>
 * 5.单独订阅数据,需要调用{@link #cleanSubscribeData()}清除订阅历史列表, {@link #addBluetoothSubscribeData(BluetoothSubScribeData)}添加参数, {@link #startSubscribe(BluetoothGatt)}启动订阅,会自动回调{@link #setBluetoothGattCallback(BluetoothGattCallback)}订阅结果
 */
@TargetApi(18)
public final class BluetoothConnectManager extends BluetoothConnectInterface{
    private static final String TAG = "BluetoothConnectManager";
    private static BluetoothConnectManager INSTANCE = null;
    private final BluetoothUtils mBluetoothUtils;
    private BluetoothGattCallback mBluetoothGattCallback;
    private BluetoothManager bluetoothManager;
    private final Map<String, BluetoothGatt> gattMap; //保存连接过的gatt
    private final Queue<BluetoothSubScribeData> subscribeQueue;
    private static String serviceUUID;
    private ReconnectParamsBean reconnectParamsBean;
    private List<ConnectStateListener> connectStateListeners;
    private ConnectState currentState = ConnectState.NORMAL;
    private static Object obj = new Object();
    public BluetoothConnectManager(Context context) {
        super(context);
        subscribeQueue = new ConcurrentLinkedQueue<BluetoothSubScribeData>();
        mBluetoothUtils = BluetoothUtils.getInstance(context);
        bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
        gattMap = new ConcurrentHashMap<String, BluetoothGatt>(); //会有并发的断开和连接,故而必须使用并发ConcurrentHashMap才行,否则会有ConcurrentModificationException
        connectStateListeners = new ArrayList<>();
        BleManager.getBleParamsOptions();
    }
    @Override
    public void release() {
        closeAll();
        gattMap.clear();
        reconnectParamsBean = null;
    }
    public static BluetoothConnectManager getInstance(Context context){
        if (INSTANCE == null){
            synchronized (obj){
                if (INSTANCE == null){
                    INSTANCE = new BluetoothConnectManager(context);
                }
            }
        }
        return INSTANCE;
    }
    /**
     * add callback of gatt connect, notice:<br>
     * 1. can not do any task which need a lot of time<br>
     * 2. you should update UI in the main thread, in callback method use {@link #runOnUiThread(Runnable)}
     * @param callback
     * @see #runOnUiThread(Runnable)
     */
    public void setBluetoothGattCallback(BluetoothGattCallback callback){
        this.mBluetoothGattCallback = callback;
    }
    /**
     * add listener of connect state
     * @param listener
     * @see #removeConnectStateListener(ConnectStateListener)
     */
    public void addConnectStateListener(ConnectStateListener listener){
        synchronized(connectStateListeners){
            connectStateListeners.add(listener);
        }
    }
    /**
     * remove listener
     * @param listener
     * @see #addBluetoothSubscribeData(BluetoothSubScribeData)
     */
    public void removeConnectStateListener(ConnectStateListener listener){
        synchronized(connectStateListeners){
            connectStateListeners.remove(listener);
        }
    }
    /**
     * add subscribe data while auto read or write characteristic(or descriptor) after discover service, you can clean subscribe list by {@link #cleanSubscribeData()}
     * @param data
     * @see #cleanSubscribeData()
     * @see #startSubscribe(BluetoothGatt)
     * @see #setServiceUUID(String)
     */
    public void addBluetoothSubscribeData(BluetoothSubScribeData data){
        subscribeQueue.add(data);
    }
    /**
     * clean subscribe list
     * @see #addBluetoothSubscribeData(BluetoothSubScribeData)
     */
    public void cleanSubscribeData(){
        subscribeQueue.clear();
    }
    /**
     * set bluetooth service uuid, can not be null
     * @see #addBluetoothSubscribeData(BluetoothSubScribeData)
     * @param serviceUUID
     */
    public void setServiceUUID(String serviceUUID){
        this.serviceUUID = serviceUUID;
    }
    @Override
    public BluetoothGatt getBluetoothGatt(String address){
        if (!isEmpty(address) && gattMap.containsKey(address)){
            return gattMap.get(address);
        }
        return null;
    }
    @Override
    protected BluetoothGattCallback getBluetoothGattCallback() {
        return mBluetoothGattCallback;
    }
    @Override
    protected String getServiceUUID() {
        return serviceUUID;
    }
    @Override
    protected void onDeviceDisconnect(final BluetoothGatt gatt, int errorState) {
        //is bluetooth enable
        //可以不关闭,以便重用,因为在连接connect的时候可以快速连接
        if (!checkIsSamsung() || !mBluetoothUtils.isBluetoothIsEnable()){//三星手机断开后直接连接
            LogUtils.e( "Disconnected from GATT server address:"+gatt.getDevice().getAddress());
            close(gatt.getDevice().getAddress()); //防止出现status 133
        }else {
            updateConnectStateListener(gatt.getDevice().getAddress(), ConnectState.NORMAL);
        }
        //if disconnect by hand, so not run reconnect device
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                reconnectDevice(gatt.getDevice().getAddress()); //如果设备断开则指定时间后尝试重新连接,重新连接
            }
        });
    }
    @Override
    protected void onDeviceConnected(BluetoothGatt gatt) {
        updateConnectStateListener(gatt.getDevice().getAddress(), ConnectState.CONNECTED);
        reconnectParamsBean = null;
    }
    @Override
    protected void onDiscoverServicesFail(final BluetoothGatt gatt) {
        if (!checkIsSamsung() || !mBluetoothUtils.isBluetoothIsEnable()){//三星手机断开后直接连接
            LogUtils.e( "Disconnected from GATT server address:"+gatt.getDevice().getAddress());
            close(gatt.getDevice().getAddress()); //防止出现status 133
        }else {
            updateConnectStateListener(gatt.getDevice().getAddress(), ConnectState.NORMAL);
        }
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                reconnectDevice(gatt.getDevice().getAddress());
            }
        });
    }
    public void updateReconnectParams(){
        reconnectParamsBean = null;
    }
    @Override
    protected void onDiscoverServicesSuccess(BluetoothGatt gatt) {
    }
    @Override
    protected Queue<BluetoothSubScribeData> getSubscribeDataQueue() {
        return subscribeQueue;
    }
    /**
     * has device is connected or connecting
     * @return
     */
    public boolean isConnectDevice(){
        if (gattMap.size() == 0) return false;
        return true;
    }
    /**
     * èŽ·å–å·²ç»è¿žæŽ¥çš„è®¾å¤‡,注意:返回的设备不一定全部是当前APP所连接的设备,需要通过UUID或设备名字等区分
     * @return
     */
    public List<BluetoothDevice> getConnectedDevice(){
        if (isConnectDevice()){
            return bluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
        }
        return Collections.EMPTY_LIST;
    }
    public ConnectState getCurrentState(){
        return currentState;
    }
    /**
     * Connects to the GATT server hosted on the Bluetooth LE device. ä¸ºä¿è¯åªæœ‰ä¸€ä¸ªè¿žæŽ¥ï¼Œå½“连接创建或初始化成功则会强制关闭其他连接
     *
     * @param address The device address of the destination device.
     * @return Return true if the connection is initiated successfully. The connection result
     * is reported asynchronously through the
     * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
     * callback.
     */
    public boolean connect(final String address) {
        BluetoothAdapter mAdapter = mBluetoothUtils.getBluetoothAdapter();
        if (mAdapter == null || address == null) {
            LogUtils.w( "BluetoothAdapter not initialized or unspecified address.");
            updateConnectStateListener(address, ConnectState.NORMAL);
            return false;
        }
        if (!mBluetoothUtils.isBluetoothIsEnable()){
            LogUtils.e( "bluetooth is not enable.");
            updateConnectStateListener(address, ConnectState.NORMAL);
            return false;
        }
        // Previously connected device.  Try to reconnect.
        if (gattMap.containsKey(address)){
            BluetoothGatt mBluetoothGatt = gattMap.get(address);
            LogUtils.i( "Trying to use an existing gatt and reconnection device " + address + " thread:" + (Thread.currentThread() == Looper.getMainLooper().getThread()));
            if (mBluetoothGatt.connect()) {
                closeOtherDevice(address);
                updateConnectStateListener(address, ConnectState.CONNECTING);
                return true;
            } else {
                close(address);
                return false;
            }
        }
        BluetoothDevice device = mAdapter.getRemoteDevice(address);
        if (device != null){
             /*if We want to directly connect to the device, we can setting the autoConnect
             parameter to false.*/
            BluetoothGatt mBluetoothGatt = device.connectGatt(context, false, gattCallback);
            if (mBluetoothGatt != null){
                LogUtils.d( "create a new connection address=" + address + " thread:" + (Thread.currentThread() == Looper.getMainLooper().getThread()));
                gattMap.put(address, mBluetoothGatt);
                closeOtherDevice(address);
                updateConnectStateListener(address, ConnectState.CONNECTING);
                return true;
            }else{
                LogUtils.e( "Get Gatt fail!, address=" + address + " thread:" + (Thread.currentThread() == Looper.getMainLooper().getThread()));
            }
        }else{
            LogUtils.e( "Device not found, address=" + address);
        }
        updateConnectStateListener(address, ConnectState.NORMAL);
        return false;
    }
    /**
     * close bluetooth, release resource
     * @param address
     */
    public boolean close(String address) {
        if (!isEmpty(address) && gattMap.containsKey(address)){
            LogUtils.w("close gatt server " + address);
            BluetoothGatt mBluetoothGatt = gattMap.get(address);
            mBluetoothGatt.close();
            gattMap.remove(address);
            updateConnectStateListener(address, ConnectState.NORMAL);
            return true;
        }
        return false;
    }
    /**
     * close all bluetooth connect,  release all resource
     */
    public void closeAll(){
        for (String address:gattMap.keySet()) {
            close(address);
        }
    }
    /**
     * æ–­å¼€è“ç‰™è¿žæŽ¥ï¼Œä¸ä¼šé‡Šæ”¾BluetoothGatt持有的所有资源,可以调用mBluetoothGatt.connect()很快重新连接上
     * å¦‚果不及时释放资源,可能出现133错误,http://www.loverobots.cn/android-ble-connection-solution-bluetoothgatt-status-133.html
     * @param address
     */
    public void disconnect(String address){
        if (!isEmpty(address) && gattMap.containsKey(address)){
            reconnectParamsBean = new ReconnectParamsBean(address);
            reconnectParamsBean.setNumber(1000);
            LogUtils.w("disconnect gatt server " + address);
            BluetoothGatt mBluetoothGatt = gattMap.get(address);
            mBluetoothGatt.disconnect();
            updateConnectStateListener(address, ConnectState.NORMAL);
        }
    }
    /**
     * é‡æ–°è¿žæŽ¥æ–­å¼€çš„设备
     * @param address
     */
    private void reconnectDevice(final String address){
        if (reconnectParamsBean != null){
            if (!reconnectParamsBean.getAddress().equals(address)){
                reconnectParamsBean.updateAddress(address);
            }else {
                if (reconnectParamsBean.getNumber() == 0){//same device
                    reconnectParamsBean.updateAddress(address);
                }else if(reconnectParamsBean.getNumber() == 1000){//disconnect by hand
                    LogUtils.i("reconnect fail! disconnect by hand");
                    reconnectParamsBean.setNumber(0);
                    return;
                }
            }
            reconnectParamsBean.addNumber();
        }else{
            reconnectParamsBean = new ReconnectParamsBean(address);
        }
        //计算下一次重连的时间
        long nextReconnectTime = reconnectParamsBean.getNextReconnectTime() - SystemClock.elapsedRealtime();
        if (nextReconnectTime < 0){
            nextReconnectTime = 0;
        }
        LogUtils.i("next reconnect time " + reconnectParamsBean.toString()+" after:"+nextReconnectTime/1000+"seconds");
        getMainLooperHandler().postDelayed(new Runnable() {
            @Override
            public void run() {
                //重新连接要求没有已经连接的设备,没有正在连接的,蓝牙可用
                if (mBluetoothUtils.isBluetoothIsEnable()) {
                    boolean isReconncted = false;
                    if (gattMap.containsKey(address)) {
                        if (gattMap.size() == 1) isReconncted = true;
                    } else if (gattMap.size() == 0) {
                        isReconncted = true;
                    }
                    //如果已经连接上,也不重连
                    if (!isEmpty(getConnectedDevice())) {
                        isReconncted = false;
                        reconnectParamsBean = null;
                    }
                    if (isReconncted && getConnectedDevice().size() == 0) {
                        LogUtils.d("reconnecting! will reconnect " + address);
                        if (reconnectParamsBean != null){
                            //重连必须在主线程运行
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    connect(address);
                                }
                            });
                        }else {
                            LogUtils.w("Fail to reconnect, ReconnectParams is null");
                        }
                    } else {
                        LogUtils.w("Fail to reconnect, refuse! " + address + " flag:" + isReconncted);
                    }
                }else{
                    LogUtils.w("Fail to reconnect, the bluetooth is disable!");
                }
            }
        }, nextReconnectTime);
    }
    /**
     * å…³é—­é™¤äº†å½“前地址的设备外的其他连接
     * @param address
     */
    private void closeOtherDevice(String address){
        if (!isEmpty(address)){
            //关闭正在已经连接的设备
            List<BluetoothDevice> list = getConnectedDevice();
            for (BluetoothDevice device:list) {
                if (!device.getAddress().equals(address)){
                    close(device.getAddress());
                }
            }
            //关闭其他已经断开的设备
            for (String ads:gattMap.keySet()) {
                BluetoothGatt mBluetoothGatt = gattMap.get(ads);
                if (!ads.equals(address)){
                    close(ads);
                }
            }
        }
    }
    private void updateConnectStateListener(String address, ConnectState state){
        synchronized (connectStateListeners){
            currentState = state;
            if (state == ConnectState.CONNECTING){
                //start check time out connect
                BleParamsOptions options = BleManager.getBleParamsOptions();
                getMainLooperHandler().postDelayed(timeOutTask, options.getConnectTimeOutTimes());
            }
            for (ConnectStateListener listener:connectStateListeners){
                if (listener != null) listener.onConnectStateChanged(address, state);
            }
        }
    }
    /**
     * connect time out task
     */
    private Runnable timeOutTask = new Runnable() {
        @Override
        public void run() {
            if (!mBluetoothUtils.isBluetoothIsEnable()){
                LogUtils.w("Fail to connect device! Bluetooth is not enable!");
                closeAll();
            }
        }
    };
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/BluetoothOperatorQueue.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,135 @@
package com.blakequ.bluetooth_manager_lib.connect;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import com.blankj.utilcode.util.LogUtils;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
 * bluetooth write and read queue oprator
 * Created by PLUSUB on 2015/11/16.
 */
public class BluetoothOperatorQueue {
    private String uuid;
    private final Queue<SimpleEntity> sWriteQueue = new ConcurrentLinkedQueue<SimpleEntity>();
    private boolean sIsWriting = false;
    private BluetoothGatt mBluetoothGatt;
    public BluetoothOperatorQueue(){
    }
    public synchronized void clean(){
        sIsWriting = false;
        uuid = null;
        sWriteQueue.clear();
    }
    /**
     * start operator to write or read BluetoothGatt Service
     * @param mBluetoothGatt
     * @exception IllegalArgumentException if mBluetoothGatt is null
     */
    public void start(BluetoothGatt mBluetoothGatt){
        if (mBluetoothGatt == null){
            throw new IllegalArgumentException("BluetoothGatt is null, can not write or read BluetoothGatt Service");
        }
        this.mBluetoothGatt = mBluetoothGatt;
        nextOperator();
    }
    /**
     * add write or read characteristic operator by order,In order execution
     * @param gattCharacteristic
     * @param isWrite true is write operator, false is read
     */
    public synchronized void addOperator(BluetoothGattCharacteristic gattCharacteristic, boolean isWrite) {
        SimpleEntity entity = new SimpleEntity(isWrite, gattCharacteristic);
        sWriteQueue.add(entity);
    }
    /**
     * add write or read descriptor operator by order,In order execution
     * @param gattDescriptor
     * @param isWrite true is write operator, false is read
     */
    public synchronized void addOperator(BluetoothGattDescriptor gattDescriptor, boolean isWrite) {
        SimpleEntity entity = new SimpleEntity(isWrite, gattDescriptor);
        sWriteQueue.add(entity);
    }
    /**
     * next operator, should invoke by hand
     */
    public synchronized void nextOperator() {
        sIsWriting = false;
        if (!sWriteQueue.isEmpty() && !sIsWriting) {
            doOperator(sWriteQueue.poll());
        }
    }
    /**
     * do operator of read or write
     * @param entity
     */
    private synchronized boolean doOperator(SimpleEntity entity) {
        if (mBluetoothGatt == null){
            LogUtils.e("do operator fail, bluetoothgatt is null");
            return false;
        }
        boolean result = true;
        if (entity.obj instanceof BluetoothGattCharacteristic) {
            sIsWriting = true;
            BluetoothGattCharacteristic character = (BluetoothGattCharacteristic) entity.obj;
            uuid = character.getUuid().toString();
            if (entity.isWrite){
                result = mBluetoothGatt.writeCharacteristic(character);
            }else{
//                test(character);
                result = mBluetoothGatt.readCharacteristic(character);
            }
        } else if (entity.obj instanceof BluetoothGattDescriptor) {
            sIsWriting = true;
            BluetoothGattDescriptor desc = (BluetoothGattDescriptor) entity.obj;
            uuid = desc.getUuid().toString();
            if (entity.isWrite){
                result = mBluetoothGatt.writeDescriptor(desc);
            }else {
                result = mBluetoothGatt.readDescriptor(desc);
            }
        } else {
            LogUtils.d("do operator next");
            nextOperator();
        }
        LogUtils.d("do operator result:"+result+" "+uuid);
        return result;
    }
//    private void test(BluetoothGattCharacteristic characteristic){
//        boolean result = (characteristic.getProperties() &
//                BluetoothGattCharacteristic.PROPERTY_READ) == 0;
//        System.out.println("result:"+result+ " "+characteristic.getProperties()+" "+BluetoothGattCharacteristic.PROPERTY_READ);
//    }
    private static class SimpleEntity {
        public boolean isWrite;
        public Object obj;
        public String info;
        public SimpleEntity(boolean isWrite, Object obj) {
            this.isWrite = isWrite;
            this.obj = obj;
        }
        public SimpleEntity(boolean isWrite, Object obj, String info) {
            this.isWrite = isWrite;
            this.obj = obj;
            this.info = info;
        }
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/BluetoothSubScribeData.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,187 @@
package com.blakequ.bluetooth_manager_lib.connect;
import com.blakequ.bluetooth_manager_lib.device.resolvers.GattAttributeResolver;
import java.util.UUID;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/18 17:48 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description:
 */
public class BluetoothSubScribeData {
    private UUID characteristicUUID;
    private byte[] characteristicValue;
    private UUID descriptorUUID;
    private byte[] descriptorValue;
    //the notification uuid for Characteristic
    private Type operatorType;
    private BluetoothSubScribeData(UUID characteristicUUID, byte[] characteristicValue, UUID descriptorUUID
        ,byte[] descriptorValue, Type operatorType){
        this.characteristicUUID = characteristicUUID;
        this.characteristicValue = characteristicValue;
        this.descriptorUUID = descriptorUUID;
        this.descriptorValue = descriptorValue;
        this.operatorType = operatorType;
    }
    private BluetoothSubScribeData(UUID characteristicUUID, Type operatorType){
        this.characteristicUUID = characteristicUUID;
        this.operatorType = operatorType;
    }
    public UUID getCharacteristicUUID() {
        return characteristicUUID;
    }
    public byte[] getCharacteristicValue() {
        return characteristicValue;
    }
    public UUID getDescriptorUUID() {
        return descriptorUUID;
    }
    public byte[] getDescriptorValue() {
        return descriptorValue;
    }
    public Type getOperatorType() {
        return operatorType;
    }
    public static final class Builder {
        private UUID characteristicUUID;
        private byte[] characteristicValue;
        private UUID descriptorUUID;
        private byte[] descriptorValue;
        //the notification uuid for Characteristic
        private Type operatorType;
        /**
         * read Characteristic
         * @param characteristicUUID
         * @return
         */
        public Builder setCharacteristicRead(UUID characteristicUUID){
            this.operatorType = Type.CHAR_READ;
            this.characteristicUUID = characteristicUUID;
            return this;
        }
        /**
         * write Characteristic
         * @param characteristicUUID
         * @param characteristicValue
         * @return
         */
        public Builder setCharacteristicWrite(UUID characteristicUUID, byte[] characteristicValue){
            this.operatorType = Type.CHAR_WIRTE;
            this.characteristicUUID = characteristicUUID;
            this.characteristicValue = characteristicValue;
            return this;
        }
        /**
         * read Descriptor
         * @param characteristicUUID
         * @param descriptorUUID
         * @return
         */
        public Builder setDescriptorRead(UUID characteristicUUID, UUID descriptorUUID){
            this.operatorType = Type.DESC_READ;
            this.characteristicUUID = characteristicUUID;
            this.descriptorUUID = descriptorUUID;
            return this;
        }
        /**
         * write Descriptor
         * @param characteristicUUID
         * @param descriptorUUID
         * @param descriptorValue
         * @return
         */
        public Builder setDescriptorWrite(UUID characteristicUUID, UUID descriptorUUID, byte[] descriptorValue){
            this.operatorType = Type.DESC_WRITE;
            this.characteristicUUID = characteristicUUID;
            this.descriptorUUID = descriptorUUID;
            this.descriptorValue = descriptorValue;
            return this;
        }
        /**
         * get notify
         * @param characteristicNotificationUUID notify characteristic uuid
         * @return
         */
        public Builder setCharacteristicNotify(UUID characteristicNotificationUUID){
            this.operatorType = Type.NOTIFY;
            this.characteristicUUID = characteristicNotificationUUID;
            this.descriptorUUID = UUID.fromString(GattAttributeResolver.CLIENT_CHARACTERISTIC_CONFIG);
            return this;
        }
        public BluetoothSubScribeData build(){
            if (characteristicUUID == null){
                throw new IllegalArgumentException("invalid characteristic, and characteristic can not be null");
            }
            BluetoothSubScribeData data = null;
            switch (operatorType){
                case CHAR_READ:
                    data = new BluetoothSubScribeData(characteristicUUID, operatorType);
                    break;
                case CHAR_WIRTE:
                    if (characteristicValue == null){
                        throw new IllegalArgumentException("invalid null characteristic value");
                    }
                    data = new BluetoothSubScribeData(characteristicUUID, characteristicValue, null,null, operatorType);
                    break;
                case DESC_READ:
                    if (descriptorUUID == null){
                        throw new IllegalArgumentException("invalid null descriptor UUID");
                    }
                    data = new BluetoothSubScribeData(characteristicUUID, null, descriptorUUID, null, operatorType);
                    break;
                case DESC_WRITE:
                    if (descriptorUUID == null || descriptorValue == null){
                        throw new IllegalArgumentException("invalid null descriptor UUID or value");
                    }
                    data = new BluetoothSubScribeData(characteristicUUID, null, descriptorUUID, descriptorValue, operatorType);
                    break;
                case NOTIFY:
                    if (descriptorUUID == null){
                        throw new IllegalArgumentException("invalid null descriptor UUID");
                    }
                    data = new BluetoothSubScribeData(characteristicUUID, null, descriptorUUID, null, operatorType);
                    break;
            }
            return data;
        }
    }
    /**
     * bluetooth subscribe operator type
     */
    public static enum Type{
        CHAR_WIRTE,
        CHAR_READ,
        DESC_WRITE,
        DESC_READ,
        NOTIFY
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/ConnectConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package com.blakequ.bluetooth_manager_lib.connect;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/29 14:04 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description:
 */
public class ConnectConfig {
    public static int maxConnectDeviceNum = 5;//一次最大连接个数
    /**线性间隔重连,每次断开后重连时间是线性增长*/
    public static final int RECONNECT_LINEAR = 1;
    /**指数间隔重新*/
    public static final int RECONNECT_EXPONENT = 2;
    /**先线性后指数重新*/
    public static final int RECONNECT_LINE_EXPONENT = 3;
    /**固定时间重连,每次断开后都是相同时间之后发起重连*/
    public static final int RECONNECT_FIXED_TIME = 4;
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/ConnectState.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.blakequ.bluetooth_manager_lib.connect;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/19 15:04 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description:
 */
public enum ConnectState {
    /**disconnect or not connect*/
    NORMAL,
    /**connected*/
    CONNECTED,
    CONNECTING
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/ConnectStateListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package com.blakequ.bluetooth_manager_lib.connect;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/23 16:51 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description:
 */
public interface ConnectStateListener {
    /**
     * invoke when bluetooth connect state changed
     * @param address bluetooth device address
     * @param state current state
     */
    void onConnectStateChanged(String address, ConnectState state);
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/GattError.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
package com.blakequ.bluetooth_manager_lib.connect;
/**
 * Copyright (C) quhao All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the Plusub License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Parses the error numbers according to the <b>gatt_api.h</b> file from bluedroid stack.
 * See: https://android.googlesource.com/platform/external/bluetooth/bluedroid/+/android-5.1.0_r1/stack/include/gatt_api.h (and other versions) for details.
 * See:https://github.com/NordicSemiconductor/Android-DFU-Library/blob/release/dfu/src/main/java/no/nordicsemi/android/error/GattError.java
 * <p/>
 * author  : quhao <blakequ@gmail.com>
 * date     : 2016/3/29 10:24
 * last modify author :
 * version : 1.0
 * description:
 */
public class GattError {
    // Starts at line 106 of gatt_api.h file
    /**
     * Converts the connection status given by the {@link android.bluetooth.BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} to error name.
     * @param error the status number
     * @return the error name as stated in the gatt_api.h file
     */
    public static String parseConnectionError(final int error) {
        switch (error) {
            case 0x01:
                return "GATT CONN L2C FAILURE";
            case 0x08:
                return "GATT CONN TIMEOUT";
            case 0x13:
                return "GATT CONN TERMINATE PEER USER";
            case 0x16:
                return "GATT CONN TERMINATE LOCAL HOST";
            case 0x3E:
                return "GATT CONN FAIL ESTABLISH";
            case 0x22:
                return "GATT CONN LMP TIMEOUT";
            case 0x0100:
                return "GATT CONN CANCEL ";
            case 0x0085: //133错误
                return "GATT ERROR"; // Device not reachable
            default:
                return parse(error);
        }
    }
    // Starts at line 29 of the gatt_api.h file
    /**
     * Converts the bluetooth communication status given by other BluetoothGattCallbacks to error name. It also parses the DFU errors.
     * @param error the status number
     * @return the error name as stated in the gatt_api.h file
     */
    private static String parse(final int error) {
        switch (error) {
            case 0:
                return "GATT SUCCESS";
            case 0x0001:
                return "GATT INVALID HANDLE";
            case 0x0002:
                return "GATT READ NOT PERMIT";
            case 0x0003:
                return "GATT WRITE NOT PERMIT";
            case 0x0004:
                return "GATT INVALID PDU";
            case 0x0005:
                return "GATT INSUF AUTHENTICATION";
            case 0x0006:
                return "GATT REQ NOT SUPPORTED";
            case 0x0007:
                return "GATT INVALID OFFSET";
            case 0x0008:
                return "GATT INSUF AUTHORIZATION";
            case 0x0009:
                return "GATT PREPARE Q FULL";
            case 0x000a:
                return "GATT NOT FOUND";
            case 0x000b:
                return "GATT NOT LONG";
            case 0x000c:
                return "GATT INSUF KEY SIZE";
            case 0x000d:
                return "GATT INVALID ATTR LEN";
            case 0x000e:
                return "GATT ERR UNLIKELY";
            case 0x000f:
                return "GATT INSUF ENCRYPTION";
            case 0x0010:
                return "GATT UNSUPPORT GRP TYPE";
            case 0x0011:
                return "GATT INSUF RESOURCE";
            case 0x0087:
                return "GATT ILLEGAL PARAMETER";
            case 0x0080:
                return "GATT NO RESOURCES";
            case 0x0081:
                return "GATT INTERNAL ERROR";
            case 0x0082:
                return "GATT WRONG STATE";
            case 0x0083:
                return "GATT DB FULL";
            case 0x0084:
                return "GATT BUSY";
            case 0x0085:
                return "GATT ERROR";
            case 0x0086:
                return "GATT CMD STARTED";
            case 0x0088:
                return "GATT PENDING";
            case 0x0089:
                return "GATT AUTH FAIL";
            case 0x008a:
                return "GATT MORE";
            case 0x008b:
                return "GATT INVALID CFG";
            case 0x008c:
                return "GATT SERVICE STARTED";
            case 0x008d:
                return "GATT ENCRYPTED NO MITM";
            case 0x008e:
                return "GATT NOT ENCRYPTED";
            case 0x008f:
                return "GATT CONGESTED";
            case 0x00FD:
                return "GATT CCCD CFG ERROR";
            case 0x00FE:
                return "GATT PROCEDURE IN PROGRESS";
            case 0x00FF:
                return "GATT VALUE OUT OF RANGE";
            case 0x0101:
                return "GATT FAILURE, TOO MANY OPEN CONNECTIONS";
            default:
                return "UNKNOWN (" + error + ")";
        }
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/ReconnectParamsBean.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
package com.blakequ.bluetooth_manager_lib.connect;
import android.os.SystemClock;
import com.blakequ.bluetooth_manager_lib.BleManager;
import com.blakequ.bluetooth_manager_lib.BleParamsOptions;
import com.blankj.utilcode.util.LogUtils;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/19 18:23 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description:
 */
public class ReconnectParamsBean {
    private String address;
    private int number;//reconnect times number
    private long nextReconnectTime;//next reconnect time
    private long startDisconnectTime; //the bluetooth disconnected time
    private boolean isReconnectNow = false;
    public ReconnectParamsBean(String address) {
        this.address = address;
        this.startDisconnectTime = SystemClock.elapsedRealtime();
        this.number = 0;
    }
    public String getAddress() {
        return address;
    }
    public void updateAddress(String address){
        this.address = address;
        this.startDisconnectTime = SystemClock.elapsedRealtime();
        this.number = 0;
    }
    /**
     * get next reconnect time
     * @return
     */
    public long getNextReconnectTime() {
        BleParamsOptions options = BleManager.getBleParamsOptions();
        switch (options.getReconnectStrategy()){
            case ConnectConfig.RECONNECT_EXPONENT:
                nextReconnectTime = (long) (startDisconnectTime + options.getReconnectBaseSpaceTime() * Math.pow(2, number));
                break;
            case ConnectConfig.RECONNECT_LINE_EXPONENT:
                if (number <= options.getReconnectedLineToExponentTimes()){
                    nextReconnectTime = startDisconnectTime + options.getReconnectBaseSpaceTime()*number;
                }else {
                    nextReconnectTime = (long) (startDisconnectTime + options.getReconnectBaseSpaceTime() * Math.pow(2, number));
                }
                break;
            case ConnectConfig.RECONNECT_LINEAR:
                nextReconnectTime = startDisconnectTime + options.getReconnectBaseSpaceTime()*number;
                break;
            case ConnectConfig.RECONNECT_FIXED_TIME:
                nextReconnectTime = startDisconnectTime + options.getReconnectBaseSpaceTime();
                break;
        }
        if (isReconnectNow){
            nextReconnectTime = SystemClock.elapsedRealtime();
        }
        //max reconnect times, not reconnect
        if (number >= options.getReconnectMaxTimes()){
            LogUtils.d("reconnect number="+number+" more than max times "+options.getReconnectMaxTimes());
            //将时间设置非常大
            nextReconnectTime = SystemClock.elapsedRealtime() + 10*24*60*60*1000;
        }
        return nextReconnectTime;
    }
    public boolean isReconnectNow() {
        return isReconnectNow;
    }
    public void setReconnectNow(boolean reconnectNow) {
        number = 0;
        startDisconnectTime = SystemClock.elapsedRealtime();
        isReconnectNow = reconnectNow;
    }
    public int getNumber() {
        return number;
    }
    /**
     * invoke after bluetooth disconnected
     */
    public void addNumber() {
        isReconnectNow = false;
        this.startDisconnectTime = SystemClock.elapsedRealtime();
        this.number++;
    }
    /**
     * if you can not want to reconnect by auto, you can set a max value
     * @param num
     */
    public void setNumber(int num){
        this.number = num;
    }
    @Override
    public String toString() {
        return "ReconnectParamsBean{" +
                "address='" + address + '\'' +
                ", number=" + number +
                ", next reconnect after " + (getNextReconnectTime()-startDisconnectTime)/1000 + "seconds"+
                ", startDisconnectTime=" + startDisconnectTime +
                '}';
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/multiple/ConnectRequestQueue.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,609 @@
package com.blakequ.bluetooth_manager_lib.connect.multiple;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.content.Context;
import android.os.Looper;
import android.os.SystemClock;
import com.blakequ.bluetooth_manager_lib.BleManager;
import com.blakequ.bluetooth_manager_lib.BleParamsOptions;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothConnectInterface;
import com.blakequ.bluetooth_manager_lib.connect.ConnectConfig;
import com.blakequ.bluetooth_manager_lib.connect.ConnectState;
import com.blakequ.bluetooth_manager_lib.connect.ConnectStateListener;
import com.blakequ.bluetooth_manager_lib.connect.ReconnectParamsBean;
import com.blakequ.bluetooth_manager_lib.util.BluetoothUtils;
import com.blankj.utilcode.util.LogUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/19 9:48 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description:the queue using for manager connect request
 */
public abstract class ConnectRequestQueue extends BluetoothConnectInterface{
    private static final String TAG = "ConnectRequestQueue";
    private Map<String, ReconnectParamsBean> reconnectMap; //reconnect device list and reconnect times number
    private Map<String, ConnectState> macMap;//<mac address, is connected>
    private Map<String, BluetoothGatt> gattMap;//notice:ArrayMap is not support concurrent, so can not use ArrayMap
    private Queue<String> deviceQueue;
    private final BluetoothUtils mBluetoothUtils;
    private List<ConnectStateListener> connectStateListeners;
    public ConnectRequestQueue(Context context){
        super(context);
        macMap = new ConcurrentHashMap<String, ConnectState>();//if not consider concurrent, should use ArrayMap
        gattMap = new ConcurrentHashMap<String, BluetoothGatt>();
        reconnectMap = new ConcurrentHashMap<String, ReconnectParamsBean>();
        deviceQueue = new ConcurrentLinkedQueue<>();
        mBluetoothUtils = BluetoothUtils.getInstance(context);
        connectStateListeners = new ArrayList<>();
    }
    public void addConnectStateListener(ConnectStateListener listener){
        synchronized (connectStateListeners){
            connectStateListeners.add(listener);
        }
    }
    public void removeConnectStateListener(ConnectStateListener listener){
        synchronized (connectStateListeners){
            connectStateListeners.remove(listener);
        }
    }
    @Override
    protected void onDeviceConnected(BluetoothGatt gatt) {
        if (gatt != null){
            updateConnectState(gatt.getDevice().getAddress(), ConnectState.CONNECTED);
        }
    }
    @Override
    protected void onDeviceDisconnect(BluetoothGatt gatt, int errorState) {
        LogUtils.e( "Disconnected from GATT server address:" + gatt.getDevice().getAddress());
        //可以不关闭,以便重用,因为在连接connect的时候可以快速连接
        if (!mBluetoothUtils.isBluetoothIsEnable()){
            //关闭所有的设备
            closeAll();
        }else {
            close(gatt.getDevice().getAddress());//防止出现status 133
        }
    }
    @Override
    protected void onDiscoverServicesFail(BluetoothGatt gatt) {
        if (gatt != null){
            updateConnectState(gatt.getDevice().getAddress(), ConnectState.NORMAL);
        }
    }
    @Override
    protected void onDiscoverServicesSuccess(BluetoothGatt gatt){
        if (gatt != null){
            updateConnectState(gatt.getDevice().getAddress(), ConnectState.CONNECTED);
        }
    }
    /**
     * start connect device one by one
     */
    public void startConnect(){
        if (deviceQueue.size() > 0 && mBluetoothUtils.isBluetoothIsEnable()){
            triggerConnectNextDevice();
        }else {
            triggerReconnect("");
            LogUtils.e( "startConnect--Fail to from connect queue, and start reconnect task. ble state:" + mBluetoothUtils.isBluetoothIsEnable());
        }
    }
    /**
     * start connect device(will trigger reconnect)
     * @param macAddress
     */
    public void startConnect(String macAddress){
        if (macAddress != null && macAddress.length() > 0){
            if (macMap.containsKey(macAddress)){
                ConnectState state = macMap.get(macAddress);
                //如果是未连接状态,则开启重连,重置重连次数,并立即连接
                if (macMap.get(macAddress) == ConnectState.NORMAL){
                    ReconnectParamsBean bean;
                    if (!reconnectMap.containsKey(macAddress)){
                        bean = new ReconnectParamsBean(macAddress);
                        reconnectMap.put(macAddress, bean);
                    }else{
                        bean = reconnectMap.get(macAddress);
                    }
                    bean.setReconnectNow(true);
                    startReconnectTask();
                }else{
                    LogUtils.i( "Device is " + state + " state");
                }
            }else{
                LogUtils.e( "Fail to connect device, device can not found in queue, you must invoke addDeviceToQueue(Stirng)");
            }
        }else{
            LogUtils.e( "Fail to connect device, mac address is null");
        }
    }
    /**
     * connect bluetooth device one by one
     * @return the next connect device
     */
    private void triggerConnectNextDevice(){
        String mac = deviceQueue.peek();
        if (!isEmpty(mac)){
            LogUtils.i( "Start trigger connect device "+mac);
            connect(mac);
        }
    }
    private void updateConnectState(String address, ConnectState state) {
        //bug:Can not remove device from queue, this position just update connect state
        if (macMap.containsKey(address)) {
            macMap.put(address, state);
            updateConnectStateListener(address, state);
        }
        switch (state){
            case NORMAL: //disconnect or close
                String mac = deviceQueue.peek();
                if (!isEmpty(mac)){
                    if (address.equals(mac)){
                        deviceQueue.poll();
                    }
                    triggerConnectNextDevice();
                }
                triggerReconnect(address);
                break;
            case CONNECTED:
                reconnectMap.remove(address);
                String mac1 = deviceQueue.peek();
                if (!isEmpty(mac1)){
                    if (address.equals(mac1)){
                        deviceQueue.poll();
                    }
                    triggerConnectNextDevice();
                }
                triggerReconnect(address);
                break;
            case CONNECTING:
                //start check time out connect
                BleParamsOptions options = BleManager.getBleParamsOptions();
                getMainLooperHandler().postDelayed(timeOutTask, options.getConnectTimeOutTimes());
                break;
        }
    }
    /**
     * connect time out task
     */
    private Runnable timeOutTask = new Runnable() {
        @Override
        public void run() {
            if (!mBluetoothUtils.isBluetoothIsEnable()){
                LogUtils.w( "Fail to connect device! Bluetooth is not enable!");
                closeAll();
            }
        }
    };
    /**
     * release resource
     */
    @Override
    public void release(){
        macMap.clear();
        closeAll();
        gattMap.clear();
        reconnectMap.clear();
        deviceQueue.clear();
        getMainLooperHandler().removeCallbacks(reconnectTask);
    }
    /**
     * get the size of current queue
     * <p>Notice:this len maybe is not equal of maxLen(connect device num<=maxLen), is dynamic length by sensor physical truth</>
     * @see #getMaxLen()
     */
    public int getQueueSize(){
        return macMap.size();
    }
    /**
     * add device to connect queue, if the number out of range will discard.
     * @param macAddress
     * @see #startConnect()
     */
    public void addDeviceToQueue(String macAddress){
        if (!macMap.containsKey(macAddress)){
            if (macMap.size() >= getMaxLen()){
                String address = deviceQueue.poll();
                if (isEmpty(address)){
                    address = getFirstDevice();
                }
                removeDeviceFromQueue(address);
            }
            deviceQueue.add(macAddress);
            macMap.put(macAddress, ConnectState.NORMAL);
        }
    }
    /**
     * add device to connect queue
     * @param devices
     * @see #startConnect()
     */
    public void addDeviceToQueue(String[] devices){
        if (devices != null && devices.length > 0){
            for (int i=0; i<devices.length; i++){
                addDeviceToQueue(devices[i]);
            }
        }
    }
    /**
     * remove device from queue, disconnected device if in connection state
     * @param macAddress
     */
    public void removeDeviceFromQueue(String macAddress){
        if (isEmpty(macAddress)) return;
        macMap.remove(macAddress);
        deviceQueue.remove(macAddress);
        reconnectMap.remove(macAddress);
        if (gattMap.containsKey(macAddress)){
            close(macAddress);
        }
    }
    public List<String> getAllDevice(){
        if (macMap.size() <= 0) return Collections.EMPTY_LIST;
        List<String> list = new ArrayList<>();
        for (String key:macMap.keySet()){
            list.add(key);
        }
        return list;
    }
    private String getFirstDevice(){
        if (macMap.size() <= 0) return null;
        for (String key:macMap.keySet()){
            return key;
        }
        return null;
    }
    /**
     * get all connected device
     * @return
     */
    public List<String> getAllConnectedDevice(){
        if (macMap.size() <= 0) return Collections.EMPTY_LIST;
        List<String> list = new ArrayList<>();
        for (String key:macMap.keySet()){
            if (macMap.get(key) == ConnectState.CONNECTED){
                list.add(key);
            }
        }
        return list;
    }
    /**
     * is contain device
     * @param address
     * @return
     */
    public boolean containsDevice(String address){
        return macMap.containsKey(address);
    }
    /**
     * get bluetooth state of connect
     * @param address
     * @return
     */
    public ConnectState getDeviceState(String address){
        return macMap.get(address);
    }
    public List<String> getAllConnectingDevice(){
        if (macMap.size() <= 0) return Collections.EMPTY_LIST;
        List<String> list = new ArrayList<>();
        for (String key:macMap.keySet()){
            if (macMap.get(key) == ConnectState.CONNECTING){
                list.add(key);
            }
        }
        return list;
    }
    /**
     * has device is not connected
     * @return
     */
    public boolean isDisconnectDevice(){
        for (ConnectState value:macMap.values()) {
            if (value == ConnectState.NORMAL){
                return true;
            }
        }
        return false;
    }
    /**
     * is have device is connecting
     * @return
     */
    public boolean isConnectingDevice(){
        for (ConnectState value:macMap.values()){
            if (value == ConnectState.CONNECTING){
                return true;
            }
        }
        return false;
    }
    /**
     * is have device is connected
     * @return
     */
    public boolean isConnectedDevice(){
        for (ConnectState value:macMap.values()){
            if (value == ConnectState.CONNECTED){
                return true;
            }
        }
        return false;
    }
    /**
     * èŽ·å–è¿žæŽ¥è®¾å¤‡çš„BluetoothGatt对象,如果没有返回null
     * @param address
     * @return
     */
    public BluetoothGatt getBluetoothGatt(String address){
        if (!isEmpty(address) && gattMap.containsKey(address)){
            return gattMap.get(address);
        }
        return null;
    }
    /**
     * trigger reconnect task
     */
    private void triggerReconnect(String mac){
        //if deviceQueue is null, start reconnect
        if (deviceQueue.size() == 0){
            //将重连的设备全部放入重连队列
            for (String key:macMap.keySet()){
                if (macMap.get(key) == ConnectState.NORMAL){
                    ReconnectParamsBean bean;
                    if (!reconnectMap.containsKey(key)){
                        bean = new ReconnectParamsBean(key);
                        reconnectMap.put(key, bean);
                    }else if(key.equals(mac)){
                        bean = reconnectMap.get(key);
                        bean.addNumber();
                        LogUtils.d( "trigger reconnect, reconnect after "+(bean.getNextReconnectTime() - SystemClock.elapsedRealtime())/1000+" seconds");
                    }
                }
            }
            startReconnectTask();
        }
    }
    /**
     * can not reconnect all the time
     */
    private synchronized void startReconnectTask(){
        if (reconnectMap.size() <= 0) return;
        long nextTime = SystemClock.elapsedRealtime()*2;
        String address = "";
        //select minimum time of list
        for (String addr:reconnectMap.keySet()){
            ReconnectParamsBean bean = reconnectMap.get(addr);
            if (bean.getNextReconnectTime() < nextTime){
                nextTime = bean.getNextReconnectTime();
                address = addr;
            }
        }
        //start reconnect task
        if (!isEmpty(address)){
            if (nextTime <= SystemClock.elapsedRealtime()){
                LogUtils.d( "start reconnect device:"+address);
                reconnectDevice(address);
            }else{
                LogUtils.d( "start reconnect device "+address+" after "+(nextTime - SystemClock.elapsedRealtime())/1000+" seconds");
                getMainLooperHandler().removeCallbacks(reconnectTask);
                getMainLooperHandler().postDelayed(reconnectTask, nextTime - SystemClock.elapsedRealtime());
            }
        }
    }
    /**
     * reconnect runnable
     */
    private Runnable reconnectTask = new Runnable() {
        @Override
        public void run() {
            LogUtils.d( "Start reconnect task by handler");
            startReconnectTask();
        }
    };
    /**
     * reconnect device
     * @param address
     */
    private synchronized void reconnectDevice(final String address){
        if (macMap.containsKey(address)){
            ReconnectParamsBean bean = reconnectMap.get(address);
            if (mBluetoothUtils.isBluetoothIsEnable()) {
                if (bean == null){
                    reconnectMap.put(address, new ReconnectParamsBean(address));
                }
                //check is connected or connectting
                ConnectState state = macMap.get(address);
                if (state == ConnectState.NORMAL){
                    LogUtils.d( "Start reconnect device "+address+" reconnect number is "+bean.getNumber());
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            connect(address);
                        }
                    });
                }else{
                    LogUtils.w( "Fail to reconnect device! "+address+" state is "+state);
                }
            }else {
                closeAll();
                LogUtils.w( "Fail to reconnect device! Bluetooth is not enable!");
            }
        }else{
            LogUtils.w("Fail to reconnect device! "+address+" is remove from reconnectMap");
            reconnectMap.remove(address);
        }
    }
    /**
     * You should invoke {@link #startConnect()} to begin to connect device. Not recommended for direct use this method
     * @see #startConnect()
     * @param address
     * @return
     */
    protected boolean connect(final String address) {
        BluetoothAdapter mAdapter = mBluetoothUtils.getBluetoothAdapter();
        if (mAdapter == null || address == null) {
            LogUtils.e("BluetoothAdapter not initialized or unspecified address "+address);
            updateConnectStateListener(address, ConnectState.NORMAL);
            return false;
        }
        if (!mBluetoothUtils.isBluetoothIsEnable()){
            LogUtils.e("bluetooth is not enable.");
            closeAll();
//            updateConnectStateListener(address, ConnectState.NORMAL);
            return false;
        }
        if (isEmpty(getServiceUUID())){
            LogUtils.w("Service uuid is null");
        }
        // Previously connected device.  Try to reconnect.
        if (gattMap.containsKey(address)){
            BluetoothGatt mBluetoothGatt = gattMap.get(address);
            LogUtils.i("Trying to use an existing gatt and reconnection device " + address + " thread:" + (Thread.currentThread() == Looper.getMainLooper().getThread()));
            if (mBluetoothGatt.connect()) {
                updateConnectState(address, ConnectState.CONNECTING);
                return true;
            } else {
                close(address);
                return false;
            }
        }
        BluetoothDevice device = mAdapter.getRemoteDevice(address);
        if (device != null){
             /*if We want to directly connect to the device, we can setting the autoConnect
             parameter to false.*/
            BluetoothGatt mBluetoothGatt = device.connectGatt(context, false, gattCallback);
            if (mBluetoothGatt != null){
                LogUtils.i("create a new connection address=" + address + " thread:" + (Thread.currentThread() == Looper.getMainLooper().getThread()));
                gattMap.put(address, mBluetoothGatt);
                updateConnectState(address, ConnectState.CONNECTING);
                return true;
            } else {
                LogUtils.e("Get Gatt fail!, address=" + address + " thread:" + (Thread.currentThread() == Looper.getMainLooper().getThread()));
            }
        } else {
            LogUtils.e("Device not found, address=" + address);
        }
        return false;
    }
    /**
     * å…³é—­è“ç‰™è¿žæŽ¥,会释放BluetoothGatt持有的所有资源
     * @param address
     */
    public boolean close(String address) {
        if (!isEmpty(address) && gattMap.containsKey(address)){
            LogUtils.w( "close gatt server " + address);
            BluetoothGatt mBluetoothGatt = gattMap.get(address);
            mBluetoothGatt.close();
            gattMap.remove(address);
            updateConnectState(address, ConnectState.NORMAL);
            return true;
        }
        return false;
    }
    /**
     * å…³é—­æ‰€æœ‰è“ç‰™è®¾å¤‡
     */
    public void closeAll(){
        for (String address:gattMap.keySet()) {
            close(address);
        }
    }
    /**
     * æ–­å¼€è“ç‰™è¿žæŽ¥ï¼Œä¸ä¼šé‡Šæ”¾BluetoothGatt持有的所有资源,可以调用mBluetoothGatt.connect()很快重新连接上
     * å¦‚果不及时释放资源,可能出现133错误,http://www.loverobots.cn/android-ble-connection-solution-bluetoothgatt-status-133.html
     * @param address
     */
    public void disconnect(String address){
        if (!isEmpty(address) && gattMap.containsKey(address)){
            LogUtils.w("disconnect gatt server " + address);
            BluetoothGatt mBluetoothGatt = gattMap.get(address);
            mBluetoothGatt.disconnect();
            updateConnectState(address, ConnectState.NORMAL);
        }
    }
    private void updateConnectStateListener(String address, ConnectState state){
        synchronized (connectStateListeners){
            for (ConnectStateListener listener:connectStateListeners){
                if (listener != null) listener.onConnectStateChanged(address, state);
            }
        }
    }
    /**
     * max connected number of bluetooth queue
     * @return
     */
    public int getMaxLen(){
        return ConnectConfig.maxConnectDeviceNum;
    }
    public boolean isEmpty(String str) {
        return str == null || str.length() == 0;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/multiple/MultiConnectManager.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,147 @@
package com.blakequ.bluetooth_manager_lib.connect.multiple;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import com.blakequ.bluetooth_manager_lib.BleManager;
import com.blakequ.bluetooth_manager_lib.connect.BluetoothSubScribeData;
import com.blakequ.bluetooth_manager_lib.connect.ConnectConfig;
import com.blakequ.bluetooth_manager_lib.connect.ConnectState;
import com.blankj.utilcode.util.LogUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/19 9:49 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description:manager multiple bluetooth device connect
 * <p>
 *     1.must set uuid for subscribe bluetooth device data, {@link #setServiceUUID(String)}, {@link #addBluetoothSubscribeData(BluetoothSubScribeData)}<br>
 *     2.register callback of bluetooth notify by {@link #setBluetoothGattCallback(BluetoothGattCallback)}<br>
 *     3.add device to connect queue, {@link #addDeviceToQueue(String)} or {@link #addDeviceToQueue(String[])}, {@link #removeDeviceFromQueue(String)}<br>
 *     4.start auto connect one by one, {@link #startConnect()}<br>
 *     5.close all connect, {@link #close(String)}, {@link #closeAll()}<br>
 *     <p/>
 */
@TargetApi(18)
public final class MultiConnectManager extends ConnectRequestQueue {
    private static final String TAG = "MultiConnectManager";
    private static MultiConnectManager INSTANCE;
    private BluetoothManager bluetoothManager;
    private static String serviceUUID;
    private BluetoothGattCallback mBluetoothGattCallback;
    private final Queue<BluetoothSubScribeData> subscribeQueue;
    private static Object obj = new Object();
    private MultiConnectManager(Context context){
        super(context);
        bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
        subscribeQueue = new ConcurrentLinkedQueue<BluetoothSubScribeData>();
        BleManager.getBleParamsOptions();
    }
    public static MultiConnectManager getInstance(Context context){
        //双重锁
        if (INSTANCE == null){
            synchronized (obj){
                if (INSTANCE == null){
                    INSTANCE = new MultiConnectManager(context);
                }
            }
        }
        return INSTANCE;
    }
    public List<BluetoothDevice> getConnectedDevice() {
        List<BluetoothDevice> devices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
        if (!isEmpty(devices)){
            List<BluetoothDevice> newDevices = new ArrayList<BluetoothDevice>();
            for (BluetoothDevice device: devices){
                if (getDeviceState(device.getAddress()) == ConnectState.CONNECTED) {
                    newDevices.add(device);
                }else {
                    LogUtils.i("Not exist connected device in queue "+device.getAddress());
                }
            }
            return newDevices;
        }
        return Collections.EMPTY_LIST;
    }
    /**
     * register callback of bluetooth notify
     * @param callback
     */
    public void setBluetoothGattCallback(BluetoothGattCallback callback){
        this.mBluetoothGattCallback = callback;
    }
    /**
     * add subscribe data while read or write characteristic(or descriptor) after discover service.
     * if each device using different config, you should invoke {@link #cleanSubscribeData()} to clean queue before using {@link #startSubscribe(BluetoothGatt)}
     * @param data
     * @see #cleanSubscribeData()
     * @see #startSubscribe(BluetoothGatt)
     * @see #setServiceUUID(String)
     */
    public void addBluetoothSubscribeData(BluetoothSubScribeData data){
        subscribeQueue.add(data);
    }
    /**
     * clean subscribe list
     * @see #addBluetoothSubscribeData(BluetoothSubScribeData)
     */
    public void cleanSubscribeData(){
        subscribeQueue.clear();
    }
    /**
     * set bluetooth service uuid, can not be null
     * @see #addBluetoothSubscribeData(BluetoothSubScribeData)
     * @param serviceUUID
     */
    public void setServiceUUID(String serviceUUID){
        this.serviceUUID = serviceUUID;
    }
    @Override
    protected BluetoothGattCallback getBluetoothGattCallback() {
        return mBluetoothGattCallback;
    }
    @Override
    protected String getServiceUUID() {
        return serviceUUID;
    }
    @Override
    protected Queue<BluetoothSubScribeData> getSubscribeDataQueue() {
        return subscribeQueue;
    }
    @Deprecated
    public void setMaxConnectDeviceNum(int number){
        ConnectConfig.maxConnectDeviceNum = number;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BeaconDevice.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
package com.blakequ.bluetooth_manager_lib.device;
/**
 *
 */
public interface BeaconDevice {
    BeaconType getBeaconType();
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BeaconManufacturerData.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.blakequ.bluetooth_manager_lib.device;
import java.util.Arrays;
/**
 *
 */
public abstract class BeaconManufacturerData {
        private final BeaconType mBeaconType;
        private final byte[] mData;
    protected BeaconManufacturerData(final BeaconType expectedType, final byte[] data){
        if (BeaconUtils.getBeaconType(data) != expectedType) {
            throw new IllegalArgumentException(
                    "Manufacturer record '"
                            + Arrays.toString(data)
                            + "' is not from a " + expectedType);
        }
        this.mData = data;
        this.mBeaconType = expectedType;
    }
    public BeaconType getBeaconType(){
        return mBeaconType;
    }
    public byte[] getData(){
        return mData;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BeaconType.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.blakequ.bluetooth_manager_lib.device;
/**
 * ble device type
 */
public enum BeaconType {
    /**
     * not ibeacon device
     */
    NOT_A_BEACON,
    /**
     * ibeacon device
     * */
    IBEACON,
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BeaconUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package com.blakequ.bluetooth_manager_lib.device;
import com.blakequ.bluetooth_manager_lib.device.adrecord.AdRecord;
import com.blakequ.bluetooth_manager_lib.device.ibeacon.IBeaconConstants;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
/**
 *
 */
public final class BeaconUtils {
    private BeaconUtils(){
        // TO AVOID INSTANTIATION
    }
    /**
     * Ascertains whether a Manufacturer Data byte array belongs to a known Beacon type;
     *
     * @param manufacturerData a Bluetooth LE device's raw manufacturerData.
     * @return the {@link BeaconType}
     */
    public static BeaconType getBeaconType(final byte[] manufacturerData) {
        if (manufacturerData == null || manufacturerData.length == 0) {
            return BeaconType.NOT_A_BEACON;
        }
        if(isIBeacon(manufacturerData)){
            return BeaconType.IBEACON;
        } else {
            return BeaconType.NOT_A_BEACON;
        }
    }
    /**
     * Ascertains whether a {@link com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice} is an iBeacon;
     *
     * @param device a {@link com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice} device.
     * @return the {@link BeaconType}
     */
    public static BeaconType getBeaconType(final BluetoothLeDevice device) {
        final int key = AdRecord.TYPE_MANUFACTURER_SPECIFIC_DATA;
        return getBeaconType(device.getAdRecordStore().getRecordDataAsString(key).getBytes());
    }
    private static boolean isIBeacon(final byte[] manufacturerData){
        // An iBeacon record must be at least 25 chars long
        if (!(manufacturerData.length >= 25)) {
            return false;
        }
        if (ByteUtils.doesArrayBeginWith(manufacturerData, IBeaconConstants.MANUFACTURER_DATA_IBEACON_PREFIX)) {
            return true;
        }
        return false;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BluetoothLeDevice.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,440 @@
package com.blakequ.bluetooth_manager_lib.device;
import android.bluetooth.BluetoothDevice;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import com.blakequ.bluetooth_manager_lib.device.adrecord.AdRecordStore;
import com.blakequ.bluetooth_manager_lib.device.adrecord.AdRecordUtils;
import com.blakequ.bluetooth_manager_lib.device.resolvers.BluetoothClassResolver;
import com.blakequ.bluetooth_manager_lib.device.ibeacon.IBeaconDevice;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import com.blakequ.bluetooth_manager_lib.util.LimitedLinkHashMap;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
// TODO: Auto-generated Javadoc
/**
 * This is a wrapper around the default BluetoothDevice object
 * As BluetoothDevice is final it cannot be extended, so to get it you
 * need to call {@link #getDevice()} method.
 *
 * @author Alexandros Schillings
 */
public class BluetoothLeDevice implements Parcelable ,BeaconDevice{
    /**
     * The Constant CREATOR.
     */
    public static final Creator<BluetoothLeDevice> CREATOR = new Creator<BluetoothLeDevice>() {
        public BluetoothLeDevice createFromParcel(final Parcel in) {
            return new BluetoothLeDevice(in);
        }
        public BluetoothLeDevice[] newArray(final int size) {
            return new BluetoothLeDevice[size];
        }
    };
    protected static final int MAX_RSSI_LOG_SIZE = 10;
    private static final String PARCEL_EXTRA_BLUETOOTH_DEVICE = "bluetooth_device";
    private static final String PARCEL_EXTRA_CURRENT_RSSI = "current_rssi";
    private static final String PARCEL_EXTRA_CURRENT_TIMESTAMP = "current_timestamp";
    private static final String PARCEL_EXTRA_DEVICE_RSSI_LOG = "device_rssi_log";
    private static final String PARCEL_EXTRA_DEVICE_SCANRECORD = "device_scanrecord";
    private static final String PARCEL_EXTRA_DEVICE_SCANRECORD_STORE = "device_scanrecord_store";
    private static final String PARCEL_EXTRA_FIRST_RSSI = "device_first_rssi";
    private static final String PARCEL_EXTRA_FIRST_TIMESTAMP = "first_timestamp";
    private static final long LOG_INVALIDATION_THRESHOLD = 10 * 1000;
    private final AdRecordStore mRecordStore;
    private final BluetoothDevice mDevice;
    private final Map<Long, Integer> mRssiLog;
    private final byte[] mScanRecord;
    private final int mFirstRssi;
    private final long mFirstTimestamp;
    private int mCurrentRssi;
    private long mCurrentTimestamp;
    private transient Set<BluetoothService> mServiceSet;
    /**
     * Instantiates a new Bluetooth LE device.
     *
     * @param device     a standard android Bluetooth device
     * @param rssi       the RSSI value of the Bluetooth device
     * @param scanRecord the scan record of the device
     * @param timestamp  the timestamp of the RSSI reading
     */
    public BluetoothLeDevice(final BluetoothDevice device, final int rssi, final byte[] scanRecord, final long timestamp) {
        mDevice = device;
        mFirstRssi = rssi;
        mFirstTimestamp = timestamp;
        mRecordStore = new AdRecordStore(AdRecordUtils.parseScanRecordAsSparseArray(scanRecord));
        mScanRecord = scanRecord;
        mRssiLog = new LimitedLinkHashMap<>(MAX_RSSI_LOG_SIZE);
        updateRssiReading(timestamp, rssi);
    }
    /**
     * Instantiates a new Bluetooth LE device.
     *
     * @param device the device
     */
    public BluetoothLeDevice(final BluetoothLeDevice device) {
        mCurrentRssi = device.getRssi();
        mCurrentTimestamp = device.getTimestamp();
        mDevice = device.getDevice();
        mFirstRssi = device.getFirstRssi();
        mFirstTimestamp = device.getFirstTimestamp();
        mRecordStore = new AdRecordStore(
                AdRecordUtils.parseScanRecordAsSparseArray(device.getScanRecord()));
        mRssiLog = device.getRssiLog();
        mScanRecord = device.getScanRecord();
    }
    /**
     * Instantiates a new bluetooth le device.
     *
     * @param in the in
     */
    @SuppressWarnings("unchecked")
    protected BluetoothLeDevice(final Parcel in) {
        final Bundle b = in.readBundle(getClass().getClassLoader());
        mCurrentRssi = b.getInt(PARCEL_EXTRA_CURRENT_RSSI, 0);
        mCurrentTimestamp = b.getLong(PARCEL_EXTRA_CURRENT_TIMESTAMP, 0);
        mDevice = b.getParcelable(PARCEL_EXTRA_BLUETOOTH_DEVICE);
        mFirstRssi = b.getInt(PARCEL_EXTRA_FIRST_RSSI, 0);
        mFirstTimestamp = b.getLong(PARCEL_EXTRA_FIRST_TIMESTAMP, 0);
        mRecordStore = b.getParcelable(PARCEL_EXTRA_DEVICE_SCANRECORD_STORE);
        mRssiLog = (Map<Long, Integer>) b.getSerializable(PARCEL_EXTRA_DEVICE_RSSI_LOG);
        mScanRecord = b.getByteArray(PARCEL_EXTRA_DEVICE_SCANRECORD);
    }
    @Override
    public BeaconType getBeaconType() {
        return BeaconUtils.getBeaconType(this);
    }
    /**
     * if ble device is ibeacon will return Ibeacon device, else return null
     * @return
     */
    public IBeaconDevice getIBeaconDevice(){
        if (getBeaconType() == BeaconType.IBEACON){
            return new IBeaconDevice(this);
        }
        return null;
    }
    /**
     * Adds the to rssi log.
     *
     * @param timestamp   the timestamp
     * @param rssiReading the rssi reading
     */
    private void addToRssiLog(final long timestamp, final int rssiReading) {
        synchronized (mRssiLog) {
            if (timestamp - mCurrentTimestamp > LOG_INVALIDATION_THRESHOLD) {
                mRssiLog.clear();
            }
            mCurrentRssi = rssiReading;
            mCurrentTimestamp = timestamp;
            mRssiLog.put(timestamp, rssiReading);
        }
    }
    /* (non-Javadoc)
     * @see android.os.Parcelable#describeContents()
     */
    @Override
    public int describeContents() {
        return 0;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final BluetoothLeDevice other = (BluetoothLeDevice) obj;
        if (mCurrentRssi != other.mCurrentRssi)
            return false;
        if (mCurrentTimestamp != other.mCurrentTimestamp)
            return false;
        if (mDevice == null) {
            if (other.mDevice != null)
                return false;
        } else if (!mDevice.equals(other.mDevice))
            return false;
        if (mFirstRssi != other.mFirstRssi)
            return false;
        if (mFirstTimestamp != other.mFirstTimestamp)
            return false;
        if (mRecordStore == null) {
            if (other.mRecordStore != null)
                return false;
        } else if (!mRecordStore.equals(other.mRecordStore))
            return false;
        if (mRssiLog == null) {
            if (other.mRssiLog != null)
                return false;
        } else if (!mRssiLog.equals(other.mRssiLog))
            return false;
        if (!Arrays.equals(mScanRecord, other.mScanRecord))
            return false;
        return true;
    }
    /**
     * Gets the ad record store.
     *
     * @return the ad record store
     */
    public AdRecordStore getAdRecordStore() {
        return mRecordStore;
    }
    /**
     * Gets the address.
     *
     * @return the address
     */
    public String getAddress() {
        return mDevice.getAddress();
    }
    /**
     * Gets the bluetooth device bond state.
     *
     * @return the bluetooth device bond state
     */
    public String getBluetoothDeviceBondState() {
        return resolveBondingState(mDevice.getBondState());
    }
    /**
     * Gets the bluetooth device class name.
     *
     * @return the bluetooth device class name
     */
    public String getBluetoothDeviceClassName() {
        return BluetoothClassResolver.resolveDeviceClass(mDevice.getBluetoothClass().getDeviceClass());
    }
    public Set<BluetoothService> getBluetoothDeviceKnownSupportedServices() {
        if (mServiceSet == null) {
            synchronized (this) {
                if (mServiceSet == null) {
                    final Set<BluetoothService> serviceSet = new HashSet<>();
                    for (final BluetoothService service : BluetoothService.values()) {
                        if (mDevice.getBluetoothClass().hasService(service.getAndroidConstant())) {
                            serviceSet.add(service);
                        }
                    }
                    mServiceSet = Collections.unmodifiableSet(serviceSet);
                }
            }
        }
        return mServiceSet;
    }
    /**
     * Gets the bluetooth device major class name.
     *
     * @return the bluetooth device major class name
     */
    public String getBluetoothDeviceMajorClassName() {
        return BluetoothClassResolver.resolveMajorDeviceClass(mDevice.getBluetoothClass().getMajorDeviceClass());
    }
    /**
     * Gets the device.
     *
     * @return the device
     */
    public BluetoothDevice getDevice() {
        return mDevice;
    }
    /**
     * Gets the first rssi.
     *
     * @return the first rssi
     */
    public int getFirstRssi() {
        return mFirstRssi;
    }
    /**
     * Gets the first timestamp.
     *
     * @return the first timestamp
     */
    public long getFirstTimestamp() {
        return mFirstTimestamp;
    }
    /**
     * Gets the name.
     *
     * @return the name
     */
    public String getName() {
        return mDevice.getName();
    }
    /**
     * Gets the rssi.
     *
     * @return the rssi
     */
    public int getRssi() {
        return mCurrentRssi;
    }
    /**
     * Gets the rssi log.
     *
     * @return the rssi log
     */
    protected Map<Long, Integer> getRssiLog() {
        synchronized (mRssiLog) {
            return mRssiLog;
        }
    }
    /**
     * Gets the running average rssi.
     *
     * @return the running average rssi
     */
    public double getRunningAverageRssi() {
        int sum = 0;
        int count = 0;
        synchronized (mRssiLog) {
            for (final Long aLong : mRssiLog.keySet()) {
                count++;
                sum += mRssiLog.get(aLong);
            }
        }
        if (count > 0) {
            return sum / count;
        } else {
            return 0;
        }
    }
    /**
     * Gets the scan record.
     *
     * @return the scan record
     */
    public byte[] getScanRecord() {
        return mScanRecord;
    }
    /**
     * Gets the timestamp.
     *
     * @return the timestamp
     */
    public long getTimestamp() {
        return mCurrentTimestamp;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + mCurrentRssi;
        result = prime * result + (int) (mCurrentTimestamp ^ (mCurrentTimestamp >>> 32));
        result = prime * result + ((mDevice == null) ? 0 : mDevice.hashCode());
        result = prime * result + mFirstRssi;
        result = prime * result + (int) (mFirstTimestamp ^ (mFirstTimestamp >>> 32));
        result = prime * result + ((mRecordStore == null) ? 0 : mRecordStore.hashCode());
        result = prime * result + ((mRssiLog == null) ? 0 : mRssiLog.hashCode());
        result = prime * result + Arrays.hashCode(mScanRecord);
        return result;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "BluetoothLeDevice [mDevice=" + mDevice + ", mRssi=" + mFirstRssi + ", mScanRecord=" + ByteUtils.byteArrayToHexString(mScanRecord) + ", mRecordStore=" + mRecordStore + ", getBluetoothDeviceBondState()=" + getBluetoothDeviceBondState() + ", getBluetoothDeviceClassName()=" + getBluetoothDeviceClassName() + "]";
    }
    /**
     * Update rssi reading.
     *
     * @param timestamp   the timestamp
     * @param rssiReading the rssi reading
     */
    public void updateRssiReading(final long timestamp, final int rssiReading) {
        addToRssiLog(timestamp, rssiReading);
    }
    /* (non-Javadoc)
     * @see android.os.Parcelable#writeToParcel(android.os.Parcel, int)
     */
    @Override
    public void writeToParcel(final Parcel parcel, final int arg1) {
        final Bundle b = new Bundle(getClass().getClassLoader());
        b.putByteArray(PARCEL_EXTRA_DEVICE_SCANRECORD, mScanRecord);
        b.putInt(PARCEL_EXTRA_FIRST_RSSI, mFirstRssi);
        b.putInt(PARCEL_EXTRA_CURRENT_RSSI, mCurrentRssi);
        b.putLong(PARCEL_EXTRA_FIRST_TIMESTAMP, mFirstTimestamp);
        b.putLong(PARCEL_EXTRA_CURRENT_TIMESTAMP, mCurrentTimestamp);
        b.putParcelable(PARCEL_EXTRA_BLUETOOTH_DEVICE, mDevice);
        b.putParcelable(PARCEL_EXTRA_DEVICE_SCANRECORD_STORE, mRecordStore);
        b.putSerializable(PARCEL_EXTRA_DEVICE_RSSI_LOG, (Serializable) mRssiLog);
        parcel.writeBundle(b);
    }
    /**
     * Resolve bonding state.
     *
     * @param bondState the bond state
     * @return the string
     */
    private static String resolveBondingState(final int bondState) {
        switch (bondState) {
            case BluetoothDevice.BOND_BONDED:
                return "Paired";
            case BluetoothDevice.BOND_BONDING:
                return "Pairing";
            case BluetoothDevice.BOND_NONE:
                return "Unbonded";
            default:
                return "Unknown";
        }
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/BluetoothService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.blakequ.bluetooth_manager_lib.device;
import android.bluetooth.BluetoothClass;
/**
 *
 */
public enum BluetoothService {
    AUDIO(BluetoothClass.Service.AUDIO),
    CAPTURE(BluetoothClass.Service.CAPTURE),
    INFORMATION(BluetoothClass.Service.INFORMATION),
    LIMITED_DISCOVERABILITY(BluetoothClass.Service.LIMITED_DISCOVERABILITY),
    NETWORKING(BluetoothClass.Service.NETWORKING),
    OBJECT_TRANSFER(BluetoothClass.Service.OBJECT_TRANSFER),
    POSITIONING(BluetoothClass.Service.POSITIONING),
    RENDER(BluetoothClass.Service.RENDER),
    TELEPHONY(BluetoothClass.Service.TELEPHONY);
    private final int mAndroidConstant;
    BluetoothService(final int androidCode){
        mAndroidConstant = androidCode;
    }
    public int getAndroidConstant(){
        return mAndroidConstant;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/adrecord/AdRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,227 @@
package com.blakequ.bluetooth_manager_lib.device.adrecord;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Arrays;
/**
 * Created by Dave Smith
 * Double Encore, Inc.
 * <p>
 * Expanded by Alexandros Schillings
 */
public final class AdRecord implements Parcelable {
    //    02 # Number of bytes that follow in first AD structure
    //    01 # Flags AD type
    //    1A # Flags value 0x1A = 000011010
    //       bit 0 (OFF) LE Limited Discoverable Mode
    //       bit 1 (ON) LE General Discoverable Mode
    //       bit 2 (OFF) BR/EDR Not Supported
    //       bit 3 (ON) Simultaneous LE and BR/EDR to Same Device Capable (controller)
    //       bit 4 (ON) Simultaneous LE and BR/EDR to Same Device Capable (Host)
    //    1A # Number of bytes that follow in second (and last) AD structure
    //    FF # Manufacturer specific data AD type
    //    4C 00 # Company identifier code (0x004C == Apple)
    //    02 # Byte 0 of iBeacon advertisement indicator
    //    15 # Byte 1 of iBeacon advertisement indicator
    //    e2 c5 6d b5 df fb 48 d2 b0 60 d0 f5 a7 10 96 e0 # iBeacon proximity uuid
    //    00 00 # major
    //    00 00 # minor
    //    c5 # The 2's complement of the calibrated Tx Power
    /**
     * General FLAGS
     * <p>
     * Description: Flags
     * <p>
     * Information:
     * Bit 0: LE Limited Discoverable Mode
     * Bit 1: LE General Discoverable Mode
     * Bit 2: BR/EDR Not Supported (i.e. bit 37 of LMP Extended Feature bits Page 0)
     * Bit 3: Simultaneous LE and BR/EDR to Same Device Capable (Controller) (i.e. bit 49 of LMP Extended Feature bits Page 0)
     * Bit 4: Simultaneous LE and BR/EDR to Same Device Capable (Host) (i.e. bit 66 of LMP Extended Feature bits Page 1)
     * Bits 5-7 Reserved
     */
    public static final int TYPE_FLAGS = 0x01;
    // SERVICE
    public static final int TYPE_UUID16_INC = 0x02;
    public static final int TYPE_UUID16 = 0x03;
    public static final int TYPE_UUID32_INC = 0x04;
    public static final int TYPE_UUID32 = 0x05;
    public static final int TYPE_UUID128_INC = 0x06;
    public static final int TYPE_UUID128 = 0x07;
    // Local name
    public static final int TYPE_LOCAL_NAME_SHORT = 0x08;
    public static final int TYPE_LOCAL_NAME_COMPLETE = 0x09;
    // TX Power Level
    public static final int TYPE_TX_POWER_LEVEL = 0x0A;
    // SIMPLE PAIRING OPTIONAL OOB TAGS
    public static final int TYPE_DEVICE_CLASS = 0x0D;
    public static final int TYPE_SIMPLE_PAIRING_HASH_C = 0x0E;
    public static final int TYPE_SIMPLE_PAIRING_RANDOMIZER_R = 0x0F;
    // SECURITY MANAGER TK VALUE
    public static final int TYPE_TK_VALUE = 0x10;
    /* SECURITY MANAGER OOB FLAGS
     *
     * Description: Flag (1 octet)
     *
     * Information:
     * Bit 0: OOB Flags Field: (0 = OOB data not present, 1 = OOB data present)
     * Bit 1: LE supported (Host) (i.e. bit 65 of LMP Extended Feature bits Page 1
     * Bit 2: Simultaneous LE and BR/EDR to Same Device Capable (Host) (i.e. bit 66 of LMP Extended Feature bits Page 1)
     * Bit 3: Address type (0 = Public Address, 1 = Random Address)
     * Bits 4-7 Reserved
     */
    public static final int TYPE_SECURITY_MANAGER_OOB_FLAGS = 0x11;
    /* SLAVE CONNECTION INTERVAL RANGE
     *
     * Description: Slave Connection Interval Range
     *
     * Information:
     * The first 2 octets defines the minimum value for the connection interval in the following manner:
     *    connInterval min = Conn_Interval_Min * 1.25 ms
     *    Conn_Interval_Min range: 0x0006 to 0x0C80
     *    Value of 0xFFFF indicates no specific minimum.
     *    Values outside the range are reserved. (excluding 0xFFFF)
     *
     * The second 2 octets defines the maximum value for the connection interval in the following manner:
     *  connInterval max = Conn_Interval_Max * 1.25 ms
     *  Conn_Interval_Max range: 0x0006 to 0x0C80
     *  Conn_Interval_Max shall be equal to or greater
     *  than the Conn_Interval_Min.
     *  Value of 0xFFFF indicates no specific maximum.
     *  Values outside the range are reserved (excluding 0xFFFF)
     */
    public static final int TYPE_CONNECTION_INTERVAL_RANGE = 0x12;
    // SERVICE SOLICITATION
    public static final int TYPE_SERVICE_UUIDS_LIST_16BIT = 0x14;
    public static final int TYPE_SERVICE_UUIDS_LIST_128BIT = 0x15;
    /* SERVICE DATA
     *
     * Description: Service Data (2 or more octets)
     * Information: The first 2 octets contain the 16 bit Service UUID followed by additional service data
     */
    public static final int TYPE_SERVICE_DATA = 0x16;
    /* MANUFACTURER SPECIFIC DATA
     *
     * Description: Manufacturer Specific Data (2 or more octets)
     * Information: The first 2 octets contain the Company Identifier Code followed by additional manufacturer specific data
     */
    public static final int TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
    public static final Creator<AdRecord> CREATOR = new Creator<AdRecord>() {
        public AdRecord createFromParcel(final Parcel in) {
            return new AdRecord(in);
        }
        public AdRecord[] newArray(final int size) {
            return new AdRecord[size];
        }
    };
    private static final String PARCEL_RECORD_DATA = "record_data";
    private static final String PARCEL_RECORD_TYPE = "record_type";
    private static final String PARCEL_RECORD_LENGTH = "record_length";
    /* Model Object Definition */
    private final int mLength;
    private final int mType;
    private final byte[] mData;
    public AdRecord(final int length, final int type, final byte[] data) {
        mLength = length;
        mType = type;
        mData = data;
    }
    public AdRecord(final Parcel in) {
        final Bundle b = in.readBundle(getClass().getClassLoader());
        mLength = b.getInt(PARCEL_RECORD_LENGTH);
        mType = b.getInt(PARCEL_RECORD_TYPE);
        mData = b.getByteArray(PARCEL_RECORD_DATA);
    }
    @Override
    public int describeContents() {
        return 0;
    }
    public byte[] getData() {
        return mData;
    }
    public String getHumanReadableType() {
        return getHumanReadableAdType(mType);
    }
    public int getLength() {
        return mLength;
    }
    public int getType() {
        return mType;
    }
    @Override
    public String toString() {
        return "AdRecord [mLength=" + mLength + ", mType=" + mType + ", mData=" + Arrays.toString(mData) + ", getHumanReadableType()=" + getHumanReadableType() + "]";
    }
    @Override
    public void writeToParcel(final Parcel parcel, final int arg1) {
        final Bundle b = new Bundle(getClass().getClassLoader());
        b.putInt(PARCEL_RECORD_LENGTH, mLength);
        b.putInt(PARCEL_RECORD_TYPE, mType);
        b.putByteArray(PARCEL_RECORD_DATA, mData);
        parcel.writeBundle(b);
    }
    private static String getHumanReadableAdType(final int type) {
        switch (type) {
            case TYPE_CONNECTION_INTERVAL_RANGE:
                return "Slave Connection Interval Range";
            case TYPE_DEVICE_CLASS:
                return "Class of device";
            case TYPE_FLAGS:
                return "Flags";
            case TYPE_MANUFACTURER_SPECIFIC_DATA:
                return "Manufacturer Specific Data";
            case TYPE_LOCAL_NAME_COMPLETE:
                return "Name (Complete)";
            case TYPE_LOCAL_NAME_SHORT:
                return "Name (Short)";
            case TYPE_SECURITY_MANAGER_OOB_FLAGS:
                return "Security Manager OOB Flags";
            case TYPE_SERVICE_UUIDS_LIST_128BIT:
                return "Service UUIDs (128bit)";
            case TYPE_SERVICE_UUIDS_LIST_16BIT:
                return "Service UUIDs (16bit)";
            case TYPE_SERVICE_DATA:
                return "Service Data";
            case TYPE_SIMPLE_PAIRING_HASH_C:
                return "Simple Pairing Hash C";
            case TYPE_SIMPLE_PAIRING_RANDOMIZER_R:
                return "Simple Pairing Randomizer R";
            case TYPE_TK_VALUE:
                return "TK Value";
            case TYPE_TX_POWER_LEVEL:
                return "Transmission Power Level";
            case TYPE_UUID128:
                return "Complete list of 128-bit UUIDs available";
            case TYPE_UUID128_INC:
                return "More 128-bit UUIDs available";
            case TYPE_UUID16:
                return "Complete list of 16-bit UUIDs available";
            case TYPE_UUID16_INC:
                return "More 16-bit UUIDs available";
            case TYPE_UUID32:
                return "Complete list of 32-bit UUIDs available";
            case TYPE_UUID32_INC:
                return "More 32-bit UUIDs available";
            default:
                return "Unknown AdRecord Structure: " + type;
        }
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/adrecord/AdRecordStore.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,157 @@
package com.blakequ.bluetooth_manager_lib.device.adrecord;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseArray;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
/**
 * The Class AdRecordStore.
 */
public class AdRecordStore implements Parcelable {
    public static final Creator<AdRecordStore> CREATOR = new Creator<AdRecordStore>() {
        public AdRecordStore createFromParcel(final Parcel in) {
            return new AdRecordStore(in);
        }
        public AdRecordStore[] newArray(final int size) {
            return new AdRecordStore[size];
        }
    };
    private final SparseArray<AdRecord> mAdRecords;
    private final String mLocalNameComplete;
    private final String mLocalNameShort;
    public AdRecordStore(final Parcel in) {
        final Bundle b = in.readBundle(getClass().getClassLoader());
        mAdRecords = b.getSparseParcelableArray("records_array");
        mLocalNameComplete = b.getString("local_name_complete");
        mLocalNameShort = b.getString("local_name_short");
    }
    /**
     * Instantiates a new Bluetooth LE device Ad Record Store.
     *
     * @param adRecords the ad records
     */
    public AdRecordStore(final SparseArray<AdRecord> adRecords) {
        mAdRecords = adRecords;
        mLocalNameComplete = AdRecordUtils.getRecordDataAsString(
                mAdRecords.get(AdRecord.TYPE_LOCAL_NAME_COMPLETE));
        mLocalNameShort = AdRecordUtils.getRecordDataAsString(
                mAdRecords.get(AdRecord.TYPE_LOCAL_NAME_SHORT));
    }
    /* (non-Javadoc)
     * @see android.os.Parcelable#describeContents()
     */
    @Override
    public int describeContents() {
        return 0;
    }
    /**
     * Gets the short local device name.
     *
     * @return the local name complete
     */
    public String getLocalNameComplete() {
        return mLocalNameComplete;
    }
    /**
     * Gets the complete local device name.
     *
     * @return the local name short
     */
    public String getLocalNameShort() {
        return mLocalNameShort;
    }
    /**
     * retrieves an individual record.
     *
     * @param record the record
     * @return the record
     */
    public AdRecord getRecord(final int record) {
        return mAdRecords.get(record);
    }
    /**
     * Gets the record data as string.
     *
     * @param record the record
     * @return the record data as string
     */
    public String getRecordDataAsString(final int record) {
        return AdRecordUtils.getRecordDataAsString(
                mAdRecords.get(record));
    }
    /**
     * Gets the record as collection.
     *
     * @return the records as collection
     */
    public Collection<AdRecord> getRecordsAsCollection() {
        return Collections.unmodifiableCollection(asList(mAdRecords));
    }
    /**
     * Checks if is record present.
     *
     * @param record the record
     * @return true, if is record present
     */
    public boolean isRecordPresent(final int record) {
        return mAdRecords.indexOfKey(record) >= 0;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "AdRecordStore [mLocalNameComplete=" + mLocalNameComplete + ", mLocalNameShort=" + mLocalNameShort + "]";
    }
    /* (non-Javadoc)
     * @see android.os.Parcelable#writeToParcel(android.os.Parcel, int)
     */
    @Override
    public void writeToParcel(final Parcel parcel, final int arg1) {
        final Bundle b = new Bundle();
        b.putString("local_name_complete", mLocalNameComplete);
        b.putString("local_name_short", mLocalNameShort);
        b.putSparseParcelableArray("records_array", mAdRecords);
        parcel.writeBundle(b);
    }
    /**
     * As list.
     *
     * @param <C>         the generic type
     * @param sparseArray the sparse array
     * @return the collection
     */
    public static <C> Collection<C> asList(final SparseArray<C> sparseArray) {
        if (sparseArray == null) return null;
        final Collection<C> arrayList = new ArrayList<>(sparseArray.size());
        for (int i = 0; i < sparseArray.size(); i++) {
            arrayList.add(sparseArray.valueAt(i));
        }
        return arrayList;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/adrecord/AdRecordUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,132 @@
package com.blakequ.bluetooth_manager_lib.device.adrecord;
import android.annotation.SuppressLint;
import android.util.SparseArray;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class AdRecordUtils {
    private AdRecordUtils(){
        // TO AVOID INSTANTIATION
    }
    public static String getRecordDataAsString(final AdRecord nameRecord) {
        if (nameRecord == null) {
            return "";
        }
        return new String(nameRecord.getData());
    }
    public static byte[] getServiceData(final AdRecord serviceData) {
        if (serviceData == null) {
            return null;
        }
        if (serviceData.getType() != AdRecord.TYPE_SERVICE_DATA) return null;
        final byte[] raw = serviceData.getData();
        //Chop out the uuid
        return Arrays.copyOfRange(raw, 2, raw.length);
    }
    public static int getServiceDataUuid(final AdRecord serviceData) {
        if (serviceData == null) {
            return -1;
        }
        if (serviceData.getType() != AdRecord.TYPE_SERVICE_DATA) return -1;
        final byte[] raw = serviceData.getData();
        //Find UUID data in byte array
        int uuid = (raw[1] & 0xFF) << 8;
        uuid += (raw[0] & 0xFF);
        return uuid;
    }
    /*
     * Read out all the AD structures from the raw scan record
     */
    public static List<AdRecord> parseScanRecordAsList(final byte[] scanRecord) {
        final List<AdRecord> records = new ArrayList<>();
        int index = 0;
        while (index < scanRecord.length) {
            final int length = scanRecord[index++];
            //Done once we run out of records
            if (length == 0) break;
            final int type = ByteUtils.getIntFromByte(scanRecord[index]);
            //Done if our record isn't a valid type
            if (type == 0) break;
            final byte[] data = Arrays.copyOfRange(scanRecord, index + 1, index + length);
            records.add(new AdRecord(length, type, data));
            //Advance
            index += length;
        }
        return Collections.unmodifiableList(records);
    }
    @SuppressLint("UseSparseArrays")
    public static Map<Integer, AdRecord> parseScanRecordAsMap(final byte[] scanRecord) {
        final Map<Integer, AdRecord> records = new HashMap<>();
        int index = 0;
        while (index < scanRecord.length) {
            final int length = scanRecord[index++];
            //Done once we run out of records
            if (length == 0) break;
            final int type = ByteUtils.getIntFromByte(scanRecord[index]);
            //Done if our record isn't a valid type
            if (type == 0) break;
            final byte[] data = Arrays.copyOfRange(scanRecord, index + 1, index + length);
            records.put(type, new AdRecord(length, type, data));
            //Advance
            index += length;
        }
        return Collections.unmodifiableMap(records);
    }
    public static SparseArray<AdRecord> parseScanRecordAsSparseArray(final byte[] scanRecord) {
        final SparseArray<AdRecord> records = new SparseArray<>();
        int index = 0;
        while (index < scanRecord.length) {
            final int length = scanRecord[index++];
            //Done once we run out of records
            if (length == 0) break;
            final int type = ByteUtils.getIntFromByte(scanRecord[index]);
            //Done if our record isn't a valid type
            if (type == 0) break;
            final byte[] data = Arrays.copyOfRange(scanRecord, index + 1, index + length);
            records.put(type, new AdRecord(length, type, data));
            //Advance
            index += length;
        }
        return records;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/ibeacon/IBeaconConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.blakequ.bluetooth_manager_lib.device.ibeacon;
/**
 *
 */
public class IBeaconConstants {
    public static final byte[] MANUFACTURER_DATA_IBEACON_PREFIX = {0x4C, 0x00, 0x02, 0x15};
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/ibeacon/IBeaconDevice.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
package com.blakequ.bluetooth_manager_lib.device.ibeacon;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
import android.os.Parcel;
import com.blakequ.bluetooth_manager_lib.device.BeaconDevice;
import com.blakequ.bluetooth_manager_lib.device.BeaconType;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
@SuppressLint("ParcelCreator")
public class IBeaconDevice extends BluetoothLeDevice implements BeaconDevice {
    /**
     * The m iBeacon data.
     */
    private final IBeaconManufacturerData mIBeaconData;
    /**
     * Instantiates a new iBeacon device.
     *
     * @param device     the device
     * @param rssi       the RSSI value
     * @param scanRecord the scanRecord
     * @throws IllegalArgumentException if the passed device is not an iBeacon
     */
    public IBeaconDevice(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
        super(device, rssi, scanRecord, 0);
        mIBeaconData = new IBeaconManufacturerData(this);
    }
    /**
     * Instantiates a new iBeacon device.
     *
     * @param device     the device
     * @param rssi       the RSSI value of the RSSI measurement
     * @param scanRecord the scan record
     * @param timestamp  the timestamp of the RSSI measurement
     * @throws IllegalArgumentException if the passed device is not an iBeacon
     */
    public IBeaconDevice(final BluetoothDevice device, final int rssi, final byte[] scanRecord, final long timestamp) {
        super(device, rssi, scanRecord, timestamp);
        mIBeaconData = new IBeaconManufacturerData(this);
    }
    /**
     * Will try to convert a {@link BluetoothLeDevice} into an
     * iBeacon Device.
     *
     * @param device the device
     * @throws IllegalArgumentException if the passed device is not an iBeacon
     */
    public IBeaconDevice(final BluetoothLeDevice device) {
        super(device);
        mIBeaconData = new IBeaconManufacturerData(this);
    }
    private IBeaconDevice(final Parcel in) {
        super(in);
        mIBeaconData = new IBeaconManufacturerData(this);
    }
    /**
     * Gets the estimated Accuracy of the reading in meters based on
     * a simple running average of the last {@link #MAX_RSSI_LOG_SIZE}
     * samples.
     *
     * @return the accuracy in meters
     */
    public double getAccuracy() {
        return IBeaconUtils.calculateAccuracy(
                getCalibratedTxPower(),
                getRunningAverageRssi());
    }
    @Override
    public BeaconType getBeaconType() {
        return BeaconType.IBEACON;
    }
    /**
     * Gets the calibrated TX power of the iBeacon device as reported.
     *
     * @return the calibrated TX power
     */
    public int getCalibratedTxPower() {
        return getIBeaconData().getCalibratedTxPower();
    }
    /**
     * Gets the iBeacon company identifier.
     *
     * @return the company identifier
     */
    public int getCompanyIdentifier() {
        return getIBeaconData().getCompanyIdentifier();
    }
    /**
     * Gets the estimated Distance descriptor.
     *
     * @return the distance descriptor
     */
    public IBeaconDistanceDescriptor getDistanceDescriptor() {
        return IBeaconUtils.getDistanceDescriptor(getAccuracy());
    }
    /**
     * Gets the iBeacon manufacturing data.
     *
     * @return the iBeacon data
     */
    public IBeaconManufacturerData getIBeaconData() {
        return mIBeaconData;
    }
    /**
     * Gets the iBeacon Major value.
     *
     * @return the Major value
     */
    public int getMajor() {
        return getIBeaconData().getMajor();
    }
    /**
     * Gets the iBeacon Minor value.
     *
     * @return the Minor value
     */
    public int getMinor() {
        return getIBeaconData().getMinor();
    }
    /**
     * Gets the iBeacon UUID.
     *
     * @return the UUID
     */
    public String getUUID() {
        return getIBeaconData().getUUID();
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/ibeacon/IBeaconDistanceDescriptor.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
package com.blakequ.bluetooth_manager_lib.device.ibeacon;
public enum IBeaconDistanceDescriptor {
    IMMEDIATE,
    NEAR,
    FAR,
    UNKNOWN,
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/ibeacon/IBeaconManufacturerData.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
package com.blakequ.bluetooth_manager_lib.device.ibeacon;
import com.blakequ.bluetooth_manager_lib.device.BeaconManufacturerData;
import com.blakequ.bluetooth_manager_lib.device.BeaconType;
import com.blakequ.bluetooth_manager_lib.device.BluetoothLeDevice;
import com.blakequ.bluetooth_manager_lib.device.adrecord.AdRecord;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
import java.util.Arrays;
/**
 * Parses the Manufactured Data field of an iBeacon
 * <p>
 * The parsing is based on the following schema:
 * <pre>
 * Byte|Value
 * -------------------------------------------------
 * 0    4C - Byte 1 (LSB) of Company identifier code
 * 1    00 - Byte 0 (MSB) of Company identifier code (0x004C == Apple)
 * 2    02 - Byte 0 of iBeacon advertisement indicator
 * 3    15 - Byte 1 of iBeacon advertisement indicator
 * 4    e2 |\
 * 5    c5 |\\
 * 6    6d |#\\
 * 7    b5 |##\\
 * 8    df |###\\
 * 9    fb |####\\
 * 10    48 |#####\\
 * 11    d2 |#####|| iBeacon
 * 12    b0 |#####|| Proximity UUID
 * 13    60 |#####//
 * 14    d0 |####//
 * 15    f5 |###//
 * 16    a7 |##//
 * 17    10 |#//
 * 18    96 |//
 * 19    e0 |/
 * 20    00 - major
 * 21    00
 * 22    00 - minor
 * 23    00
 * 24    c5 - The 2's complement of the calibrated Tx Power
 * </pre>
 * @author Alexandros Schillings
 */
public final class IBeaconManufacturerData extends BeaconManufacturerData {
    private final int mCalibratedTxPower;
    private final int mCompanyIdentidier;
    private final int mIBeaconAdvertisment;
    private final int mMajor;
    private final int mMinor;
    private final String mUUID;
    /**
     * Instantiates a new iBeacon manufacturer data object.
     *
     * @param device a {@link BluetoothLeDevice}
     * @throws IllegalArgumentException if the data is not from an iBeacon.
     */
    public IBeaconManufacturerData(final BluetoothLeDevice device) {
        this(device.getAdRecordStore().getRecord(AdRecord.TYPE_MANUFACTURER_SPECIFIC_DATA).getData());
    }
    /**
     * Instantiates a new iBeacon manufacturer data object.
     *
     * @param manufacturerData the {@link AdRecord#TYPE_MANUFACTURER_SPECIFIC_DATA} data array
     * @throws IllegalArgumentException if the data is not from an iBeacon.
     */
    public IBeaconManufacturerData(final byte[] manufacturerData) {
        super(BeaconType.IBEACON, manufacturerData);
        final byte[] intArray = Arrays.copyOfRange(manufacturerData, 0, 2);
        ByteUtils.invertArray(intArray);
        mCompanyIdentidier = ByteUtils.getIntFrom2ByteArray(intArray);
        mIBeaconAdvertisment = ByteUtils.getIntFrom2ByteArray(Arrays.copyOfRange(manufacturerData, 2, 4));
        mUUID = IBeaconUtils.calculateUuidString(Arrays.copyOfRange(manufacturerData, 4, 20));
        mMajor = ByteUtils.getIntFrom2ByteArray(Arrays.copyOfRange(manufacturerData, 20, 22));
        mMinor = ByteUtils.getIntFrom2ByteArray(Arrays.copyOfRange(manufacturerData, 22, 24));
        mCalibratedTxPower = manufacturerData[24];
    }
    /**
     * Gets the calibrated TX power of the iBeacon device as reported.
     *
     * @return the calibrated TX power
     */
    public int getCalibratedTxPower() {
        return mCalibratedTxPower;
    }
    /**
     * Gets the iBeacon company identifier.
     *
     * @return the company identifier
     */
    public int getCompanyIdentifier() {
        return mCompanyIdentidier;
    }
    public int getIBeaconAdvertisement() {
        return mIBeaconAdvertisment;
    }
    /**
     * Gets the iBeacon Major value.
     *
     * @return the Major value
     */
    public int getMajor() {
        return mMajor;
    }
    /**
     * Gets the iBeacon Minor value.
     *
     * @return the Minor value
     */
    public int getMinor() {
        return mMinor;
    }
    /**
     * Gets the iBeacon UUID.
     *
     * @return the UUID
     */
    public String getUUID() {
        return mUUID;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/ibeacon/IBeaconUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
package com.blakequ.bluetooth_manager_lib.device.ibeacon;
import com.blakequ.bluetooth_manager_lib.util.ByteUtils;
final class IBeaconUtils {
    private static final double DISTANCE_THRESHOLD_WTF = 0.0;
    private static final double DISTANCE_THRESHOLD_IMMEDIATE = 0.5;
    private static final double DISTANCE_THRESHOLD_NEAR = 3.0;
    private IBeaconUtils(){
        // TO AVOID INSTANTIATION
    }
    /**
     * Calculates the accuracy of an RSSI reading.
     * <p/>
     * The code was taken from <a href="http://stackoverflow.com/questions/20416218/understanding-ibeacon-distancing" /a>
     *
     * @param txPower the calibrated TX power of an iBeacon
     * @param rssi    the RSSI value of the iBeacon
     * @return the calculated Accuracy
     */
    public static double calculateAccuracy(final int txPower, final double rssi) {
        if (rssi == 0) {
            return -1.0; // if we cannot determine accuracy, return -1.
        }
        final double ratio = rssi * 1.0 / txPower;
        if (ratio < 1.0) {
            return Math.pow(ratio, 10);
        } else {
            return (0.89976) * Math.pow(ratio, 7.7095) + 0.111;
        }
    }
    public static String calculateUuidString(final byte[] uuid) {
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < uuid.length; i++) {
            if (i == 4) {
                sb.append('-');
            }
            if (i == 6) {
                sb.append('-');
            }
            if (i == 8) {
                sb.append('-');
            }
            if (i == 10) {
                sb.append('-');
            }
            final int intFromByte = ByteUtils.getIntFromByte(uuid[i]);
            if(intFromByte <= 0xF){
                sb.append('0');
            }
            sb.append(Integer.toHexString(intFromByte));
        }
        return sb.toString();
    }
    public static IBeaconDistanceDescriptor getDistanceDescriptor(final double accuracy) {
        if (accuracy < DISTANCE_THRESHOLD_WTF) {
            return IBeaconDistanceDescriptor.UNKNOWN;
        }
        if (accuracy < DISTANCE_THRESHOLD_IMMEDIATE) {
            return IBeaconDistanceDescriptor.IMMEDIATE;
        }
        if (accuracy < DISTANCE_THRESHOLD_NEAR) {
            return IBeaconDistanceDescriptor.NEAR;
        }
        return IBeaconDistanceDescriptor.FAR;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/resolvers/BluetoothClassResolver.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
package com.blakequ.bluetooth_manager_lib.device.resolvers;
import android.bluetooth.BluetoothClass;
public class BluetoothClassResolver {
    public static String resolveDeviceClass(final int btClass) {
        switch (btClass) {
            case BluetoothClass.Device.AUDIO_VIDEO_CAMCORDER:
                return "A/V, Camcorder";
            case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO:
                return "A/V, Car Audio";
            case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE:
                return "A/V, Handsfree";
            case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES:
                return "A/V, Headphones";
            case BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO:
                return "A/V, HiFi Audio";
            case BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER:
                return "A/V, Loudspeaker";
            case BluetoothClass.Device.AUDIO_VIDEO_MICROPHONE:
                return "A/V, Microphone";
            case BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO:
                return "A/V, Portable Audio";
            case BluetoothClass.Device.AUDIO_VIDEO_SET_TOP_BOX:
                return "A/V, Set Top Box";
            case BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED:
                return "A/V, Uncategorized";
            case BluetoothClass.Device.AUDIO_VIDEO_VCR:
                return "A/V, VCR";
            case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CAMERA:
                return "A/V, Video Camera";
            case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CONFERENCING:
                return "A/V, Video Conferencing";
            case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER:
                return "A/V, Video Display and Loudspeaker";
            case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_GAMING_TOY:
                return "A/V, Video Gaming Toy";
            case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_MONITOR:
                return "A/V, Video Monitor";
            case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
                return "A/V, Video Wearable Headset";
            case BluetoothClass.Device.COMPUTER_DESKTOP:
                return "Computer, Desktop";
            case BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA:
                return "Computer, Handheld PC/PDA";
            case BluetoothClass.Device.COMPUTER_LAPTOP:
                return "Computer, Laptop";
            case BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA:
                return "Computer, Palm Size PC/PDA";
            case BluetoothClass.Device.COMPUTER_SERVER:
                return "Computer, Server";
            case BluetoothClass.Device.COMPUTER_UNCATEGORIZED:
                return "Computer, Uncategorized";
            case BluetoothClass.Device.COMPUTER_WEARABLE:
                return "Computer, Wearable";
            case BluetoothClass.Device.HEALTH_BLOOD_PRESSURE:
                return "Health, Blood Pressure";
            case BluetoothClass.Device.HEALTH_DATA_DISPLAY:
                return "Health, Data Display";
            case BluetoothClass.Device.HEALTH_GLUCOSE:
                return "Health, Glucose";
            case BluetoothClass.Device.HEALTH_PULSE_OXIMETER:
                return "Health, Pulse Oximeter";
            case BluetoothClass.Device.HEALTH_PULSE_RATE:
                return "Health, Pulse Rate";
            case BluetoothClass.Device.HEALTH_THERMOMETER:
                return "Health, Thermometer";
            case BluetoothClass.Device.HEALTH_UNCATEGORIZED:
                return "Health, Uncategorized";
            case BluetoothClass.Device.HEALTH_WEIGHING:
                return "Health, Weighting";
            case BluetoothClass.Device.PHONE_CELLULAR:
                return "Phone, Cellular";
            case BluetoothClass.Device.PHONE_CORDLESS:
                return "Phone, Cordless";
            case BluetoothClass.Device.PHONE_ISDN:
                return "Phone, ISDN";
            case BluetoothClass.Device.PHONE_MODEM_OR_GATEWAY:
                return "Phone, Modem or Gateway";
            case BluetoothClass.Device.PHONE_SMART:
                return "Phone, Smart";
            case BluetoothClass.Device.PHONE_UNCATEGORIZED:
                return "Phone, Uncategorized";
            case BluetoothClass.Device.TOY_CONTROLLER:
                return "Toy, Controller";
            case BluetoothClass.Device.TOY_DOLL_ACTION_FIGURE:
                return "Toy, Doll/Action Figure";
            case BluetoothClass.Device.TOY_GAME:
                return "Toy, Game";
            case BluetoothClass.Device.TOY_ROBOT:
                return "Toy, Robot";
            case BluetoothClass.Device.TOY_UNCATEGORIZED:
                return "Toy, Uncategorized";
            case BluetoothClass.Device.TOY_VEHICLE:
                return "Toy, Vehicle";
            case BluetoothClass.Device.WEARABLE_GLASSES:
                return "Wearable, Glasses";
            case BluetoothClass.Device.WEARABLE_HELMET:
                return "Wearable, Helmet";
            case BluetoothClass.Device.WEARABLE_JACKET:
                return "Wearable, Jacket";
            case BluetoothClass.Device.WEARABLE_PAGER:
                return "Wearable, Pager";
            case BluetoothClass.Device.WEARABLE_UNCATEGORIZED:
                return "Wearable, Uncategorized";
            case BluetoothClass.Device.WEARABLE_WRIST_WATCH:
                return "Wearable, Wrist Watch";
            default:
                return "Unknown, Unknown (class=" + btClass + ")";
        }
    }
    public static String resolveMajorDeviceClass(final int majorBtClass) {
        switch (majorBtClass) {
            case BluetoothClass.Device.Major.AUDIO_VIDEO:
                return "Audio/ Video";
            case BluetoothClass.Device.Major.COMPUTER:
                return "Computer";
            case BluetoothClass.Device.Major.HEALTH:
                return "Health";
            case BluetoothClass.Device.Major.IMAGING:
                return "Imaging";
            case BluetoothClass.Device.Major.MISC:
                return "Misc";
            case BluetoothClass.Device.Major.NETWORKING:
                return "Networking";
            case BluetoothClass.Device.Major.PERIPHERAL:
                return "Peripheral";
            case BluetoothClass.Device.Major.PHONE:
                return "Phone";
            case BluetoothClass.Device.Major.TOY:
                return "Toy";
            case BluetoothClass.Device.Major.UNCATEGORIZED:
                return "Uncategorized";
            case BluetoothClass.Device.Major.WEARABLE:
                return "Wearable";
            default:
                return "Unknown (" +majorBtClass+ ")";
        }
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/resolvers/CompanyIdentifierResolver.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,646 @@
package com.blakequ.bluetooth_manager_lib.device.resolvers;
import android.util.SparseArray;
public class CompanyIdentifierResolver {
    public static final int ERICSSON_TECHNOLOGY_LICENSING = 0x0000;
    public static final int NOKIA_MOBILE_PHONES = 0x0001;
    public static final int INTEL_CORP = 0x0002;
    public static final int IBM_CORP = 0x0003;
    public static final int TOSHIBA_CORP = 0x0004;
    public static final int THREE_COM = 0x0005;
    public static final int MICROSOFT = 0x0006;
    public static final int LUCENT = 0x0007;
    public static final int MOTOROLA = 0x0008;
    public static final int INFINEON_TECHNOLOGIES_AG = 0x0009;
    public static final int CAMBRIDGE_SILICON_RADIO = 0x000A;
    public static final int SILICON_WAVE = 0x000B;
    public static final int DIGIANSWER_A_S = 0x000C;
    public static final int TEXAS_INSTRUMENTS_INC = 0x000D;
    public static final int CEVA_INC_FORMERLY_PARTHUS_TECHNOLOGIES_INC = 0x000E;
    public static final int BROADCOM_CORPORATION = 0x000F;
    public static final int MITEL_SEMICONDUCTOR = 0x0010;
    public static final int WIDCOMM_INC = 0x0011;
    public static final int ZEEVO_INC = 0x0012;
    public static final int ATMEL_CORPORATION = 0x0013;
    public static final int MITSUBISHI_ELECTRIC_CORPORATION = 0x0014;
    public static final int RTX_TELECOM_A_S = 0x0015;
    public static final int KC_TECHNOLOGY_INC = 0x0016;
    public static final int NEWLOGIC = 0x0017;
    public static final int TRANSILICA_INC = 0x0018;
    public static final int ROHDE_SCHWARZ_GMBH_CO_KG = 0x0019;
    public static final int TTPCOM_LIMITED = 0x001A;
    public static final int SIGNIA_TECHNOLOGIES_INC = 0x001B;
    public static final int CONEXANT_SYSTEMS_INC = 0x001C;
    public static final int QUALCOMM = 0x001D;
    public static final int INVENTEL = 0x001E;
    public static final int AVM_BERLIN = 0x001F;
    public static final int BANDSPEED_INC = 0x0020;
    public static final int MANSELLA_LTD = 0x0021;
    public static final int NEC_CORPORATION = 0x0022;
    public static final int WAVEPLUS_TECHNOLOGY_CO_LTD = 0x0023;
    public static final int ALCATEL = 0x0024;
    public static final int PHILIPS_SEMICONDUCTORS = 0x0025;
    public static final int C_TECHNOLOGIES = 0x0026;
    public static final int OPEN_INTERFACE = 0x0027;
    public static final int R_F_MICRO_DEVICES = 0x0028;
    public static final int HITACHI_LTD = 0x0029;
    public static final int SYMBOL_TECHNOLOGIES_INC = 0x002A;
    public static final int TENOVIS = 0x002B;
    public static final int MACRONIX_INTERNATIONAL_CO_LTD = 0x002C;
    public static final int GCT_SEMICONDUCTOR = 0x002D;
    public static final int NORWOOD_SYSTEMS = 0x002E;
    public static final int MEWTEL_TECHNOLOGY_INC = 0x002F;
    public static final int ST_MICROELECTRONICS = 0x0030;
    public static final int SYNOPSIS = 0x0031;
    public static final int REDM_COMMUNICATIONS_LTD = 0x0032;
    public static final int COMMIL_LTD = 0x0033;
    public static final int COMPUTER_ACCESS_TECHNOLOGY_CORPORATION_CATC = 0x0034;
    public static final int ECLIPSE_HQ_ESPANA_SL = 0x0035;
    public static final int RENESAS_TECHNOLOGY_CORP = 0x0036;
    public static final int MOBILIAN_CORPORATION = 0x0037;
    public static final int TERAX = 0x0038;
    public static final int INTEGRATED_SYSTEM_SOLUTION_CORP = 0x0039;
    public static final int MATSUSHITA_ELECTRIC_INDUSTRIAL_CO_LTD = 0x003A;
    public static final int GENNUM_CORPORATION = 0x003B;
    public static final int RESEARCH_IN_MOTION = 0x003C;
    public static final int IPEXTREME_INC = 0x003D;
    public static final int SYSTEMS_AND_CHIPS_INC = 0x003E;
    public static final int BLUETOOTH_SIG_INC = 0x003F;
    public static final int SEIKO_EPSON_CORPORATION = 0x0040;
    public static final int INTEGRATED_SILICON_SOLUTION_TAIWAN_INC = 0x0041;
    public static final int CONWISE_TECHNOLOGY_CORPORATION_LTD = 0x0042;
    public static final int PARROT_SA = 0x0043;
    public static final int SOCKET_MOBILE = 0x0044;
    public static final int ATHEROS_COMMUNICATIONS_INC = 0x0045;
    public static final int MEDIATEK_INC = 0x0046;
    public static final int BLUEGIGA = 0x0047;
    public static final int MARVELL_TECHNOLOGY_GROUP_LTD = 0x0048;
    public static final int THREE_DSP_CORPORATION = 0x0049;
    public static final int ACCEL_SEMICONDUCTOR_LTD = 0x004A;
    public static final int CONTINENTAL_AUTOMOTIVE_SYSTEMS = 0x004B;
    public static final int APPLE_INC = 0x004C;
    public static final int STACCATO_COMMUNICATIONS_INC = 0x004D;
    public static final int AVAGO_TECHNOLOGIES = 0x004E;
    public static final int APT_LICENSING_LTD = 0x004F;
    public static final int SIRF_TECHNOLOGY = 0x0050;
    public static final int TZERO_TECHNOLOGIES_INC = 0x0051;
    public static final int JM_CORPORATION = 0x0052;
    public static final int FREE2MOVE_AB = 0x0053;
    public static final int THREE_DIJOY_CORPORATION = 0x0054;
    public static final int PLANTRONICS_INC = 0x0055;
    public static final int SONY_ERICSSON_MOBILE_COMMUNICATIONS = 0x0056;
    public static final int HARMAN_INTERNATIONAL_INDUSTRIES_INC = 0x0057;
    public static final int VIZIO_INC = 0x0058;
    public static final int NORDIC_SEMICONDUCTOR_ASA = 0x0059;
    public static final int EM_MICROELECTRONICMARIN_SA = 0x005A;
    public static final int RALINK_TECHNOLOGY_CORPORATION = 0x005B;
    public static final int BELKIN_INTERNATIONAL_INC = 0x005C;
    public static final int REALTEK_SEMICONDUCTOR_CORPORATION = 0x005D;
    public static final int STONESTREET_ONE_LLC = 0x005E;
    public static final int WICENTRIC_INC = 0x005F;
    public static final int RIVIERAWAVES_SAS = 0x0060;
    public static final int RDA_MICROELECTRONICS = 0x0061;
    public static final int GIBSON_GUITARS = 0x0062;
    public static final int MICOMMAND_INC = 0x0063;
    public static final int BAND_XI_INTERNATIONAL_LLC = 0x0064;
    public static final int HEWLETTPACKARD_COMPANY = 0x0065;
    public static final int NINE_SOLUTIONS_OY = 0x0066;
    public static final int GN_NETCOM_A_S = 0x0067;
    public static final int GENERAL_MOTORS = 0x0068;
    public static final int AD_ENGINEERING_INC = 0x0069;
    public static final int MINDTREE_LTD = 0x006A;
    public static final int POLAR_ELECTRO_OY = 0x006B;
    public static final int BEAUTIFUL_ENTERPRISE_CO_LTD = 0x006C;
    public static final int BRIARTEK_INC = 0x006D;
    public static final int SUMMIT_DATA_COMMUNICATIONS_INC = 0x006E;
    public static final int SOUND_ID = 0x006F;
    public static final int MONSTER_LLC = 0x0070;
    public static final int CONNECTBLUE_AB = 0x0071;
    public static final int SHANGHAI_SUPER_SMART_ELECTRONICS_CO_LTD = 0x0072;
    public static final int GROUP_SENSE_LTD = 0x0073;
    public static final int ZOMM_LLC = 0x0074;
    public static final int SAMSUNG_ELECTRONICS_CO_LTD = 0x0075;
    public static final int CREATIVE_TECHNOLOGY_LTD = 0x0076;
    public static final int LAIRD_TECHNOLOGIES = 0x0077;
    public static final int NIKE_INC = 0x0078;
    public static final int LESSWIRE_AG = 0x0079;
    public static final int MSTAR_SEMICONDUCTOR_INC = 0x007A;
    public static final int HANLYNN_TECHNOLOGIES = 0x007B;
    public static final int A_R_CAMBRIDGE = 0x007C;
    public static final int SEERS_TECHNOLOGY_CO_LTD = 0x007D;
    public static final int SPORTS_TRACKING_TECHNOLOGIES_LTD = 0x007E;
    public static final int AUTONET_MOBILE = 0x007F;
    public static final int DELORME_PUBLISHING_COMPANY_INC = 0x0080;
    public static final int WUXI_VIMICRO = 0x0081;
    public static final int SENNHEISER_COMMUNICATIONS_A_S = 0x0082;
    public static final int TIMEKEEPING_SYSTEMS_INC = 0x0083;
    public static final int LUDUS_HELSINKI_LTD = 0x0084;
    public static final int BLUERADIOS_INC = 0x0085;
    public static final int EQUINOX_AG = 0x0086;
    public static final int GARMIN_INTERNATIONAL_INC = 0x0087;
    public static final int ECOTEST = 0x0088;
    public static final int GN_RESOUND_A_S = 0x0089;
    public static final int JAWBONE = 0x008A;
    public static final int TOPCORN_POSITIONING_SYSTEMS_LLC = 0x008B;
    public static final int QUALCOMM_RETAIL_SOLUTIONS_INC_FORMERLY_QUALCOMM_LABS_INC = 0x008C;
    public static final int ZSCAN_SOFTWARE = 0x008D;
    public static final int QUINTIC_CORP = 0x008E;
    public static final int STOLLMAN_EV_GMBH = 0x008F;
    public static final int FUNAI_ELECTRIC_CO_LTD = 0x0090;
    public static final int ADVANCED_PANMOBIL_SYSTEMS_GMBH_CO_KG = 0x0091;
    public static final int THINKOPTICS_INC = 0x0092;
    public static final int UNIVERSAL_ELECTRONICS_INC = 0x0093;
    public static final int AIROHA_TECHNOLOGY_CORP = 0x0094;
    public static final int NEC_LIGHTING_LTD = 0x0095;
    public static final int ODM_TECHNOLOGY_INC = 0x0096;
    public static final int CONNECTEDEVICE_LTD = 0x0097;
    public static final int ZER01TV_GMBH = 0x0098;
    public static final int ITECH_DYNAMIC_GLOBAL_DISTRIBUTION_LTD = 0x0099;
    public static final int ALPWISE = 0x009A;
    public static final int JIANGSU_TOPPOWER_AUTOMOTIVE_ELECTRONICS_CO_LTD = 0x009B;
    public static final int COLORFY_INC = 0x009C;
    public static final int GEOFORCE_INC = 0x009D;
    public static final int BOSE_CORPORATION = 0x009E;
    public static final int SUUNTO_OY = 0x009F;
    public static final int KENSINGTON_COMPUTER_PRODUCTS_GROUP = 0x00A0;
    public static final int SRMEDIZINELEKTRONIK = 0x00A1;
    public static final int VERTU_CORPORATION_LIMITED = 0x00A2;
    public static final int META_WATCH_LTD = 0x00A3;
    public static final int LINAK_A_S = 0x00A4;
    public static final int OTL_DYNAMICS_LLC = 0x00A5;
    public static final int PANDA_OCEAN_INC = 0x00A6;
    public static final int VISTEON_CORPORATION = 0x00A7;
    public static final int ARP_DEVICES_LIMITED = 0x00A8;
    public static final int MAGNETI_MARELLI_SPA = 0x00A9;
    public static final int CAEN_RFID_SRL = 0x00AA;
    public static final int INGENIEURSYSTEMGRUPPE_ZAHN_GMBH = 0x00AB;
    public static final int GREEN_THROTTLE_GAMES = 0x00AC;
    public static final int PETER_SYSTEMTECHNIK_GMBH = 0x00AD;
    public static final int OMEGAWAVE_OY = 0x00AE;
    public static final int CINETIX = 0x00AF;
    public static final int PASSIF_SEMICONDUCTOR_CORP = 0x00B0;
    public static final int SARIS_CYCLING_GROUP_INC = 0x00B1;
    public static final int BEKEY_A_S = 0x00B2;
    public static final int CLARINOX_TECHNOLOGIES_PTY_LTD = 0x00B3;
    public static final int BDE_TECHNOLOGY_CO_LTD = 0x00B4;
    public static final int SWIRL_NETWORKS = 0x00B5;
    public static final int MESO_INTERNATIONAL = 0x00B6;
    public static final int TRELAB_LTD = 0x00B7;
    public static final int QUALCOMM_INNOVATION_CENTER_INC_QUIC = 0x00B8;
    public static final int JOHNSON_CONTROLS_INC = 0x00B9;
    public static final int STARKEY_LABORATORIES_INC = 0x00BA;
    public static final int SPOWER_ELECTRONICS_LIMITED = 0x00BB;
    public static final int ACE_SENSOR_INC = 0x00BC;
    public static final int APLIX_CORPORATION = 0x00BD;
    public static final int AAMP_OF_AMERICA = 0x00BE;
    public static final int STALMART_TECHNOLOGY_LIMITED = 0x00BF;
    public static final int AMICCOM_ELECTRONICS_CORPORATION = 0x00C0;
    public static final int SHENZHEN_EXCELSECU_DATA_TECHNOLOGY_COLTD = 0x00C1;
    public static final int GENEQ_INC = 0x00C2;
    public static final int ADIDAS_AG = 0x00C3;
    public static final int LG_ELECTRONICS = 0x00C4;
    public static final int ONSET_COMPUTER_CORPORATION = 0x00C5;
    public static final int SELFLY_BV = 0x00C6;
    public static final int QUUPPA_OY = 0x00C7;
    public static final int GELO_INC = 0x00C8;
    public static final int EVLUMA = 0x00C9;
    public static final int MC10 = 0x00CA;
    public static final int BINAURIC_SE = 0x00CB;
    public static final int BEATS_ELECTRONICS = 0x00CC;
    public static final int MICROCHIP_TECHNOLOGY_INC = 0x00CD;
    public static final int ELGATO_SYSTEMS_GMBH = 0x00CE;
    public static final int ARCHOS_SA = 0x00CF;
    public static final int DEXCOM_INC = 0x00D0;
    public static final int POLAR_ELECTRO_EUROPE_BV = 0x00D1;
    public static final int DIALOG_SEMICONDUCTOR_BV = 0x00D2;
    public static final int TAIXINGBANG_TECHNOLOGY_HK_CO_LTD = 0x00D3;
    public static final int KAWANTECH = 0x00D4;
    public static final int AUSTCO_COMMUNICATION_SYSTEMS = 0x00D5;
    public static final int TIMEX_GROUP_USA_INC = 0x00D6;
    public static final int QUALCOMM_TECHNOLOGIES_INC = 0x00D7;
    public static final int QUALCOMM_CONNECTED_EXPERIENCES_INC = 0x00D8;
    public static final int VOYETRA_TURTLE_BEACH = 0x00D9;
    public static final int TXTR_GMBH = 0x00DA;
    public static final int BIOSENTRONICS = 0x00DB;
    public static final int PROCTER_GAMBLE = 0x00DC;
    public static final int HOSIDEN_CORPORATION = 0x00DD;
    public static final int MUZIK_LLC = 0x00DE;
    public static final int MISFIT_WEARABLES_CORP = 0x00DF;
    public static final int GOOGLE = 0x00E0;
    public static final int DANLERS_LTD = 0x00E1;
    public static final int SEMILINK_INC = 0x00E2;
    public static final int INMUSIC_BRANDS_INC = 0x00E3;
    public static final int LS_RESEARCH_INC = 0x00E4;
    public static final int EDEN_SOFTWARE_CONSULTANTS_LTD = 0x00E5;
    public static final int FRESHTEMP = 0x00E6;
    public static final int KS_TECHNOLOGIES = 0x00E7;
    public static final int ACTS_TECHNOLOGIES = 0x00E8;
    public static final int VTRACK_SYSTEMS = 0x00E9;
    public static final int NIELSENKELLERMAN_COMPANY = 0x00EA;
    public static final int SERVER_TECHNOLOGY_INC = 0x00EB;
    public static final int BIORESEARCH_ASSOCIATES = 0x00EC;
    public static final int JOLLY_LOGIC_LLC = 0x00ED;
    public static final int ABOVE_AVERAGE_OUTCOMES_INC = 0x00EE;
    public static final int BITSPLITTERS_GMBH = 0x00EF;
    public static final int PAYPAL_INC = 0x00F0;
    public static final int WITRON_TECHNOLOGY_LIMITED = 0x00F1;
    public static final int MORSE_PROJECT_INC = 0x00F2;
    public static final int KENT_DISPLAYS_INC = 0x00F3;
    public static final int NAUTILUS_INC = 0x00F4;
    public static final int SMARTIFIER_OY = 0x00F5;
    public static final int ELCOMETER_LIMITED = 0x00F6;
    public static final int VSN_TECHNOLOGIES_INC = 0x00F7;
    public static final int ACEUNI_CORP_LTD = 0x00F8;
    public static final int STICKNFIND = 0x00F9;
    public static final int CRYSTAL_CODE_AB = 0x00FA;
    public static final int KOUKAAM_AS = 0x00FB;
    public static final int DELPHI_CORPORATION = 0x00FC;
    public static final int VALENCETECH_LIMITED = 0x00FD;
    public static final int RESERVED = 0x00FE;
    public static final int TYPO_PRODUCTS_LLC = 0x00FF;
    public static final int TOMTOM_INTERNATIONAL_BV = 0x0100;
    public static final int FUGOO_INC = 0x0101;
    public static final int KEISER_CORPORATION = 0x0102;
    public static final int BANG_OLUFSEN_A_S = 0x0103;
    public static final int PLUS_LOCATIONS_SYSTEMS_PTY_LTD = 0x0104;
    public static final int UBIQUITOUS_COMPUTING_TECHNOLOGY_CORPORATION = 0x0105;
    public static final int INNOVATIVE_YACHTTER_SOLUTIONS = 0x0106;
    public static final int WILLIAM_DEMANT_HOLDING_A_S = 0x0107;
    public static final int CHICONY_ELECTRONICS_CO_LTD = 0x0108;
    public static final int ATUS_BV = 0x0109;
    public static final int CODEGATE_LTD = 0x010A;
    public static final int ERI_INC = 0x010B;
    public static final int TRANSDUCERS_DIRECT_LLC = 0x010C;
    public static final int FUJITSU_TEN_LIMITED = 0x010D;
    public static final int AUDI_AG = 0x010E;
    public static final int HISILICON_TECHNOLOGIES_CO_LTD = 0x010F;
    public static final int NIPPON_SEIKI_CO_LTD = 0x0110;
    public static final int STEELSERIES_APS = 0x0111;
    public static final int VYZYBL_INC = 0x0112;
    public static final int OPENBRAIN_TECHNOLOGIES_CO_LTD = 0x0113;
    public static final int XENSR = 0x0114;
    public static final int ESOLUTIONS = 0x0115;
    public static final int ONE_OAK_TECHNOLOGIES = 0x0116;
    public static final int WIMOTO_TECHNOLOGIES_INC = 0x0117;
    public static final int RADIUS_NETWORKS_INC = 0x0118;
    public static final int WIZE_TECHNOLOGY_CO_LTD = 0x0119;
    public static final int QUALCOMM_LABS_INC = 0x011A;
    public static final int ARUBA_NETWORKS = 0x011B;
    public static final int BAIDU = 0x011C;
    public static final int ARENDI_AG = 0x011D;
    public static final int SKODA_AUTO_AS = 0x011E;
    public static final int VOLKSWAGON_AG = 0x011F;
    public static final int PORSCHE_AG = 0x0120;
    public static final int SINO_WEALTH_ELECTRONIC_LTD = 0x0121;
    public static final int AIRTURN_INC = 0x0122;
    public static final int KINSA_INC = 0x0123;
    public static final int HID_GLOBAL = 0x0124;
    public static final int SEAT_ES = 0x0125;
    public static final int PROMETHEAN_LTD = 0x0126;
    public static final int SALUTICA_ALLIED_SOLUTIONS = 0x0127;
    public static final int GPSI_GROUP_PTY_LTD = 0x0128;
    public static final int NIMBLE_DEVICES_OY = 0x0129;
    public static final int CHANGZHOU_YONGSE_INFOTECH_CO_LTD = 0x012A;
    public static final int SPORTIQ = 0x012B;
    public static final int TEMEC_INSTRUMENTS_BV = 0x012C;
    public static final int SONY_CORPORATION = 0x012D;
    public static final int ASSA_ABLOY = 0x012E;
    public static final int CLARION_CO_LTD = 0x012F;
    public static final int WAREHOUSE_INNOVATIONS = 0x0130;
    public static final int CYPRESS_SEMICONDUCTOR_CORPORATION = 0x0131;
    public static final int MADS_INC = 0x0132;
    public static final int BLUE_MAESTRO_LIMITED = 0x0133;
    public static final int RESOLUTION_PRODUCTS_INC = 0x0134;
    public static final int AIREWEAR_LLC = 0x0135;
    public static final int ETC_SP_ZOO = 0x0136;
    public static final int PRESTIGIO_PLAZA_LTD = 0x0137;
    private static final SparseArray<String> COMPANY_NAME_MAP = populateCompanyNameMap();
    public static String getCompanyName(final int companyId, final String fallback) {
        final String name = COMPANY_NAME_MAP.get(companyId);
        return name == null ? fallback : name;
    }
    private static SparseArray<String> populateCompanyNameMap() {
        final SparseArray<String> map = new SparseArray<>();
        map.put(ERICSSON_TECHNOLOGY_LICENSING, "Ericsson Technology Licensing");
        map.put(NOKIA_MOBILE_PHONES, "Nokia Mobile Phones");
        map.put(INTEL_CORP, "Intel Corp.");
        map.put(IBM_CORP, "IBM Corp.");
        map.put(TOSHIBA_CORP, "Toshiba Corp.");
        map.put(THREE_COM, "3Com");
        map.put(MICROSOFT, "Microsoft");
        map.put(LUCENT, "Lucent");
        map.put(MOTOROLA, "Motorola");
        map.put(INFINEON_TECHNOLOGIES_AG, "Infineon Technologies AG");
        map.put(CAMBRIDGE_SILICON_RADIO, "Cambridge Silicon Radio");
        map.put(SILICON_WAVE, "Silicon Wave");
        map.put(DIGIANSWER_A_S, "Digianswer A/S");
        map.put(TEXAS_INSTRUMENTS_INC, "Texas Instruments Inc.");
        map.put(CEVA_INC_FORMERLY_PARTHUS_TECHNOLOGIES_INC, "Ceva, Inc. (formerly Parthus Technologies, Inc.)");
        map.put(BROADCOM_CORPORATION, "Broadcom Corporation");
        map.put(MITEL_SEMICONDUCTOR, "Mitel Semiconductor");
        map.put(WIDCOMM_INC, "Widcomm, Inc");
        map.put(ZEEVO_INC, "Zeevo, Inc.");
        map.put(ATMEL_CORPORATION, "Atmel Corporation");
        map.put(MITSUBISHI_ELECTRIC_CORPORATION, "Mitsubishi Electric Corporation");
        map.put(RTX_TELECOM_A_S, "RTX Telecom A/S");
        map.put(KC_TECHNOLOGY_INC, "KC Technology Inc.");
        map.put(NEWLOGIC, "NewLogic");
        map.put(TRANSILICA_INC, "Transilica, Inc.");
        map.put(ROHDE_SCHWARZ_GMBH_CO_KG, "Rohde & Schwarz GmbH & Co. KG");
        map.put(TTPCOM_LIMITED, "TTPCom Limited");
        map.put(SIGNIA_TECHNOLOGIES_INC, "Signia Technologies, Inc.");
        map.put(CONEXANT_SYSTEMS_INC, "Conexant Systems Inc.");
        map.put(QUALCOMM, "Qualcomm");
        map.put(INVENTEL, "Inventel");
        map.put(AVM_BERLIN, "AVM Berlin");
        map.put(BANDSPEED_INC, "BandSpeed, Inc.");
        map.put(MANSELLA_LTD, "Mansella Ltd");
        map.put(NEC_CORPORATION, "NEC Corporation");
        map.put(WAVEPLUS_TECHNOLOGY_CO_LTD, "WavePlus Technology Co., Ltd.");
        map.put(ALCATEL, "Alcatel");
        map.put(PHILIPS_SEMICONDUCTORS, "Philips Semiconductors");
        map.put(C_TECHNOLOGIES, "C Technologies");
        map.put(OPEN_INTERFACE, "Open Interface");
        map.put(R_F_MICRO_DEVICES, "R F Micro Devices");
        map.put(HITACHI_LTD, "Hitachi Ltd");
        map.put(SYMBOL_TECHNOLOGIES_INC, "Symbol Technologies, Inc.");
        map.put(TENOVIS, "Tenovis");
        map.put(MACRONIX_INTERNATIONAL_CO_LTD, "Macronix International Co. Ltd.");
        map.put(GCT_SEMICONDUCTOR, "GCT Semiconductor");
        map.put(NORWOOD_SYSTEMS, "Norwood Systems");
        map.put(MEWTEL_TECHNOLOGY_INC, "MewTel Technology Inc.");
        map.put(ST_MICROELECTRONICS, "ST Microelectronics");
        map.put(SYNOPSIS, "Synopsis");
        map.put(REDM_COMMUNICATIONS_LTD, "Red-M (Communications) Ltd");
        map.put(COMMIL_LTD, "Commil Ltd");
        map.put(COMPUTER_ACCESS_TECHNOLOGY_CORPORATION_CATC, "Computer Access Technology Corporation (CATC)");
        map.put(ECLIPSE_HQ_ESPANA_SL, "Eclipse (HQ Espana) S.L.");
        map.put(RENESAS_TECHNOLOGY_CORP, "Renesas Technology Corp.");
        map.put(MOBILIAN_CORPORATION, "Mobilian Corporation");
        map.put(TERAX, "Terax");
        map.put(INTEGRATED_SYSTEM_SOLUTION_CORP, "Integrated System Solution Corp.");
        map.put(MATSUSHITA_ELECTRIC_INDUSTRIAL_CO_LTD, "Matsushita Electric Industrial Co., Ltd.");
        map.put(GENNUM_CORPORATION, "Gennum Corporation");
        map.put(RESEARCH_IN_MOTION, "Research In Motion");
        map.put(IPEXTREME_INC, "IPextreme, Inc.");
        map.put(SYSTEMS_AND_CHIPS_INC, "Systems and Chips, Inc.");
        map.put(BLUETOOTH_SIG_INC, "Bluetooth SIG, Inc.");
        map.put(SEIKO_EPSON_CORPORATION, "Seiko Epson Corporation");
        map.put(INTEGRATED_SILICON_SOLUTION_TAIWAN_INC, "Integrated Silicon Solution Taiwan, Inc.");
        map.put(CONWISE_TECHNOLOGY_CORPORATION_LTD, "CONWISE Technology Corporation Ltd");
        map.put(PARROT_SA, "PARROT SA");
        map.put(SOCKET_MOBILE, "Socket Mobile");
        map.put(ATHEROS_COMMUNICATIONS_INC, "Atheros Communications, Inc.");
        map.put(MEDIATEK_INC, "MediaTek, Inc.");
        map.put(BLUEGIGA, "Bluegiga");
        map.put(MARVELL_TECHNOLOGY_GROUP_LTD, "Marvell Technology Group Ltd.");
        map.put(THREE_DSP_CORPORATION, "3DSP Corporation");
        map.put(ACCEL_SEMICONDUCTOR_LTD, "Accel Semiconductor Ltd.");
        map.put(CONTINENTAL_AUTOMOTIVE_SYSTEMS, "Continental Automotive Systems");
        map.put(APPLE_INC, "Apple, Inc.");
        map.put(STACCATO_COMMUNICATIONS_INC, "Staccato Communications, Inc.");
        map.put(AVAGO_TECHNOLOGIES, "Avago Technologies");
        map.put(APT_LICENSING_LTD, "APT Licensing Ltd.");
        map.put(SIRF_TECHNOLOGY, "SiRF Technology");
        map.put(TZERO_TECHNOLOGIES_INC, "Tzero Technologies, Inc.");
        map.put(JM_CORPORATION, "J&M Corporation");
        map.put(FREE2MOVE_AB, "Free2move AB");
        map.put(THREE_DIJOY_CORPORATION, "3DiJoy Corporation");
        map.put(PLANTRONICS_INC, "Plantronics, Inc.");
        map.put(SONY_ERICSSON_MOBILE_COMMUNICATIONS, "Sony Ericsson Mobile Communications");
        map.put(HARMAN_INTERNATIONAL_INDUSTRIES_INC, "Harman International Industries, Inc.");
        map.put(VIZIO_INC, "Vizio, Inc.");
        map.put(NORDIC_SEMICONDUCTOR_ASA, "Nordic Semiconductor ASA");
        map.put(EM_MICROELECTRONICMARIN_SA, "EM Microelectronic-Marin SA");
        map.put(RALINK_TECHNOLOGY_CORPORATION, "Ralink Technology Corporation");
        map.put(BELKIN_INTERNATIONAL_INC, "Belkin International, Inc.");
        map.put(REALTEK_SEMICONDUCTOR_CORPORATION, "Realtek Semiconductor Corporation");
        map.put(STONESTREET_ONE_LLC, "Stonestreet One, LLC");
        map.put(WICENTRIC_INC, "Wicentric, Inc.");
        map.put(RIVIERAWAVES_SAS, "RivieraWaves S.A.S");
        map.put(RDA_MICROELECTRONICS, "RDA Microelectronics");
        map.put(GIBSON_GUITARS, "Gibson Guitars");
        map.put(MICOMMAND_INC, "MiCommand Inc.");
        map.put(BAND_XI_INTERNATIONAL_LLC, "Band XI International, LLC");
        map.put(HEWLETTPACKARD_COMPANY, "Hewlett-Packard Company");
        map.put(NINE_SOLUTIONS_OY, "9Solutions Oy");
        map.put(GN_NETCOM_A_S, "GN Netcom A/S");
        map.put(GENERAL_MOTORS, "General Motors");
        map.put(AD_ENGINEERING_INC, "A&D Engineering, Inc.");
        map.put(MINDTREE_LTD, "MindTree Ltd.");
        map.put(POLAR_ELECTRO_OY, "Polar Electro OY");
        map.put(BEAUTIFUL_ENTERPRISE_CO_LTD, "Beautiful Enterprise Co., Ltd.");
        map.put(BRIARTEK_INC, "BriarTek, Inc.");
        map.put(SUMMIT_DATA_COMMUNICATIONS_INC, "Summit Data Communications, Inc.");
        map.put(SOUND_ID, "Sound ID");
        map.put(MONSTER_LLC, "Monster, LLC");
        map.put(CONNECTBLUE_AB, "connectBlue AB");
        map.put(SHANGHAI_SUPER_SMART_ELECTRONICS_CO_LTD, "ShangHai Super Smart Electronics Co. Ltd.");
        map.put(GROUP_SENSE_LTD, "Group Sense Ltd.");
        map.put(ZOMM_LLC, "Zomm, LLC");
        map.put(SAMSUNG_ELECTRONICS_CO_LTD, "Samsung Electronics Co. Ltd.");
        map.put(CREATIVE_TECHNOLOGY_LTD, "Creative Technology Ltd.");
        map.put(LAIRD_TECHNOLOGIES, "Laird Technologies");
        map.put(NIKE_INC, "Nike, Inc.");
        map.put(LESSWIRE_AG, "lesswire AG");
        map.put(MSTAR_SEMICONDUCTOR_INC, "MStar Semiconductor, Inc.");
        map.put(HANLYNN_TECHNOLOGIES, "Hanlynn Technologies");
        map.put(A_R_CAMBRIDGE, "A & R Cambridge");
        map.put(SEERS_TECHNOLOGY_CO_LTD, "Seers Technology Co. Ltd");
        map.put(SPORTS_TRACKING_TECHNOLOGIES_LTD, "Sports Tracking Technologies Ltd.");
        map.put(AUTONET_MOBILE, "Autonet Mobile");
        map.put(DELORME_PUBLISHING_COMPANY_INC, "DeLorme Publishing Company, Inc.");
        map.put(WUXI_VIMICRO, "WuXi Vimicro");
        map.put(SENNHEISER_COMMUNICATIONS_A_S, "Sennheiser Communications A/S");
        map.put(TIMEKEEPING_SYSTEMS_INC, "TimeKeeping Systems, Inc.");
        map.put(LUDUS_HELSINKI_LTD, "Ludus Helsinki Ltd.");
        map.put(BLUERADIOS_INC, "BlueRadios, Inc.");
        map.put(EQUINOX_AG, "equinox AG");
        map.put(GARMIN_INTERNATIONAL_INC, "Garmin International, Inc.");
        map.put(ECOTEST, "Ecotest");
        map.put(GN_RESOUND_A_S, "GN ReSound A/S");
        map.put(JAWBONE, "Jawbone");
        map.put(TOPCORN_POSITIONING_SYSTEMS_LLC, "Topcorn Positioning Systems, LLC");
        map.put(QUALCOMM_RETAIL_SOLUTIONS_INC_FORMERLY_QUALCOMM_LABS_INC, "Qualcomm Retail Solutions, Inc. (formerly Qualcomm Labs, Inc.)");
        map.put(ZSCAN_SOFTWARE, "Zscan Software");
        map.put(QUINTIC_CORP, "Quintic Corp.");
        map.put(STOLLMAN_EV_GMBH, "Stollman E+V GmbH");
        map.put(FUNAI_ELECTRIC_CO_LTD, "Funai Electric Co., Ltd.");
        map.put(ADVANCED_PANMOBIL_SYSTEMS_GMBH_CO_KG, "Advanced PANMOBIL Systems GmbH & Co. KG");
        map.put(THINKOPTICS_INC, "ThinkOptics, Inc.");
        map.put(UNIVERSAL_ELECTRONICS_INC, "Universal Electronics, Inc.");
        map.put(AIROHA_TECHNOLOGY_CORP, "Airoha Technology Corp.");
        map.put(NEC_LIGHTING_LTD, "NEC Lighting, Ltd.");
        map.put(ODM_TECHNOLOGY_INC, "ODM Technology, Inc.");
        map.put(CONNECTEDEVICE_LTD, "ConnecteDevice Ltd.");
        map.put(ZER01TV_GMBH, "zer01.tv GmbH");
        map.put(ITECH_DYNAMIC_GLOBAL_DISTRIBUTION_LTD, "i.Tech Dynamic Global Distribution Ltd.");
        map.put(ALPWISE, "Alpwise");
        map.put(JIANGSU_TOPPOWER_AUTOMOTIVE_ELECTRONICS_CO_LTD, "Jiangsu Toppower Automotive Electronics Co., Ltd.");
        map.put(COLORFY_INC, "Colorfy, Inc.");
        map.put(GEOFORCE_INC, "Geoforce Inc.");
        map.put(BOSE_CORPORATION, "Bose Corporation");
        map.put(SUUNTO_OY, "Suunto Oy");
        map.put(KENSINGTON_COMPUTER_PRODUCTS_GROUP, "Kensington Computer Products Group");
        map.put(SRMEDIZINELEKTRONIK, "SR-Medizinelektronik");
        map.put(VERTU_CORPORATION_LIMITED, "Vertu Corporation Limited");
        map.put(META_WATCH_LTD, "Meta Watch Ltd.");
        map.put(LINAK_A_S, "LINAK A/S");
        map.put(OTL_DYNAMICS_LLC, "OTL Dynamics LLC");
        map.put(PANDA_OCEAN_INC, "Panda Ocean Inc.");
        map.put(VISTEON_CORPORATION, "Visteon Corporation");
        map.put(ARP_DEVICES_LIMITED, "ARP Devices Limited");
        map.put(MAGNETI_MARELLI_SPA, "Magneti Marelli S.p.A");
        map.put(CAEN_RFID_SRL, "CAEN RFID srl");
        map.put(INGENIEURSYSTEMGRUPPE_ZAHN_GMBH, "Ingenieur-Systemgruppe Zahn GmbH");
        map.put(GREEN_THROTTLE_GAMES, "Green Throttle Games");
        map.put(PETER_SYSTEMTECHNIK_GMBH, "Peter Systemtechnik GmbH");
        map.put(OMEGAWAVE_OY, "Omegawave Oy");
        map.put(CINETIX, "Cinetix");
        map.put(PASSIF_SEMICONDUCTOR_CORP, "Passif Semiconductor Corp");
        map.put(SARIS_CYCLING_GROUP_INC, "Saris Cycling Group, Inc");
        map.put(BEKEY_A_S, "Bekey A/S");
        map.put(CLARINOX_TECHNOLOGIES_PTY_LTD, "Clarinox Technologies Pty. Ltd.");
        map.put(BDE_TECHNOLOGY_CO_LTD, "BDE Technology Co., Ltd.");
        map.put(SWIRL_NETWORKS, "Swirl Networks");
        map.put(MESO_INTERNATIONAL, "Meso international");
        map.put(TRELAB_LTD, "TreLab Ltd");
        map.put(QUALCOMM_INNOVATION_CENTER_INC_QUIC, "Qualcomm Innovation Center, Inc. (QuIC)");
        map.put(JOHNSON_CONTROLS_INC, "Johnson Controls, Inc.");
        map.put(STARKEY_LABORATORIES_INC, "Starkey Laboratories Inc.");
        map.put(SPOWER_ELECTRONICS_LIMITED, "S-Power Electronics Limited");
        map.put(ACE_SENSOR_INC, "Ace Sensor Inc");
        map.put(APLIX_CORPORATION, "Aplix Corporation");
        map.put(AAMP_OF_AMERICA, "AAMP of America");
        map.put(STALMART_TECHNOLOGY_LIMITED, "Stalmart Technology Limited");
        map.put(AMICCOM_ELECTRONICS_CORPORATION, "AMICCOM Electronics Corporation");
        map.put(SHENZHEN_EXCELSECU_DATA_TECHNOLOGY_COLTD, "Shenzhen Excelsecu Data Technology Co.,Ltd");
        map.put(GENEQ_INC, "Geneq Inc.");
        map.put(ADIDAS_AG, "adidas AG");
        map.put(LG_ELECTRONICS, "LG Electronics");
        map.put(ONSET_COMPUTER_CORPORATION, "Onset Computer Corporation");
        map.put(SELFLY_BV, "Selfly BV");
        map.put(QUUPPA_OY, "Quuppa Oy.");
        map.put(GELO_INC, "GeLo Inc");
        map.put(EVLUMA, "Evluma");
        map.put(MC10, "MC10");
        map.put(BINAURIC_SE, "Binauric SE");
        map.put(BEATS_ELECTRONICS, "Beats Electronics");
        map.put(MICROCHIP_TECHNOLOGY_INC, "Microchip Technology Inc.");
        map.put(ELGATO_SYSTEMS_GMBH, "Elgato Systems GmbH");
        map.put(ARCHOS_SA, "ARCHOS SA");
        map.put(DEXCOM_INC, "Dexcom, Inc.");
        map.put(POLAR_ELECTRO_EUROPE_BV, "Polar Electro Europe B.V.");
        map.put(DIALOG_SEMICONDUCTOR_BV, "Dialog Semiconductor B.V.");
        map.put(TAIXINGBANG_TECHNOLOGY_HK_CO_LTD, "Taixingbang Technology (HK) Co,. LTD.");
        map.put(KAWANTECH, "Kawantech");
        map.put(AUSTCO_COMMUNICATION_SYSTEMS, "Austco Communication Systems");
        map.put(TIMEX_GROUP_USA_INC, "Timex Group USA, Inc.");
        map.put(QUALCOMM_TECHNOLOGIES_INC, "Qualcomm Technologies, Inc.");
        map.put(QUALCOMM_CONNECTED_EXPERIENCES_INC, "Qualcomm Connected Experiences, Inc.");
        map.put(VOYETRA_TURTLE_BEACH, "Voyetra Turtle Beach");
        map.put(TXTR_GMBH, "txtr GmbH");
        map.put(BIOSENTRONICS, "Biosentronics");
        map.put(PROCTER_GAMBLE, "Procter & Gamble");
        map.put(HOSIDEN_CORPORATION, "Hosiden Corporation");
        map.put(MUZIK_LLC, "Muzik LLC");
        map.put(MISFIT_WEARABLES_CORP, "Misfit Wearables Corp");
        map.put(GOOGLE, "Google");
        map.put(DANLERS_LTD, "Danlers Ltd");
        map.put(SEMILINK_INC, "Semilink Inc");
        map.put(INMUSIC_BRANDS_INC, "inMusic Brands, Inc");
        map.put(LS_RESEARCH_INC, "L.S. Research Inc.");
        map.put(EDEN_SOFTWARE_CONSULTANTS_LTD, "Eden Software Consultants Ltd.");
        map.put(FRESHTEMP, "Freshtemp");
        map.put(KS_TECHNOLOGIES, "KS Technologies");
        map.put(ACTS_TECHNOLOGIES, "ACTS Technologies");
        map.put(VTRACK_SYSTEMS, "Vtrack Systems");
        map.put(NIELSENKELLERMAN_COMPANY, "Nielsen-Kellerman Company");
        map.put(SERVER_TECHNOLOGY_INC, "Server Technology, Inc.");
        map.put(BIORESEARCH_ASSOCIATES, "BioResearch Associates");
        map.put(JOLLY_LOGIC_LLC, "Jolly Logic, LLC");
        map.put(ABOVE_AVERAGE_OUTCOMES_INC, "Above Average Outcomes, Inc.");
        map.put(BITSPLITTERS_GMBH, "Bitsplitters GmbH");
        map.put(PAYPAL_INC, "PayPal, Inc.");
        map.put(WITRON_TECHNOLOGY_LIMITED, "Witron Technology Limited");
        map.put(MORSE_PROJECT_INC, "Morse Project Inc.");
        map.put(KENT_DISPLAYS_INC, "Kent Displays Inc.");
        map.put(NAUTILUS_INC, "Nautilus Inc.");
        map.put(SMARTIFIER_OY, "Smartifier Oy");
        map.put(ELCOMETER_LIMITED, "Elcometer Limited");
        map.put(VSN_TECHNOLOGIES_INC, "VSN Technologies Inc.");
        map.put(ACEUNI_CORP_LTD, "AceUni Corp., Ltd.");
        map.put(STICKNFIND, "StickNFind");
        map.put(CRYSTAL_CODE_AB, "Crystal Code AB");
        map.put(KOUKAAM_AS, "KOUKAAM a.s.");
        map.put(DELPHI_CORPORATION, "Delphi Corporation");
        map.put(VALENCETECH_LIMITED, "ValenceTech Limited");
        map.put(RESERVED, "Reserved");
        map.put(TYPO_PRODUCTS_LLC, "Typo Products, LLC");
        map.put(TOMTOM_INTERNATIONAL_BV, "TomTom International BV");
        map.put(FUGOO_INC, "Fugoo, Inc");
        map.put(KEISER_CORPORATION, "Keiser Corporation");
        map.put(BANG_OLUFSEN_A_S, "Bang & Olufsen A/S");
        map.put(PLUS_LOCATIONS_SYSTEMS_PTY_LTD, "PLUS Locations Systems Pty Ltd");
        map.put(UBIQUITOUS_COMPUTING_TECHNOLOGY_CORPORATION, "Ubiquitous Computing Technology Corporation");
        map.put(INNOVATIVE_YACHTTER_SOLUTIONS, "Innovative Yachtter Solutions");
        map.put(WILLIAM_DEMANT_HOLDING_A_S, "William Demant Holding A/S");
        map.put(CHICONY_ELECTRONICS_CO_LTD, "Chicony Electronics Co., Ltd.");
        map.put(ATUS_BV, "Atus BV");
        map.put(CODEGATE_LTD, "Codegate Ltd.");
        map.put(ERI_INC, "ERi, Inc.");
        map.put(TRANSDUCERS_DIRECT_LLC, "Transducers Direct, LLC");
        map.put(FUJITSU_TEN_LIMITED, "Fujitsu Ten Limited");
        map.put(AUDI_AG, "Audi AG");
        map.put(HISILICON_TECHNOLOGIES_CO_LTD, "HiSilicon Technologies Co., Ltd.");
        map.put(NIPPON_SEIKI_CO_LTD, "Nippon Seiki Co., Ltd.");
        map.put(STEELSERIES_APS, "Steelseries ApS");
        map.put(VYZYBL_INC, "vyzybl Inc.");
        map.put(OPENBRAIN_TECHNOLOGIES_CO_LTD, "Openbrain Technologies, Co., Ltd.");
        map.put(XENSR, "Xensr");
        map.put(ESOLUTIONS, "e.solutions");
        map.put(ONE_OAK_TECHNOLOGIES, "1OAK Technologies");
        map.put(WIMOTO_TECHNOLOGIES_INC, "Wimoto Technologies Inc");
        map.put(RADIUS_NETWORKS_INC, "Radius Networks, Inc.");
        map.put(WIZE_TECHNOLOGY_CO_LTD, "Wize Technology Co., Ltd.");
        map.put(QUALCOMM_LABS_INC, "Qualcomm Labs, Inc.");
        map.put(ARUBA_NETWORKS, "Aruba Networks");
        map.put(BAIDU, "Baidu");
        map.put(ARENDI_AG, "Arendi AG");
        map.put(SKODA_AUTO_AS, "Skoda Auto a.s.");
        map.put(VOLKSWAGON_AG, "Volkswagon AG");
        map.put(PORSCHE_AG, "Porsche AG");
        map.put(SINO_WEALTH_ELECTRONIC_LTD, "Sino Wealth Electronic Ltd.");
        map.put(AIRTURN_INC, "AirTurn, Inc.");
        map.put(KINSA_INC, "Kinsa, Inc.");
        map.put(HID_GLOBAL, "HID Global");
        map.put(SEAT_ES, "SEAT es");
        map.put(PROMETHEAN_LTD, "Promethean Ltd.");
        map.put(SALUTICA_ALLIED_SOLUTIONS, "Salutica Allied Solutions");
        map.put(GPSI_GROUP_PTY_LTD, "GPSI Group Pty Ltd");
        map.put(NIMBLE_DEVICES_OY, "Nimble Devices Oy");
        map.put(CHANGZHOU_YONGSE_INFOTECH_CO_LTD, "Changzhou Yongse Infotech Co., Ltd");
        map.put(SPORTIQ, "SportIQ");
        map.put(TEMEC_INSTRUMENTS_BV, "TEMEC Instruments B.V.");
        map.put(SONY_CORPORATION, "Sony Corporation");
        map.put(ASSA_ABLOY, "ASSA ABLOY");
        map.put(CLARION_CO_LTD, "Clarion Co., Ltd.");
        map.put(WAREHOUSE_INNOVATIONS, "Warehouse Innovations");
        map.put(CYPRESS_SEMICONDUCTOR_CORPORATION, "Cypress Semiconductor Corporation");
        map.put(MADS_INC, "MADS Inc");
        map.put(BLUE_MAESTRO_LIMITED, "Blue Maestro Limited");
        map.put(RESOLUTION_PRODUCTS_INC, "Resolution Products, Inc.");
        map.put(AIREWEAR_LLC, "Airewear LLC");
        map.put(ETC_SP_ZOO, "ETC sp. z.o.o.");
        map.put(PRESTIGIO_PLAZA_LTD, "Prestigio Plaza Ltd.");
        return map;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/device/resolvers/GattAttributeResolver.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,380 @@
package com.blakequ.bluetooth_manager_lib.device.resolvers;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
 * The UUIDS have been collected from the following sources:
 * <p>
 * - http://developer.nokia.com/community/wiki/Bluetooth_Services_for_Windows_Phone
 * - The Bluez project
 *
 * @author Alexandros Schillings
 */
public class GattAttributeResolver {
    public static final String BASE_GUID = "00000000-0000-1000-8000-00805f9b34fb";
    public static final String SERVICE_DISCOVERY_PROTOCOL_SDP = "00000001-0000-1000-8000-00805f9b34fb";
    public static final String USER_DATAGRAM_PROTOCOL_UDP = "00000002-0000-1000-8000-00805f9b34fb";
    public static final String RADIO_FREQUENCY_COMMUNICATION_PROTOCOL_RFCOMM = "00000003-0000-1000-8000-00805f9b34fb";
    public static final String TCP = "00000004-0000-1000-8000-00805f9b34fb";
    public static final String TCSBIN = "00000005-0000-1000-8000-00805f9b34fb";
    public static final String TCSAT = "00000006-0000-1000-8000-00805f9b34fb";
    public static final String OBJECT_EXCHANGE_PROTOCOL_OBEX = "00000008-0000-1000-8000-00805f9b34fb";
    public static final String IP = "00000009-0000-1000-8000-00805f9b34fb";
    public static final String FTP = "0000000a-0000-1000-8000-00805f9b34fb";
    public static final String HTTP = "0000000c-0000-1000-8000-00805f9b34fb";
    public static final String WSP = "0000000e-0000-1000-8000-00805f9b34fb";
    public static final String BNEP_SVC = "0000000f-0000-1000-8000-00805f9b34fb";
    public static final String UPNP_PROTOCOL = "00000010-0000-1000-8000-00805f9b34fb";
    public static final String HIDP = "00000011-0000-1000-8000-00805f9b34fb";
    public static final String HARDCOPY_CONTROL_CHANNEL_PROTOCOL = "00000012-0000-1000-8000-00805f9b34fb";
    public static final String HARDCOPY_DATA_CHANNEL_PROTOCOL = "00000014-0000-1000-8000-00805f9b34fb";
    public static final String HARDCOPY_NOTIFICATION_PROTOCOL = "00000016-0000-1000-8000-00805f9b34fb";
    public static final String VCTP_PROTOCOL = "00000017-0000-1000-8000-00805f9b34fb";
    public static final String VDTP_PROTOCOL = "00000019-0000-1000-8000-00805f9b34fb";
    public static final String CMPT_PROTOCOL = "0000001b-0000-1000-8000-00805f9b34fb";
    public static final String UDI_C_PLANE_PROTOCOL = "0000001d-0000-1000-8000-00805f9b34fb";
    public static final String MCAP_CONTROL_CHANNEL = "0000001e-0000-1000-8000-00805f9b34fb";
    public static final String MCAP_DATA_CHANNEL = "0000001f-0000-1000-8000-00805f9b34fb";
    public static final String L2CAP = "00000100-0000-1000-8000-00805f9b34fb";
    public static final String SERVICE_DISCOVERY_SERVER = "00001000-0000-1000-8000-00805f9b34fb";
    public static final String BROWSE_GROUP_DESCRIPTOR = "00001001-0000-1000-8000-00805f9b34fb";
    public static final String PUBLIC_BROWSE_GROUP = "00001002-0000-1000-8000-00805f9b34fb";
    public static final String SPP = "00001101-0000-1000-8000-00805f9b34fb";
    public static final String LAN_ACCESS_USING_PPP = "00001102-0000-1000-8000-00805f9b34fb";
    public static final String DUN_GW = "00001103-0000-1000-8000-00805f9b34fb";
    public static final String OBEX_SYNC = "00001104-0000-1000-8000-00805f9b34fb";
    public static final String OBEX_OBJECT_PUSH = "00001105-0000-1000-8000-00805f9b34fb";
    public static final String OBEX_FILE_TRANSFER = "00001106-0000-1000-8000-00805f9b34fb";
    public static final String IRMC_SYNC_COMMAND = "00001107-0000-1000-8000-00805f9b34fb";
    public static final String HSP_HS = "00001108-0000-1000-8000-00805f9b34fb";
    public static final String CORDLESS_TELEPHONY = "00001109-0000-1000-8000-00805f9b34fb";
    public static final String AUDIO_SOURCE = "0000110a-0000-1000-8000-00805f9b34fb";
    public static final String AUDIO_SINK = "0000110b-0000-1000-8000-00805f9b34fb";
    public static final String AV_REMOTE_CONTROL_TARGET = "0000110c-0000-1000-8000-00805f9b34fb";
    public static final String ADVANCED_AUDIO = "0000110d-0000-1000-8000-00805f9b34fb";
    public static final String AVRCP_REMOTE = "0000110e-0000-1000-8000-00805f9b34fb";
    public static final String VIDEO_CONFERENCING = "0000110f-0000-1000-8000-00805f9b34fb";
    public static final String INTERCOM = "00001110-0000-1000-8000-00805f9b34fb";
    public static final String FAX = "00001111-0000-1000-8000-00805f9b34fb";
    public static final String HEADSET_PROFILE_HSP_AUDIO_GATEWAY = "00001112-0000-1000-8000-00805f9b34fb";
    public static final String WAP = "00001113-0000-1000-8000-00805f9b34fb";
    public static final String WAP_CLIENT = "00001114-0000-1000-8000-00805f9b34fb";
    public static final String PANU = "00001115-0000-1000-8000-00805f9b34fb";
    public static final String NAP = "00001116-0000-1000-8000-00805f9b34fb";
    public static final String GN = "00001117-0000-1000-8000-00805f9b34fb";
    public static final String DIRECT_PRINTING = "00001118-0000-1000-8000-00805f9b34fb";
    public static final String REFERENCE_PRINTING = "00001119-0000-1000-8000-00805f9b34fb";
    public static final String IMAGING = "0000111a-0000-1000-8000-00805f9b34fb";
    public static final String IMAGING_RESPONDER = "0000111b-0000-1000-8000-00805f9b34fb";
    public static final String IMAGING_AUTOMATIC_ARCHIVE = "0000111c-0000-1000-8000-00805f9b34fb";
    public static final String IMAGING_REFERENCE_OBJECTS = "0000111d-0000-1000-8000-00805f9b34fb";
    public static final String HANDS_FREE_PROFILE_HFP = "0000111e-0000-1000-8000-00805f9b34fb";
    public static final String HANDS_FREE_PROFILE_HFP_AUDIO_GATEWAY = "0000111f-0000-1000-8000-00805f9b34fb";
    public static final String DIRECT_PRINTING_REFERENCE_OBJECTS = "00001120-0000-1000-8000-00805f9b34fb";
    public static final String REFLECTED_UI = "00001121-0000-1000-8000-00805f9b34fb";
    public static final String BASIC_PRINTING = "00001122-0000-1000-8000-00805f9b34fb";
    public static final String PRINTING_STATUS = "00001123-0000-1000-8000-00805f9b34fb";
    public static final String HID = "00001124-0000-1000-8000-00805f9b34fb";
    public static final String HARDCOPY_CABLE_REPLACEMENT = "00001125-0000-1000-8000-00805f9b34fb";
    public static final String HCR_PRINT = "00001126-0000-1000-8000-00805f9b34fb";
    public static final String HCR_SCAN = "00001127-0000-1000-8000-00805f9b34fb";
    public static final String COMMON_ISDN_ACCESS = "00001128-0000-1000-8000-00805f9b34fb";
    public static final String VIDEO_CONFERENCING_GATEWAY = "00001129-0000-1000-8000-00805f9b34fb";
    public static final String UDIMT = "0000112a-0000-1000-8000-00805f9b34fb";
    public static final String UDITA = "0000112b-0000-1000-8000-00805f9b34fb";
    public static final String AUDIO_VIDEO = "0000112c-0000-1000-8000-00805f9b34fb";
    public static final String SIM_ACCESS = "0000112d-0000-1000-8000-00805f9b34fb";
    public static final String OBEX_PCE = "0000112e-0000-1000-8000-00805f9b34fb";
    public static final String OBEX_PSE = "0000112f-0000-1000-8000-00805f9b34fb";
    public static final String OBEX_PBAP = "00001130-0000-1000-8000-00805f9b34fb";
    public static final String OBEX_MAS = "00001132-0000-1000-8000-00805f9b34fb";
    public static final String OBEX_MNS = "00001133-0000-1000-8000-00805f9b34fb";
    public static final String OBEX_MAP = "00001134-0000-1000-8000-00805f9b34fb";
    public static final String PNP = "00001200-0000-1000-8000-00805f9b34fb";
    public static final String GENERIC_NETWORKING = "00001201-0000-1000-8000-00805f9b34fb";
    public static final String GENERIC_FILE_TRANSFER = "00001202-0000-1000-8000-00805f9b34fb";
    public static final String GENERIC_AUDIO = "00001203-0000-1000-8000-00805f9b34fb";
    public static final String GENERIC_TELEPHONY = "00001204-0000-1000-8000-00805f9b34fb";
    public static final String UPNP = "00001205-0000-1000-8000-00805f9b34fb";
    public static final String UPNP_IP = "00001206-0000-1000-8000-00805f9b34fb";
    public static final String ESDP_UPNP_IP_PAN = "00001300-0000-1000-8000-00805f9b34fb";
    public static final String ESDP_UPNP_IP_LAP = "00001301-0000-1000-8000-00805f9b34fb";
    public static final String ESDP_UPNP_L2CAP = "00001302-0000-1000-8000-00805f9b34fb";
    public static final String VIDEO_DISTRIBUTION_PROFILE_VDP_SOURCE = "00001303-0000-1000-8000-00805f9b34fb";
    public static final String VIDEO_DISTRIBUTION_PROFILE_VDP_SINK = "00001304-0000-1000-8000-00805f9b34fb";
    public static final String VIDEO_DISTRIBUTION_PROFILE_VDP = "00001305-0000-1000-8000-00805f9b34fb";
    public static final String HEALTH_DEVICE_PROFILE_HDP = "00001400-0000-1000-8000-00805f9b34fb";
    public static final String HEALTH_DEVICE_PROFILE_HDP_SOURCE = "00001401-0000-1000-8000-00805f9b34fb";
    public static final String HEALTH_DEVICE_PROFILE_HDP_SINK = "00001402-0000-1000-8000-00805f9b34fb";
    public static final String GAP = "00001800-0000-1000-8000-00805f9b34fb";
    public static final String GATT = "00001801-0000-1000-8000-00805f9b34fb";
    public static final String IMMEDIATE_ALERT = "00001802-0000-1000-8000-00805f9b34fb";
    public static final String LINK_LOSS = "00001803-0000-1000-8000-00805f9b34fb";
    public static final String TX_POWER = "00001804-0000-1000-8000-00805f9b34fb";
    public static final String HEALTH_THERMOMETER = "00001809-0000-1000-8000-00805f9b34fb";
    public static final String DEVICE_INFORMATION = "0000180a-0000-1000-8000-00805f9b34fb";
    public static final String HEART_RATE = "0000180d-0000-1000-8000-00805f9b34fb";
    public static final String CYCLING_SC = "00001816-0000-1000-8000-00805f9b34fb";
    public static final String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";
    public static final String DEVICE_NAME = "00002a00-0000-1000-8000-00805f9b34fb";
    public static final String APPEARANCE = "00002a01-0000-1000-8000-00805f9b34fb";
    public static final String PERIPHERAL_PRIVACY_FLAG = "00002a02-0000-1000-8000-00805f9b34fb";
    public static final String RECONNECTION_ADDRESS = "00002a03-0000-1000-8000-00805f9b34fb";
    public static final String PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS = "00002a04-0000-1000-8000-00805f9b34fb";
    public static final String SERVICE_CHANGED = "00002a05-0000-1000-8000-00805f9b34fb";
    public static final String ALERT_LEVEL = "00002a06-0000-1000-8000-00805f9b34fb";
    public static final String TX_POWER_LEVEL = "00002a07-0000-1000-8000-00805f9b34fb";
    public static final String DATE_TIME = "00002a08-0000-1000-8000-00805f9b34fb";
    public static final String DAY_OF_WEEK = "00002a09-0000-1000-8000-00805f9b34fb";
    public static final String DAY_DATE_TIME = "00002a0a-0000-1000-8000-00805f9b34fb";
    public static final String EXACT_TIME_256 = "00002a0c-0000-1000-8000-00805f9b34fb";
    public static final String DST_OFFSET = "00002a0d-0000-1000-8000-00805f9b34fb";
    public static final String TIME_ZONE = "00002a0e-0000-1000-8000-00805f9b34fb";
    public static final String LOCAL_TIME_INFORMATION = "00002a0f-0000-1000-8000-00805f9b34fb";
    public static final String TIME_WITH_DST = "00002a11-0000-1000-8000-00805f9b34fb";
    public static final String TIME_ACCURACY = "00002a12-0000-1000-8000-00805f9b34fb";
    public static final String TIME_SOURCE = "00002a13-0000-1000-8000-00805f9b34fb";
    public static final String REFERENCE_TIME_INFORMATION = "00002a14-0000-1000-8000-00805f9b34fb";
    public static final String TIME_UPDATE_CONTROL_POINT = "00002a16-0000-1000-8000-00805f9b34fb";
    public static final String TIME_UPDATE_STATE = "00002a17-0000-1000-8000-00805f9b34fb";
    public static final String TEMPERATURE_MEASUREMENT = "00002a1c-0000-1000-8000-00805f9b34fb";
    public static final String TEMPERATURE_TYPE = "00002a1d-0000-1000-8000-00805f9b34fb";
    public static final String INTERMEDIATE_TEMPERATURE = "00002a1e-0000-1000-8000-00805f9b34fb";
    public static final String MEASUREMENT_INTERVAL = "00002a21-0000-1000-8000-00805f9b34fb";
    public static final String SYSTEM_ID = "00002a23-0000-1000-8000-00805f9b34fb";
    public static final String MODEL_NUMBER_STRING = "00002a24-0000-1000-8000-00805f9b34fb";
    public static final String SERIAL_NUMBER_STRING = "00002a25-0000-1000-8000-00805f9b34fb";
    public static final String FIRMWARE_REVISION_STRING = "00002a26-0000-1000-8000-00805f9b34fb";
    public static final String HARDWARE_REVISION_STRING = "00002a27-0000-1000-8000-00805f9b34fb";
    public static final String SOFTWARE_REVISION_STRING = "00002a28-0000-1000-8000-00805f9b34fb";
    public static final String MANUFACTURER_NAME_STRING = "00002a29-0000-1000-8000-00805f9b34fb";
    public static final String IEEE_1107320601_REGULATORY = "00002a2a-0000-1000-8000-00805f9b34fb";
    public static final String CURRENT_TIME = "00002a2b-0000-1000-8000-00805f9b34fb";
    public static final String BLOOD_PRESSURE_MEASUREMENT = "00002a35-0000-1000-8000-00805f9b34fb";
    public static final String INTERMEDIATE_CUFF_PRESSURE = "00002a36-0000-1000-8000-00805f9b34fb";
    public static final String HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb";
    public static final String BODY_SENSOR_LOCATION = "00002a38-0000-1000-8000-00805f9b34fb";
    public static final String HEART_RATE_CONTROL_POINT = "00002a39-0000-1000-8000-00805f9b34fb";
    public static final String ALERT_STATUS = "00002a3f-0000-1000-8000-00805f9b34fb";
    public static final String RINGER_CONTROL_POINT = "00002a40-0000-1000-8000-00805f9b34fb";
    public static final String RINGER_SETTING = "00002a41-0000-1000-8000-00805f9b34fb";
    public static final String ALERT_CATEGORY_ID_BIT_MASK = "00002a42-0000-1000-8000-00805f9b34fb";
    public static final String ALERT_CATEGORY_ID = "00002a43-0000-1000-8000-00805f9b34fb";
    public static final String ALERT_NOTIFICATION_CONTROL_POINT = "00002a44-0000-1000-8000-00805f9b34fb";
    public static final String UNREAD_ALERT_STATUS = "00002a45-0000-1000-8000-00805f9b34fb";
    public static final String NEW_ALERT = "00002a46-0000-1000-8000-00805f9b34fb";
    public static final String SUPPORTED_NEW_ALERT_CATEGORY = "00002a47-0000-1000-8000-00805f9b34fb";
    public static final String SUPPORTED_UNREAD_ALERT_CATEGORY = "00002a48-0000-1000-8000-00805f9b34fb";
    public static final String BLOOD_PRESSURE_FEATURE = "00002a49-0000-1000-8000-00805f9b34fb";
    public static final String PNPID = "00002a50-0000-1000-8000-00805f9b34fb";
    public static final String SC_CONTROL_POINT = "00002a55-0000-1000-8000-00805f9b34fb";
    public static final String CSC_MEASUREMENT = "00002a5b-0000-1000-8000-00805f9b34fb";
    public static final String CSC_FEATURE = "00002a5c-0000-1000-8000-00805f9b34fb";
    public static final String SENSOR_LOCATION = "00002a5d-0000-1000-8000-00805f9b34fb";
    public static final String ACTIVESYNC = "831c4071-7bc8-4a9c-a01c-15df25a4adbc";
    public static final String ESTIMOTE_SERVICE = "b9403000-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_UUID = "b9403003-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_MAJOR = "b9403001-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_MINOR = "b9403002-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_BATTERY = "b9403041-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_TEMPERATURE = "b9403021-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_POWER = "b9403011-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_ADVERTISING_INTERVAL = "b9403012-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_VERSION_SERVICE = "b9404000-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_SOFTWARE_VERSION = "b9404001-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_HARDWARE_VERSION = "b9404002-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_AUTHENTICATION_SERVICE = "b9402000-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_ADVERTISING_SEED = "b9402001-f5f8-466e-aff9-25556b57fe6d";
    public static final String ESTIMOTE_ADVERTISING_VECTOR = "b9402002-f5f8-466e-aff9-25556b57fe6d";
    private final static Map<String, String> sGattAttributesMap = populateGattAttributesMap();
    public static String getAttributeName(final String uuid, final String fallback) {
        final String name = sGattAttributesMap.get(uuid.toLowerCase(Locale.US));
        return name == null ? fallback : name;
    }
    private static Map<String, String> populateGattAttributesMap() {
        final Map<String, String> map = new HashMap<>();
        map.put(BASE_GUID , "Base GUID");
        map.put(SERVICE_DISCOVERY_PROTOCOL_SDP , "Service Discovery Protocol (SDP)");
        map.put(USER_DATAGRAM_PROTOCOL_UDP , "User Datagram Protocol (UDP)");
        map.put(RADIO_FREQUENCY_COMMUNICATION_PROTOCOL_RFCOMM , "Radio Frequency Communication Protocol (RFCOMM)");
        map.put(TCP , "TCP");
        map.put(TCSBIN , "TCSBIN");
        map.put(TCSAT , "TCSAT");
        map.put(OBJECT_EXCHANGE_PROTOCOL_OBEX , "Object Exchange Protocol (OBEX)");
        map.put(IP , "IP");
        map.put(FTP , "FTP");
        map.put(HTTP , "HTTP");
        map.put(WSP , "WSP");
        map.put(BNEP_SVC , "BNEP_SVC");
        map.put(UPNP_PROTOCOL , "UPNP Protocol");
        map.put(HIDP , "HIDP");
        map.put(HARDCOPY_CONTROL_CHANNEL_PROTOCOL , "Hardcopy Control Channel Protocol");
        map.put(HARDCOPY_DATA_CHANNEL_PROTOCOL , "Hardcopy Data Channel Protocol");
        map.put(HARDCOPY_NOTIFICATION_PROTOCOL , "Hardcopy Notification Protocol");
        map.put(VCTP_PROTOCOL , "VCTP Protocol");
        map.put(VDTP_PROTOCOL , "VDTP Protocol");
        map.put(CMPT_PROTOCOL , "CMPT Protocol");
        map.put(UDI_C_PLANE_PROTOCOL , "UDI C Plane Protocol");
        map.put(MCAP_CONTROL_CHANNEL , "MCAP Control Channel");
        map.put(MCAP_DATA_CHANNEL , "MCAP Data Channel");
        map.put(L2CAP , "L2CAP");
        map.put(SERVICE_DISCOVERY_SERVER , "Service Discovery Server");
        map.put(BROWSE_GROUP_DESCRIPTOR , "Browse Group Descriptor");
        map.put(PUBLIC_BROWSE_GROUP , "Public Browse Group");
        map.put(SPP , "SPP");
        map.put(LAN_ACCESS_USING_PPP , "LAN Access Using PPP");
        map.put(DUN_GW , "DUN_GW");
        map.put(OBEX_SYNC , "OBEX_SYNC");
        map.put(OBEX_OBJECT_PUSH , "OBEX Object Push");
        map.put(OBEX_FILE_TRANSFER , "OBEX File Transfer");
        map.put(IRMC_SYNC_COMMAND , "IrMC Sync Command");
        map.put(HSP_HS , "HSP_HS");
        map.put(CORDLESS_TELEPHONY , "Cordless Telephony");
        map.put(AUDIO_SOURCE , "Audio Source");
        map.put(AUDIO_SINK , "Audio Sink");
        map.put(AV_REMOTE_CONTROL_TARGET , "AV Remote Control Target");
        map.put(ADVANCED_AUDIO , "ADVANCED_AUDIO");
        map.put(AVRCP_REMOTE , "AVRCP_REMOTE");
        map.put(VIDEO_CONFERENCING , "Video Conferencing");
        map.put(INTERCOM , "Intercom");
        map.put(FAX , "FAX");
        map.put(HEADSET_PROFILE_HSP_AUDIO_GATEWAY , "Headset Profile (HSP) - Audio Gateway");
        map.put(WAP , "WAP");
        map.put(WAP_CLIENT , "WAP Client");
        map.put(PANU , "PANU");
        map.put(NAP , "NAP");
        map.put(GN , "GN");
        map.put(DIRECT_PRINTING , "Direct Printing");
        map.put(REFERENCE_PRINTING , "Reference Printing");
        map.put(IMAGING , "Imaging");
        map.put(IMAGING_RESPONDER , "Imaging Responder");
        map.put(IMAGING_AUTOMATIC_ARCHIVE , "Imaging Automatic Archive");
        map.put(IMAGING_REFERENCE_OBJECTS , "Imaging Reference Objects");
        map.put(HANDS_FREE_PROFILE_HFP , "Hands Free Profile (HFP)");
        map.put(HANDS_FREE_PROFILE_HFP_AUDIO_GATEWAY , "Hands Free Profile (HFP) â€“ Audio Gateway");
        map.put(DIRECT_PRINTING_REFERENCE_OBJECTS , "Direct Printing Reference Objects");
        map.put(REFLECTED_UI , "Reflected UI");
        map.put(BASIC_PRINTING , "Basic Printing");
        map.put(PRINTING_STATUS , "Printing Status");
        map.put(HID , "HID");
        map.put(HARDCOPY_CABLE_REPLACEMENT , "Hardcopy Cable Replacement");
        map.put(HCR_PRINT , "HCR Print");
        map.put(HCR_SCAN , "HCR Scan");
        map.put(COMMON_ISDN_ACCESS , "Common ISDN Access");
        map.put(VIDEO_CONFERENCING_GATEWAY , "Video Conferencing Gateway");
        map.put(UDIMT , "UDIMT");
        map.put(UDITA , "UDITA");
        map.put(AUDIO_VIDEO , "Audio Video");
        map.put(SIM_ACCESS , "SIM Access");
        map.put(OBEX_PCE , "OBEX PCE");
        map.put(OBEX_PSE , "OBEX PSE");
        map.put(OBEX_PBAP , "OBEX PBAP");
        map.put(OBEX_MAS , "OBEX MAS");
        map.put(OBEX_MNS , "OBEX MNS");
        map.put(OBEX_MAP , "OBEX MAP");
        map.put(PNP , "PNP");
        map.put(GENERIC_NETWORKING , "Generic Networking");
        map.put(GENERIC_FILE_TRANSFER , "Generic File Transfer");
        map.put(GENERIC_AUDIO , "Generic Audio");
        map.put(GENERIC_TELEPHONY , "Generic Telephony");
        map.put(UPNP , "UPNP");
        map.put(UPNP_IP , "UPNP IP");
        map.put(ESDP_UPNP_IP_PAN , "ESDP UPnP IP PAN");
        map.put(ESDP_UPNP_IP_LAP , "ESDP UPnP IP LAP");
        map.put(ESDP_UPNP_L2CAP , "ESDP Upnp L2CAP");
        map.put(VIDEO_DISTRIBUTION_PROFILE_VDP_SOURCE , "Video Distribution Profile (VDP) - Source");
        map.put(VIDEO_DISTRIBUTION_PROFILE_VDP_SINK , "Video Distribution Profile (VDP) - Sink");
        map.put(VIDEO_DISTRIBUTION_PROFILE_VDP , "Video Distribution Profile (VDP)");
        map.put(HEALTH_DEVICE_PROFILE_HDP , "Health Device Profile (HDP)");
        map.put(HEALTH_DEVICE_PROFILE_HDP_SOURCE , "Health Device Profile (HDP) - Source");
        map.put(HEALTH_DEVICE_PROFILE_HDP_SINK , "Health Device Profile (HDP) - Sink");
        map.put(GAP , "GAP");
        map.put(GATT , "GATT");
        map.put(IMMEDIATE_ALERT , "IMMEDIATE_ALERT");
        map.put(LINK_LOSS , "LINK_LOSS");
        map.put(TX_POWER , "TX_POWER");
        map.put(HEALTH_THERMOMETER , "Health Thermometer");
        map.put(DEVICE_INFORMATION , "Device Information");
        map.put(HEART_RATE , "HEART_RATE");
        map.put(CYCLING_SC , "CYCLING_SC");
        map.put(CLIENT_CHARACTERISTIC_CONFIG , "CLIENT_CHARACTERISTIC_CONFIG");
        map.put(DEVICE_NAME , "Device Name");
        map.put(APPEARANCE , "Appearance");
        map.put(PERIPHERAL_PRIVACY_FLAG , "Peripheral Privacy Flag");
        map.put(RECONNECTION_ADDRESS , "Reconnection Address");
        map.put(PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS , "Peripheral Preferred Connection Parameters");
        map.put(SERVICE_CHANGED , "Service Changed");
        map.put(ALERT_LEVEL , "Alert Level");
        map.put(TX_POWER_LEVEL , "Tx Power Level");
        map.put(DATE_TIME , "Date Time");
        map.put(DAY_OF_WEEK , "Day of Week");
        map.put(DAY_DATE_TIME , "Day Date Time");
        map.put(EXACT_TIME_256 , "Exact Time 256");
        map.put(DST_OFFSET , "DST Offset");
        map.put(TIME_ZONE , "Time Zone");
        map.put(LOCAL_TIME_INFORMATION , "Local Time Information");
        map.put(TIME_WITH_DST , "Time with DST");
        map.put(TIME_ACCURACY , "Time Accuracy");
        map.put(TIME_SOURCE , "Time Source");
        map.put(REFERENCE_TIME_INFORMATION , "Reference Time Information");
        map.put(TIME_UPDATE_CONTROL_POINT , "Time Update Control Point");
        map.put(TIME_UPDATE_STATE , "Time Update State");
        map.put(TEMPERATURE_MEASUREMENT , "Temperature Measurement");
        map.put(TEMPERATURE_TYPE , "Temperature Type");
        map.put(INTERMEDIATE_TEMPERATURE , "Intermediate Temperature");
        map.put(MEASUREMENT_INTERVAL , "Measurement Interval");
        map.put(SYSTEM_ID , "System ID");
        map.put(MODEL_NUMBER_STRING , "Model Number String");
        map.put(SERIAL_NUMBER_STRING , "Serial Number String");
        map.put(FIRMWARE_REVISION_STRING , "Firmware Revision String");
        map.put(HARDWARE_REVISION_STRING , "Hardware Revision String");
        map.put(SOFTWARE_REVISION_STRING , "Software Revision String");
        map.put(MANUFACTURER_NAME_STRING , "Manufacturer Name String");
        map.put(IEEE_1107320601_REGULATORY , "IEEE 11073-20601 Regulatory");
        map.put(CURRENT_TIME , "Current Time");
        map.put(BLOOD_PRESSURE_MEASUREMENT , "Blood Pressure Measurement");
        map.put(INTERMEDIATE_CUFF_PRESSURE , "Intermediate Cuff Pressure");
        map.put(HEART_RATE_MEASUREMENT , "Heart Rate Measurement");
        map.put(BODY_SENSOR_LOCATION , "Body Sensor Location");
        map.put(HEART_RATE_CONTROL_POINT , "Heart Rate Control Point");
        map.put(ALERT_STATUS , "Alert Status");
        map.put(RINGER_CONTROL_POINT , "Ringer Control Point");
        map.put(RINGER_SETTING , "Ringer Setting");
        map.put(ALERT_CATEGORY_ID_BIT_MASK , "Alert Category ID Bit Mask");
        map.put(ALERT_CATEGORY_ID , "Alert Category ID");
        map.put(ALERT_NOTIFICATION_CONTROL_POINT , "Alert Notification Control Point");
        map.put(UNREAD_ALERT_STATUS , "Unread Alert Status");
        map.put(NEW_ALERT , "New Alert");
        map.put(SUPPORTED_NEW_ALERT_CATEGORY , "Supported New Alert Category");
        map.put(SUPPORTED_UNREAD_ALERT_CATEGORY , "Supported Unread Alert Category");
        map.put(BLOOD_PRESSURE_FEATURE , "Blood Pressure Feature");
        map.put(PNPID , "PNPID");
        map.put(SC_CONTROL_POINT , "SC_CONTROL_POINT");
        map.put(CSC_MEASUREMENT , "CSC_MEASUREMENT");
        map.put(CSC_FEATURE , "CSC_FEATURE");
        map.put(SENSOR_LOCATION , "SENSOR_LOCATION");
        map.put(ACTIVESYNC , "ActiveSync");
        map.put(ESTIMOTE_SERVICE , "Estimote Service");
        map.put(ESTIMOTE_UUID , "Estimote UUID");
        map.put(ESTIMOTE_MAJOR , "Estimote Major");
        map.put(ESTIMOTE_MINOR , "Estimote Minor");
        map.put(ESTIMOTE_BATTERY , "Estimote Battery");
        map.put(ESTIMOTE_TEMPERATURE , "Estimote Temperature");
        map.put(ESTIMOTE_POWER , "Estimote Power");
        map.put(ESTIMOTE_ADVERTISING_INTERVAL , "Estimote Advertising Interval");
        map.put(ESTIMOTE_VERSION_SERVICE , "Estimote Version Service");
        map.put(ESTIMOTE_SOFTWARE_VERSION , "Estimote Software Version");
        map.put(ESTIMOTE_HARDWARE_VERSION , "Estimote Hardware Version");
        map.put(ESTIMOTE_AUTHENTICATION_SERVICE , "Estimote Authentication Service");
        map.put(ESTIMOTE_ADVERTISING_SEED , "Estimote Advertising Seed");
        map.put(ESTIMOTE_ADVERTISING_VECTOR , "Estimote Advertising Vector");
        return map;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/BackgroundPowerSaver.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,197 @@
package com.blakequ.bluetooth_manager_lib.scan;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import com.blakequ.bluetooth_manager_lib.BleManager;
import com.blakequ.bluetooth_manager_lib.BleParamsOptions;
import com.blankj.utilcode.util.LogUtils;
/**
 *
 * Simply creating an instance of this class and holding a reference to it in your Application can
 * improve battery life by 60% by slowing down scans when your app is in the background.
 *
 */
@TargetApi(18)
public class BackgroundPowerSaver implements Application.ActivityLifecycleCallbacks {
    private int activeActivityCount = 0;
    private Context mContext;
    /**
     * The default duration in milliseconds of the Bluetooth scan cycle
     */
    public static final long DEFAULT_FOREGROUND_SCAN_PERIOD = 10000;
    /**
     * The default duration in milliseconds spent not scanning between each Bluetooth scan cycle
     */
    public static final long DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD = 5*1000;
    /**
     * The default duration in milliseconds of the Bluetooth scan cycle when no ranging/monitoring clients are in the foreground
     */
    public static final long DEFAULT_BACKGROUND_SCAN_PERIOD = 10000;
    /**
     * The default duration in milliseconds spent not scanning between each Bluetooth scan cycle when no ranging/monitoring clients are in the foreground
     */
    public static final long DEFAULT_BACKGROUND_BETWEEN_SCAN_PERIOD = 5 * 60 * 1000;
    private long foregroundScanPeriod = DEFAULT_FOREGROUND_SCAN_PERIOD;
    private long foregroundBetweenScanPeriod = DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD;
    private long backgroundScanPeriod = DEFAULT_BACKGROUND_SCAN_PERIOD;
    private long backgroundBetweenScanPeriod = DEFAULT_BACKGROUND_BETWEEN_SCAN_PERIOD;
    /**
     *
     * Constructs a new BackgroundPowerSaver
     *
     * @param context
     * @deprecated the countActiveActivityStrategy flag is no longer used.
     *
     */
    public BackgroundPowerSaver(Context context, boolean countActiveActivityStrategy) {
        this(context);
    }
    /**
     *
     * Constructs a new BackgroundPowerSaver using the default background determination strategy
     *
     * @param context
     */
    public BackgroundPowerSaver(Context context) {
        if (android.os.Build.VERSION.SDK_INT < 18) {
            LogUtils.w("BackgroundPowerSaver requires API 18 or higher.");
            return;
        }
        ((Application)context.getApplicationContext()).registerActivityLifecycleCallbacks(this);
        this.mContext = context;
    }
    /**
     * Sets the duration in milliseconds of each Bluetooth LE scan cycle to look for beacons.
     * This function is used to setup the period when switching
     * between background/foreground. To have it effect on an already running scan (when the next
     * cycle starts), call {@link BluetoothScanManager#setBackgroundMode}
     *
     * @param p
     */
    @Deprecated
    public void setForegroundScanPeriod(long p) {
        foregroundScanPeriod = p;
    }
    /**
     * Sets the duration in milliseconds between each Bluetooth LE scan cycle to look for beacons.
     * This function is used to setup the period when switching
     * between background/foreground. To have it effect on an already running scan (when the next
     * cycle starts), call {@link BluetoothScanManager#setBackgroundMode}
     *
     * @param p
     */
    @Deprecated
    public void setForegroundBetweenScanPeriod(long p) {
        foregroundBetweenScanPeriod = p;
    }
    /**
     * Sets the duration in milliseconds of each Bluetooth LE scan cycle to look for beacons.
     * This function is used to setup the period when switching
     * between background/foreground. To have it effect on an already running scan (when the next
     * cycle starts), call {@link BluetoothScanManager#setBackgroundMode}
     *
     * @param p
     */
    @Deprecated
    public void setBackgroundScanPeriod(long p) {
        backgroundScanPeriod = p;
    }
    /**
     * Sets the duration in milliseconds spent not scanning between each Bluetooth LE scan cycle when no ranging/monitoring clients are in the foreground
     *
     * @param p
     */
    @Deprecated
    public void setBackgroundBetweenScanPeriod(long p) {
        backgroundBetweenScanPeriod = p;
    }
    public long getScanPeriod() {
        BleParamsOptions options = BleManager.getBleParamsOptions();
        if (BluetoothScanManager.getInstance(mContext).isBackgroundMode()) {
            if (options != null){
                return options.getBackgroundScanPeriod();
            }else {
                return backgroundScanPeriod;
            }
        } else {
            if (options != null){
                return options.getForegroundScanPeriod();
            }else {
                return foregroundScanPeriod;
            }
        }
    }
    public long getBetweenScanPeriod() {
        BleParamsOptions options = BleManager.getBleParamsOptions();
        if (BluetoothScanManager.getInstance(mContext).isBackgroundMode()) {
            if (options != null){
                return options.getBackgroundBetweenScanPeriod();
            }else {
                return backgroundBetweenScanPeriod;
            }
        } else {
            if (options != null){
                return options.getForegroundBetweenScanPeriod();
            }else {
                return foregroundBetweenScanPeriod;
            }
        }
    }
    @Override
    public void onActivityCreated(Activity activity, Bundle bundle) {
    }
    @Override
    public void onActivityStarted(Activity activity) {
    }
    @Override
    public void onActivityResumed(Activity activity) {
        activeActivityCount++;
        if (activeActivityCount < 1) {
            LogUtils.d("reset active activity count on resume.  It was " + activeActivityCount);
            activeActivityCount = 1;
        }
        BluetoothScanManager.getInstance(mContext).setBackgroundMode(false);
        LogUtils.d("activity resumed: "+activity+" active activities: "+activeActivityCount);
    }
    @Override
    public void onActivityPaused(Activity activity) {
        activeActivityCount--;
        LogUtils.d("activity paused: "+activity+" active activities: "+activeActivityCount);
        if (activeActivityCount < 1) {
            LogUtils.d("setting background mode");
            BluetoothScanManager.getInstance(mContext).setBackgroundMode(true);
        }
    }
    @Override
    public void onActivityStopped(Activity activity) {
    }
    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
    }
    @Override
    public void onActivityDestroyed(Activity activity) {
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/BluetoothScanManager.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,243 @@
package com.blakequ.bluetooth_manager_lib.scan;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import com.blakequ.bluetooth_manager_lib.BleManager;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanCallbackCompat;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanFilterCompat;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanResultCompat;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanSettingsCompat;
import com.blankj.utilcode.util.LogUtils;
import java.util.List;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/17 11:30 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description: æ‰«æç®¡ç†å™¨ï¼Œè¦å®žçŽ°çš„åŠŸèƒ½
 * <ol>
 * <li>扫描封装</li>
 * <li>扫描管理</li>
 * <li>支持ibeacon扫描解析</li>
 * <li>持续扫描省电管理(BackgroundPowerSaver)</li>
 * <li>循环扫描暂停与开始(当连接时可以让扫描暂停,一旦断开就重启扫描)</li>
 * <li>当前扫描状态</li>
 * </ol>
 * æ³¨æ„ï¼šå›žè°ƒä¸åœ¨ä¸»çº¿ç¨‹ä¸­æ‰§è¡Œï¼Œéœ€è¦è‡ªå·±åœ¨ä¸»çº¿ç¨‹ä¸­å¤„理回调({@link com.blakequ.bluetooth_manager_lib.scan.ScanOverListener} and {@link com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanCallbackCompat}),
 * å°¤å…¶æ˜¯æƒ³åœ¨æ‰«æç»“束之后直接执行连接蓝牙或断开蓝牙设备,都需要在主线程执行,否则在某些机型如三星会出现异常。
 */
@TargetApi(18)
public final class BluetoothScanManager {
    private static BluetoothScanManager INSTANCE = null;
    //is background mode or not
    private boolean backgroundMode = false;
    private BackgroundPowerSaver mPowerSaver;
    private CycledLeScanner cycledLeScanner;
    private ScanCallbackCompat scanCallbackCompat;
    private final Handler mHandler;
    private static Object obj = new Object();
    private BluetoothScanManager(Context context){
        mPowerSaver = new BackgroundPowerSaver(context);
        mHandler = new Handler(Looper.getMainLooper());
        cycledLeScanner = new CycledLeScanner(context,
                BackgroundPowerSaver.DEFAULT_FOREGROUND_SCAN_PERIOD,
                BackgroundPowerSaver.DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD,
                backgroundMode,
                getScanCallback());
        BleManager.getBleParamsOptions();
    }
    public static BluetoothScanManager getInstance(Context context){
        if (INSTANCE == null){
            synchronized (obj){
                if (INSTANCE == null){
                    LogUtils.d("BluetoothScanManager instance creation");
                    INSTANCE = new BluetoothScanManager(context);
                }
            }
        }
        return INSTANCE;
    }
    /**
     * Runs the specified action on the UI thread. If the current thread is the UI
     * thread, then the action is executed immediately. If the current thread is
     * not the UI thread, the action is posted to the event queue of the UI thread.
     *
     * @param action the action to run on the UI thread
     */
    public final void runOnUiThread(Runnable action) {
        if (Thread.currentThread() != Looper.getMainLooper().getThread()) {
            mHandler.post(action);
        } else {
            action.run();
        }
    }
    /**
     * @return
     */
    public BackgroundPowerSaver getPowerSaver(){
        return mPowerSaver;
    }
    /**
     * set scan device invoke
     * @param scanCallbackCompat
     */
    public void setScanCallbackCompat(ScanCallbackCompat scanCallbackCompat) {
        this.scanCallbackCompat = scanCallbackCompat;
    }
    public void setScanOverListener(ScanOverListener scanOverListener) {
        cycledLeScanner.setScanOverListener(scanOverListener);
    }
    /**
     * is scanning
     * @return
     */
    public boolean isScanning(){
      return cycledLeScanner.isScanning();
    }
    public boolean isPauseScanning(){
        return cycledLeScanner.isPauseScan();
    }
    /**
     * stop cycle scan and will restart when invoke {@link #startCycleScan()}
     */
    public void stopCycleScan(){
        cycledLeScanner.setPauseScan(true);
    }
    /**
     * start scan device and will stop until invoke {@link #stopCycleScan()}
     * notice: maybe is not scan right now
     * @see #startScanNow()
     * @see #stopCycleScan()
     */
    public void startCycleScan(){
        cycledLeScanner.startScan();
    }
    /**
     * Immediately start a scan(only one times)
     */
    public void startScanOnce(){
        cycledLeScanner.startOnceScan();
    }
    /**
     * start scan right now, is different {@link #startCycleScan()}
     * @see #startCycleScan()
     */
    public void startScanNow(){
        // FIXME: 2017/6/22 modify config not enable first time
        cycledLeScanner.setBackgroundMode(mPowerSaver.getScanPeriod(), mPowerSaver.getBetweenScanPeriod(), backgroundMode);
        cycledLeScanner.startScanNow();
    }
    /**
     * add scan filter
     * @param scanFilter
     */
    public void addScanFilterCompats(ScanFilterCompat scanFilter){
        cycledLeScanner.addScanFilterCompats(scanFilter);
    }
    public void setScanSettings(ScanSettingsCompat scanSettings) {
        cycledLeScanner.setScanSettings(scanSettings);
    }
    /**
     * default using new scan method if API >= 21
     * @return
     */
    @Deprecated
    public static boolean isAPI21ScanningDisabled(){
        return false;
    }
    /**
     * This method notifies the beacon service that the application is either moving to background
     * mode or foreground mode.  When in background mode, BluetoothLE scans to look for beacons are
     * executed less frequently in order to save battery life. The specific scan rates for
     * background and foreground operation are set by the defaults below, but may be customized.
     * When ranging in the background, the time between updates will be much less frequent than in
     * the foreground.  Updates will come every time interval equal to the sum total of the
     * BackgroundScanPeriod and the BackgroundBetweenScanPeriod.
     *
     * @param backgroundMode true indicates the app is in the background
     */
    public void setBackgroundMode(boolean backgroundMode) {
        if (android.os.Build.VERSION.SDK_INT < 18) {
            LogUtils.w("Not supported prior to API 18.  Method invocation will be ignored");
        }
        if (backgroundMode != this.backgroundMode) {
            this.backgroundMode = backgroundMode;
            cycledLeScanner.setBackgroundMode(mPowerSaver.getScanPeriod(), mPowerSaver.getBetweenScanPeriod(), backgroundMode);
        }
    }
    public boolean isBackgroundMode() {
        return backgroundMode;
    }
    private ScanCallbackCompat getScanCallback(){
        return new ScanCallbackCompat() {
            @Override
            public void onBatchScanResults(final List<ScanResultCompat> results) {
                if (scanCallbackCompat != null){
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            scanCallbackCompat.onBatchScanResults(results);
                        }
                    });
                }
            }
            @Override
            public void onScanFailed(final int errorCode) {
                if (scanCallbackCompat != null){
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            scanCallbackCompat.onScanFailed(errorCode);
                        }
                    });
                }
            }
            @Override
            public void onScanResult(final int callbackType, final ScanResultCompat result) {
                if (scanCallbackCompat != null){
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            scanCallbackCompat.onScanResult(callbackType, result);
                        }
                    });
                }
            }
        };
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/CycledLeScanner.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,363 @@
package com.blakequ.bluetooth_manager_lib.scan;
import android.Manifest;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.os.Handler;
import android.os.SystemClock;
import androidx.core.content.ContextCompat;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.BluetoothLeScannerCompat;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanCallbackCompat;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanFilterCompat;
import com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat.ScanSettingsCompat;
import com.blakequ.bluetooth_manager_lib.util.BluetoothUtils;
import com.blankj.utilcode.util.LogUtils;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/18 11:54 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description:
 */
@TargetApi(18)
public class CycledLeScanner {
    private boolean isPrintCycleTime = true;
    private final Context mContext;
    private long scanPeriod;
    private long betweenScanPeriod;
    private boolean mBackgroundFlag = false;
    private ScanCallbackCompat scanCallbackCompat;
    private final BluetoothUtils mBluetoothUtils;
    private ScanOverListener scanOverListener;
    private long nextScanStartTime = 0;
    private long scanStopTime = 0;
    private long lastScanEndTime = 0;
    private boolean mScanning = false;
    private boolean isPauseScan = false; //pause scan or restart
    private boolean isOnceScan = false; //scan only once
    private boolean isStartNow = false;
    private final Handler mHandler = new Handler();
    private boolean isSetScanSetting = false;
    private ScanSettingsCompat scanSettings;
    private final List<ScanFilterCompat> scanFilterCompats = new CopyOnWriteArrayList<>();
    public CycledLeScanner(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, ScanCallbackCompat callbackCompat){
        this.mContext = context;
        this.scanPeriod = scanPeriod;
        this.betweenScanPeriod = betweenScanPeriod;
        this.scanCallbackCompat = callbackCompat;
        this.mBackgroundFlag = backgroundFlag;
        this.mBluetoothUtils = BluetoothUtils.getInstance(context);
    }
    /**
     * invoke at the end of scan(every scan cycle over)
     * @param scanOverListener
     */
    public void setScanOverListener(ScanOverListener scanOverListener) {
        this.scanOverListener = scanOverListener;
    }
    /**
     * add scan filter
     * @param scanFilter
     */
    public void addScanFilterCompats(ScanFilterCompat scanFilter){
        scanFilterCompats.add(scanFilter);
    }
    public void setScanSettings(ScanSettingsCompat scanSettings) {
        isSetScanSetting = true;
        this.scanSettings = scanSettings;
    }
    public void startOnceScan() {
        isOnceScan = true;
        scanLeDevice(true);
    }
    /**
     * pause or restart scan device cycle
     * @param isPauseScan
     */
    public void setPauseScan(boolean isPauseScan) {
        this.isPauseScan = isPauseScan;
        if (!isPauseScan){
            scanLeDevice(true);
        }else {
            scanLeDevice(false);
        }
    }
    /**
     * start scan device
     */
    public void startScan(){
        isPauseScan = false;
        scanLeDevice(true);
    }
    /**
     * start scan device right now
     */
    public void startScanNow(){
        isStartNow = true;
        isPauseScan = false;
        scanLeDevice(true);
    }
    /**
     * Tells the cycler the scan rate and whether it is in operating in background mode.
     * Background mode flag  is used only with the Android 5.0 scanning implementations to switch
     * between LOW_POWER_MODE vs. LOW_LATENCY_MODE
     * @param backgroundFlag is running background
     */
    public void setBackgroundMode(long scanPeriod, long betweenScanPeriod, boolean backgroundFlag) {
        if (android.os.Build.VERSION.SDK_INT < 18) {
            LogUtils.w("Not supported prior to API 18.  Method invocation will be ignored");
            return;
        }
        this.scanPeriod = scanPeriod;
        this.betweenScanPeriod = betweenScanPeriod;
        if (backgroundFlag != mBackgroundFlag) {
            LogUtils.d("restart polling task scanPeriod:" + scanPeriod + " betweenScanPeriod:" + betweenScanPeriod + " backgroundFlag:" + backgroundFlag + " mode:" + mBackgroundFlag);
            mBackgroundFlag = backgroundFlag;
            long now = SystemClock.elapsedRealtime();
            //update next scan start time(在等待开始扫描时修正下一次开始时间,提前开始)
            if (nextScanStartTime > now){
                long proposedNextScanStartTime = lastScanEndTime + betweenScanPeriod;
                if (proposedNextScanStartTime < nextScanStartTime){
                    LogUtils.d("Waiting...Adjusted nextScanStartTime to be" + (proposedNextScanStartTime - now) + " old:" + (nextScanStartTime - now));
                    nextScanStartTime = proposedNextScanStartTime;
                }
            }
            //update current scan stop time(如果在扫描中则修正本次的结束时间,提前结束)
            if (scanStopTime > now){
                long proposedStopTime = nextScanStartTime + scanPeriod;
                if (proposedStopTime < scanStopTime){
                    LogUtils.d("Scanning...Adjusted scanStopTime to be " + (proposedStopTime - now) + " old:" + (scanStopTime - now));
                    scanStopTime = proposedStopTime;
                }
            }
            //set scan setting params
            if (!isSetScanSetting || scanSettings == null){
                if (mBackgroundFlag) {
                    LogUtils.d("starting filtered scan in SCAN_MODE_LOW_POWER");
                    scanSettings = (new ScanSettingsCompat.Builder().setScanMode(ScanSettingsCompat.SCAN_MODE_LOW_POWER)).build();
                } else {
                    LogUtils.d("starting non-filtered scan in SCAN_MODE_LOW_LATENCY");
                    scanSettings = (new ScanSettingsCompat.Builder().setScanMode(ScanSettingsCompat.SCAN_MODE_LOW_LATENCY)).build();
                }
            }
        }
    }
    public boolean isScanning() {
        return mScanning;
    }
    public boolean isPauseScan() {
        return isPauseScan;
    }
    /**
     * start or stop scan
     * @param enable true-start scan right now,false-stop scan
     */
    private void scanLeDevice(boolean enable) {
        BluetoothAdapter mAdapter = mBluetoothUtils.getBluetoothAdapter();
        if (mBluetoothUtils == null || !mBluetoothUtils.isBluetoothIsEnable()){
            LogUtils.e("ScanDevice: Scanning fail! BluetoothAdapter is null");
            return;
        }
        if (enable) {
            //is delay scan
            if (deferScanIfNeeded()){
                if (!isStartNow){
                    return;
                }else{
                    LogUtils.i("ScanDevice: Scan right now!");
                    isStartNow = false;
                }
            }
            if (mScanning) {
                LogUtils.d("ScanDevice: Scanning is running now !");
                return;
            }
            LogUtils.d("ScanDevice: Starting Scanning scanPeriod:"+scanPeriod+", between:"+betweenScanPeriod);
            mScanning = true;
            if (!isPauseScan || isOnceScan){
                try {
                    if (android.os.Build.VERSION.SDK_INT < 23 || checkLocationPermission()) {
                        if (android.os.Build.VERSION.SDK_INT >= 23 && !isGpsProviderEnabled(mContext)){
                            LogUtils.e("If SDK>=23, current SDK=" + android.os.Build.VERSION.SDK_INT+", Location info not open and can not scan any device!");
                            scanCallbackCompat.onScanFailed(ScanCallbackCompat.SCAN_FAILED_LOCATION_CLOSE);
                        }else {
                            LogUtils.i("ScanDevice: Start scan...");
                            BluetoothLeScannerCompat.startScan(mAdapter, scanFilterCompats, getScanSettings(), scanCallbackCompat);
                        }
                    }else{
                        scanCallbackCompat.onScanFailed(ScanCallbackCompat.SCAN_FAILED_LOCATION_PERMISSION_FORBID);
                        LogUtils.e("If SDK>=23, current SDK="+android.os.Build.VERSION.SDK_INT+", Please check the location permission is enabled(ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION)");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    LogUtils.e("Internal Android exception scanning for beacons "+e.toString());
                }
                if (isOnceScan){
                    LogUtils.d("ScanDevice: Scanning once");
                    isOnceScan = false;
                }
            }else{
                LogUtils.d("ScanDevice: Pause Scanning");
            }
            scanStopTime = SystemClock.elapsedRealtime() + scanPeriod;
            nextScanStartTime = scanStopTime + betweenScanPeriod;
            scheduleScanStop();
        } else {
            LogUtils.d("ScanDevice: Stopping Scan");
            stopScan();
        }
    }
    private void scheduleScanStop(){
        // Stops scanning after a pre-defined scan period.
        long millisecondsUntilStop = scanStopTime - SystemClock.elapsedRealtime();
        if (millisecondsUntilStop > 0) {
            if (isPrintCycleTime){
                LogUtils.d("Waiting to stop scan cycle for another " + millisecondsUntilStop + " milliseconds");
            }
            if (!isPauseScan) {
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        scheduleScanStop();
                    }
                }, millisecondsUntilStop > 1000 ? 1000 : millisecondsUntilStop);
            }
        } else {
            LogUtils.d("Stop cycle scan");
            stopScan();
        }
    }
    private void stopScan(){
        if (mScanning) {
            BluetoothAdapter mAdapter = mBluetoothUtils.getBluetoothAdapter();
            if (mAdapter != null && mBluetoothUtils.isBluetoothIsEnable()) {
                try {
                    BluetoothLeScannerCompat.stopScan(mAdapter, scanCallbackCompat);
                    lastScanEndTime = SystemClock.elapsedRealtime();
                    LogUtils.d("stopping bluetooth le scan "+lastScanEndTime);
                } catch (Exception e) {
                    LogUtils.w("Internal Android exception scanning for beacons "+e.toString());
                }
            } else {
                LogUtils.d("Bluetooth is disabled.  Cannot scan for beacons.");
            }
            nextScanStartTime = SystemClock.elapsedRealtime() + betweenScanPeriod;
            //start next scan cycle
            if (!isPauseScan){
                scanLeDevice(true);
            }
        }
        mScanning = false;
        //// FIXME: 2017/6/22 å°†å…¶è°ƒæ•´åˆ°mScanning后面
        if (scanOverListener != null){
            scanOverListener.onScanOver();
        }
    }
    /**
     * check is defter scan
     * @return
     */
    private boolean deferScanIfNeeded(){
        long millisecondsUntilStart = nextScanStartTime - SystemClock.elapsedRealtime();
        if (millisecondsUntilStart > 0) {
            if (isPrintCycleTime){
                LogUtils.d("Waiting to start next Bluetooth scan for another "+millisecondsUntilStart+" milliseconds");
            }
            // Don't actually wait until the next scan time -- only wait up to 1 second.  This
            // allows us to start scanning sooner if a consumer enters the foreground and expects
            // results more quickly.
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (!isPauseScan) {
                        scanLeDevice(true);
                    }
                }
            }, millisecondsUntilStart > 1000 ? 1000 : millisecondsUntilStart);
            return true;
        }
        LogUtils.d("Start cycle scan");
        return false;
    }
    /**
     * get scan settings
     * @return
     */
    private ScanSettingsCompat getScanSettings() {
        if (scanSettings == null){
            if (mBackgroundFlag) {
                LogUtils.d("starting filtered scan in SCAN_MODE_LOW_POWER");
                scanSettings = (new ScanSettingsCompat.Builder().setScanMode(ScanSettingsCompat.SCAN_MODE_LOW_POWER)).build();
            } else {
                LogUtils.d("starting non-filtered scan in SCAN_MODE_LOW_LATENCY");
                scanSettings = (new ScanSettingsCompat.Builder().setScanMode(ScanSettingsCompat.SCAN_MODE_LOW_LATENCY)).build();
            }
        }
        return scanSettings;
    }
    /**
     * is open GPS
     * @param context
     * @return
     */
    public static boolean isGpsProviderEnabled(Context context){
        LocationManager service = (LocationManager) context.getSystemService(context.LOCATION_SERVICE);
        return service.isProviderEnabled(LocationManager.GPS_PROVIDER);
    }
    /**
     * when API>=23, if the location disabled, can not scan any devices
     * @return
     */
    private boolean checkLocationPermission() {
        return checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION) || checkPermission(Manifest.permission.ACCESS_FINE_LOCATION);
    }
    private boolean checkPermission(final String permission) {
        return ContextCompat.checkSelfPermission(mContext, permission) == PackageManager.PERMISSION_GRANTED;
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/ScanOverListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.blakequ.bluetooth_manager_lib.scan;
/**
 * Copyright (C) BlakeQu All Rights Reserved <blakequ@gmail.com>
 * <p/>
 * Licensed under the blakequ.com License, Version 1.0 (the "License");
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * author  : quhao <blakequ@gmail.com> <br>
 * date     : 2016/8/18 15:35 <br>
 * last modify author : <br>
 * version : 1.0 <br>
 * description:when once scan at the cycle end
 */
public interface ScanOverListener {
    void onScanOver();
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/BluetoothLeScannerCompat.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,261 @@
/*
 * Copyright (C) 2015 Joe Rogers
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
import androidx.collection.SimpleArrayMap;
import com.blakequ.bluetooth_manager_lib.scan.BluetoothScanManager;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class BluetoothLeScannerCompat {
    private static final String TAG = "BLeScannerCompat";
    @RequiresPermission("android.permission.BLUETOOTH_ADMIN")
    public static void flushPendingScanResults(@NonNull BluetoothAdapter adapter, @NonNull ScanCallbackCompat callbackCompat) {
        IMPL.flushPendingScanResults(adapter, callbackCompat);
    }
    @RequiresPermission("android.permission.BLUETOOTH_ADMIN")
    public static void startScan(@NonNull BluetoothAdapter adapter, @Nullable List<ScanFilterCompat> filters, @NonNull ScanSettingsCompat settings, @NonNull ScanCallbackCompat callbackCompat) {
        IMPL.startScan(adapter, filters, settings, callbackCompat);
    }
    @RequiresPermission("android.permission.BLUETOOTH_ADMIN")
    public static void startScan(@NonNull BluetoothAdapter adapter, @NonNull ScanCallbackCompat callbackCompat) {
        IMPL.startScan(adapter, callbackCompat);
    }
    @RequiresPermission("android.permission.BLUETOOTH_ADMIN")
    public static void stopScan(@NonNull BluetoothAdapter adapter, @NonNull ScanCallbackCompat callbackCompat) {
        IMPL.stopScan(adapter, callbackCompat);
    }
    static final BluetoothLeScannerCompatImpl IMPL;
    static {
        final int version = Build.VERSION.SDK_INT;
        if (BluetoothScanManager.isAPI21ScanningDisabled()){
            IMPL = new API18BluetoothLeScannerCompatImpl();
        }else{
            if (version >= Build.VERSION_CODES.LOLLIPOP) {
                IMPL = new API21BluetoothLeScannerCompatImpl();
            }else {
                IMPL = new API18BluetoothLeScannerCompatImpl();
            }
        }
    }
    interface BluetoothLeScannerCompatImpl {
        void flushPendingScanResults(BluetoothAdapter adapter, ScanCallbackCompat callbackCompat);
        void startScan(BluetoothAdapter adapter, List<ScanFilterCompat> filters, ScanSettingsCompat settings, ScanCallbackCompat callbackCompat);
        void startScan(BluetoothAdapter adapter, ScanCallbackCompat callbackCompat);
        void stopScan(BluetoothAdapter adapter, ScanCallbackCompat callbackCompat);
    }
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    static class API21BluetoothLeScannerCompatImpl implements BluetoothLeScannerCompatImpl {
        static final SimpleArrayMap<ScanCallbackCompat, API21ScanCallback> callbackMap = new SimpleArrayMap<>();
        @Override
        public void flushPendingScanResults(BluetoothAdapter adapter, ScanCallbackCompat callbackCompat) {
            API21ScanCallback result = callbackMap.get(callbackCompat);
            if (result == null) {
                return;
            }
            adapter.getBluetoothLeScanner().flushPendingScanResults(result);
        }
        @Override
        public void startScan(BluetoothAdapter adapter, List<ScanFilterCompat> filters, ScanSettingsCompat settings, ScanCallbackCompat callbackCompat) {
            List<ScanFilter> scanFilters = null;
            if (filters != null) {
                scanFilters = new ArrayList<>(filters.size());
                for (ScanFilterCompat filter : filters) {
                    scanFilters.add(filter.toApi21());
                }
            }
            if (settings == null) {
                throw new IllegalStateException("Scan settings are null");
            }
            ScanSettings scanSettings = settings.toApi21();
            adapter.getBluetoothLeScanner().startScan(scanFilters, scanSettings, registerCallback(callbackCompat));
        }
        @Override
        public void startScan(BluetoothAdapter adapter, ScanCallbackCompat callbackCompat) {
            adapter.getBluetoothLeScanner().startScan(registerCallback(callbackCompat));
        }
        @Override
        public void stopScan(BluetoothAdapter adapter, ScanCallbackCompat callbackCompat) {
            API21ScanCallback result = callbackMap.remove(callbackCompat);
            if (result == null) {
                return;
            }
            adapter.getBluetoothLeScanner().stopScan(result);
        }
        private API21ScanCallback registerCallback(ScanCallbackCompat callbackCompat) {
            API21ScanCallback result = callbackMap.get(callbackCompat);
            // Attempting to rescan, just let it fail deeper down.
            if (result != null) {
                return result;
            }
            result = new API21ScanCallback(callbackCompat);
            callbackMap.put(callbackCompat, result);
            return result;
        }
    }
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    static class API18BluetoothLeScannerCompatImpl implements BluetoothLeScannerCompatImpl {
        static final SimpleArrayMap<ScanCallbackCompat, API18ScanCallback> callbackMap = new SimpleArrayMap<>();
        @Override
        public void flushPendingScanResults(BluetoothAdapter adapter, ScanCallbackCompat callbackCompat) {
            // no matching api
            new IllegalArgumentException("this method can not found");
        }
        @SuppressWarnings("deprecation")
        @Override
        public void startScan(BluetoothAdapter adapter, List<ScanFilterCompat> filters, ScanSettingsCompat settings, ScanCallbackCompat callbackCompat) {
            adapter.startLeScan(registerCallback(filters, callbackCompat));
        }
        @SuppressWarnings("deprecation")
        @Override
        public void startScan(BluetoothAdapter adapter, ScanCallbackCompat callbackCompat) {
            adapter.startLeScan(registerCallback(null, callbackCompat));
        }
        @SuppressWarnings("deprecation")
        @Override
        public void stopScan(BluetoothAdapter adapter, ScanCallbackCompat callbackCompat) {
            API18ScanCallback callback = callbackMap.remove(callbackCompat);
            if (callback == null) {
                return;
            }
            adapter.stopLeScan(callback);
        }
        private API18ScanCallback registerCallback(List<ScanFilterCompat> filters, ScanCallbackCompat callbackCompat) {
            API18ScanCallback result = callbackMap.get(callbackCompat);
            // Attempting to rescan, just let it fail deeper down.
            if (result != null) {
                return result;
            }
            result = new API18ScanCallback(filters, callbackCompat);
            callbackMap.put(callbackCompat, result);
            return result;
        }
    }
    static class API18ScanCallback implements BluetoothAdapter.LeScanCallback {
        private final List<ScanFilterCompat> filters;
        private final WeakReference<ScanCallbackCompat> callbackCompatRef;
        API18ScanCallback(List<ScanFilterCompat> filters, ScanCallbackCompat callbackCompat) {
            this.filters = filters;
            this.callbackCompatRef = new WeakReference<>(callbackCompat);
        }
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
            ScanCallbackCompat callbackCompat = callbackCompatRef.get();
            if (callbackCompat == null) return;
            ScanResultCompat result = new ScanResultCompat(
                    device,
                    ScanRecordCompat.parseFromBytes(scanRecord),
                    rssi, System.currentTimeMillis());
            // No filters so return any result
            if (filters == null) {
                callbackCompat.onScanResult(ScanSettingsCompat.CALLBACK_TYPE_ALL_MATCHES, result);
                return;
            }
            // Filters specified, so see if there is a match.
            for (ScanFilterCompat filter : filters) {
                if (filter.matches(result)) {
                    callbackCompat.onScanResult(ScanSettingsCompat.CALLBACK_TYPE_ALL_MATCHES, result);
                    return;
                }
            }
        }
    }
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    static class API21ScanCallback extends ScanCallback {
        private final WeakReference<ScanCallbackCompat> callbackCompatRef;
        API21ScanCallback(ScanCallbackCompat callbackCompat) {
            this.callbackCompatRef = new WeakReference<>(callbackCompat);
        }
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            ScanCallbackCompat callbackCompat = callbackCompatRef.get();
            if (callbackCompat != null) {
                callbackCompat.onScanResult(callbackType, new ScanResultCompat(result));
            }
        }
        @Override
        public void onBatchScanResults(List<ScanResult> results) {
            ScanCallbackCompat callbackCompat = callbackCompatRef.get();
            if (callbackCompat == null) return;
            List<ScanResultCompat> compatResults = new ArrayList<>(results.size());
            for (ScanResult result : results) {
                compatResults.add(new ScanResultCompat(result));
            }
            callbackCompat.onBatchScanResults(compatResults);
        }
        @Override
        public void onScanFailed(int errorCode) {
            ScanCallbackCompat callbackCompat = callbackCompatRef.get();
            if (callbackCompat != null) {
                callbackCompat.onScanFailed(errorCode);
            }
        }
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/BluetoothUuidCompat.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,310 @@
/*
 * Copyright (C) 2009 The Android Open Source Project
 * Copyright (C) 2015 Joe Rogers
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat;
import android.os.ParcelUuid;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.HashSet;
import java.util.UUID;
/**
 * Compat version of BluetoothUuid
 */
/* package */ class BluetoothUuidCompat {
    /* See Bluetooth Assigned Numbers document - SDP section, to get the values of UUIDs
     * for the various services.
     *
     * The following 128 bit values are calculated as:
     *  uuid * 2^96 + BASE_UUID
     */
    public static final ParcelUuid AudioSink =
            ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid AudioSource =
            ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid AdvAudioDist =
            ParcelUuid.fromString("0000110D-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid HSP =
            ParcelUuid.fromString("00001108-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid HSP_AG =
            ParcelUuid.fromString("00001112-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid Handsfree =
            ParcelUuid.fromString("0000111E-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid Handsfree_AG =
            ParcelUuid.fromString("0000111F-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid AvrcpController =
            ParcelUuid.fromString("0000110E-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid AvrcpTarget =
            ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid ObexObjectPush =
            ParcelUuid.fromString("00001105-0000-1000-8000-00805f9b34fb");
    public static final ParcelUuid Hid =
            ParcelUuid.fromString("00001124-0000-1000-8000-00805f9b34fb");
    public static final ParcelUuid Hogp =
            ParcelUuid.fromString("00001812-0000-1000-8000-00805f9b34fb");
    public static final ParcelUuid PANU =
            ParcelUuid.fromString("00001115-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid NAP =
            ParcelUuid.fromString("00001116-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid BNEP =
            ParcelUuid.fromString("0000000f-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid PBAP_PSE =
            ParcelUuid.fromString("0000112f-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid MAP =
            ParcelUuid.fromString("00001134-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid MNS =
            ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid MAS =
            ParcelUuid.fromString("00001132-0000-1000-8000-00805F9B34FB");
    public static final ParcelUuid BASE_UUID =
            ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB");
    /**
     * Length of bytes for 16 bit UUID
     */
    public static final int UUID_BYTES_16_BIT = 2;
    /**
     * Length of bytes for 32 bit UUID
     */
    public static final int UUID_BYTES_32_BIT = 4;
    /**
     * Length of bytes for 128 bit UUID
     */
    public static final int UUID_BYTES_128_BIT = 16;
    public static final ParcelUuid[] RESERVED_UUIDS = {
            AudioSink, AudioSource, AdvAudioDist, HSP, Handsfree, AvrcpController, AvrcpTarget,
            ObexObjectPush, PANU, NAP, MAP, MNS, MAS};
    public static boolean isAudioSource(ParcelUuid uuid) {
        return uuid.equals(AudioSource);
    }
    public static boolean isAudioSink(ParcelUuid uuid) {
        return uuid.equals(AudioSink);
    }
    public static boolean isAdvAudioDist(ParcelUuid uuid) {
        return uuid.equals(AdvAudioDist);
    }
    public static boolean isHandsfree(ParcelUuid uuid) {
        return uuid.equals(Handsfree);
    }
    public static boolean isHeadset(ParcelUuid uuid) {
        return uuid.equals(HSP);
    }
    public static boolean isAvrcpController(ParcelUuid uuid) {
        return uuid.equals(AvrcpController);
    }
    public static boolean isAvrcpTarget(ParcelUuid uuid) {
        return uuid.equals(AvrcpTarget);
    }
    public static boolean isInputDevice(ParcelUuid uuid) {
        return uuid.equals(Hid);
    }
    public static boolean isPanu(ParcelUuid uuid) {
        return uuid.equals(PANU);
    }
    public static boolean isNap(ParcelUuid uuid) {
        return uuid.equals(NAP);
    }
    public static boolean isBnep(ParcelUuid uuid) {
        return uuid.equals(BNEP);
    }
    public static boolean isMap(ParcelUuid uuid) {
        return uuid.equals(MAP);
    }
    public static boolean isMns(ParcelUuid uuid) {
        return uuid.equals(MNS);
    }
    public static boolean isMas(ParcelUuid uuid) {
        return uuid.equals(MAS);
    }
    /**
     * Returns true if ParcelUuid is present in uuidArray
     *
     * @param uuidArray - Array of ParcelUuids
     * @param uuid
     */
    public static boolean isUuidPresent(ParcelUuid[] uuidArray, ParcelUuid uuid) {
        if ((uuidArray == null || uuidArray.length == 0) && uuid == null)
            return true;
        if (uuidArray == null)
            return false;
        for (ParcelUuid element : uuidArray) {
            if (element.equals(uuid)) return true;
        }
        return false;
    }
    /**
     * Returns true if there any common ParcelUuids in uuidA and uuidB.
     *
     * @param uuidA - List of ParcelUuids
     * @param uuidB - List of ParcelUuids
     */
    public static boolean containsAnyUuid(ParcelUuid[] uuidA, ParcelUuid[] uuidB) {
        if (uuidA == null && uuidB == null) return true;
        if (uuidA == null) {
            return uuidB.length == 0;
        }
        if (uuidB == null) {
            return uuidA.length == 0;
        }
        HashSet<ParcelUuid> uuidSet = new HashSet<>(Arrays.asList(uuidA));
        for (ParcelUuid uuid : uuidB) {
            if (uuidSet.contains(uuid)) return true;
        }
        return false;
    }
    /**
     * Returns true if all the ParcelUuids in ParcelUuidB are present in
     * ParcelUuidA
     *
     * @param uuidA - Array of ParcelUuidsA
     * @param uuidB - Array of ParcelUuidsB
     */
    public static boolean containsAllUuids(ParcelUuid[] uuidA, ParcelUuid[] uuidB) {
        if (uuidA == null && uuidB == null) return true;
        if (uuidA == null) {
            return uuidB.length == 0;
        }
        if (uuidB == null) return true;
        HashSet<ParcelUuid> uuidSet = new HashSet<>(Arrays.asList(uuidA));
        for (ParcelUuid uuid : uuidB) {
            if (!uuidSet.contains(uuid)) return false;
        }
        return true;
    }
    /**
     * Extract the Service Identifier or the actual uuid from the Parcel Uuid.
     * For example, if 0000110B-0000-1000-8000-00805F9B34FB is the parcel Uuid,
     * this function will return 110B
     *
     * @param parcelUuid
     * @return the service identifier.
     */
    public static int getServiceIdentifierFromParcelUuid(ParcelUuid parcelUuid) {
        UUID uuid = parcelUuid.getUuid();
        long value = (uuid.getMostSignificantBits() & 0x0000FFFF00000000L) >>> 32;
        return (int) value;
    }
    /**
     * Parse UUID from bytes. The {@code uuidBytes} can represent a 16-bit, 32-bit or 128-bit UUID,
     * but the returned UUID is always in 128-bit format.
     * Note UUID is little endian in Bluetooth.
     *
     * @param uuidBytes Byte representation of uuid.
     * @return {@link ParcelUuid} parsed from bytes.
     * @throws IllegalArgumentException If the {@code uuidBytes} cannot be parsed.
     */
    public static ParcelUuid parseUuidFrom(byte[] uuidBytes) {
        if (uuidBytes == null) {
            throw new IllegalArgumentException("uuidBytes cannot be null");
        }
        int length = uuidBytes.length;
        if (length != UUID_BYTES_16_BIT && length != UUID_BYTES_32_BIT &&
                length != UUID_BYTES_128_BIT) {
            throw new IllegalArgumentException("uuidBytes length invalid - " + length);
        }
        // Construct a 128 bit UUID.
        if (length == UUID_BYTES_128_BIT) {
            ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN);
            long msb = buf.getLong(8);
            long lsb = buf.getLong(0);
            return new ParcelUuid(new UUID(msb, lsb));
        }
        // For 16 bit and 32 bit UUID we need to convert them to 128 bit value.
        // 128_bit_value = uuid * 2^96 + BASE_UUID
        long shortUuid;
        if (length == UUID_BYTES_16_BIT) {
            shortUuid = uuidBytes[0] & 0xFF;
            shortUuid += (uuidBytes[1] & 0xFF) << 8;
        }
        else {
            shortUuid = uuidBytes[0] & 0xFF;
            shortUuid += (uuidBytes[1] & 0xFF) << 8;
            shortUuid += (uuidBytes[2] & 0xFF) << 16;
            shortUuid += (uuidBytes[3] & 0xFF) << 24;
        }
        long msb = BASE_UUID.getUuid().getMostSignificantBits() + (shortUuid << 32);
        long lsb = BASE_UUID.getUuid().getLeastSignificantBits();
        return new ParcelUuid(new UUID(msb, lsb));
    }
    /**
     * Check whether the given parcelUuid can be converted to 16 bit bluetooth uuid.
     *
     * @param parcelUuid
     * @return true if the parcelUuid can be converted to 16 bit uuid, false otherwise.
     */
    public static boolean is16BitUuid(ParcelUuid parcelUuid) {
        UUID uuid = parcelUuid.getUuid();
        if (uuid.getLeastSignificantBits() != BASE_UUID.getUuid().getLeastSignificantBits()) {
            return false;
        }
        return ((uuid.getMostSignificantBits() & 0xFFFF0000FFFFFFFFL) == 0x1000L);
    }
    /**
     * Check whether the given parcelUuid can be converted to 32 bit bluetooth uuid.
     *
     * @param parcelUuid
     * @return true if the parcelUuid can be converted to 32 bit uuid, false otherwise.
     */
    public static boolean is32BitUuid(ParcelUuid parcelUuid) {
        UUID uuid = parcelUuid.getUuid();
        if (uuid.getLeastSignificantBits() != BASE_UUID.getUuid().getLeastSignificantBits()) {
            return false;
        }
        if (is16BitUuid(parcelUuid)) {
            return false;
        }
        return ((uuid.getMostSignificantBits() & 0xFFFFFFFFL) == 0x1000L);
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ObjectsCompat.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
/*
 * Copyright (C) 2013 The Android Open Source Project
 * Copyright (C) 2015 Joe Rogers
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat;
import java.util.Arrays;
/**
 * Subset of utility methods for objects, used by the existing bluetooth framework
 */
/* package */ class ObjectsCompat {
    /* package */ static String toString(Object o) {
        return (o == null) ? "null" : o.toString();
    }
    /* package */ static int hash(Object... values) {
        return Arrays.hashCode(values);
    }
    /* package */ static boolean equals(Object a, Object b) {
        return (a == null) ? (b == null) : a.equals(b);
    }
    /* package */ static boolean deepEquals(Object a, Object b) {
        if (a == null || b == null) {
            return a == b;
        }
        else if (a instanceof Object[] && b instanceof Object[]) {
            return Arrays.deepEquals((Object[]) a, (Object[]) b);
        }
        else if (a instanceof boolean[] && b instanceof boolean[]) {
            return Arrays.equals((boolean[]) a, (boolean[]) b);
        }
        else if (a instanceof byte[] && b instanceof byte[]) {
            return Arrays.equals((byte[]) a, (byte[]) b);
        }
        else if (a instanceof char[] && b instanceof char[]) {
            return Arrays.equals((char[]) a, (char[]) b);
        }
        else if (a instanceof double[] && b instanceof double[]) {
            return Arrays.equals((double[]) a, (double[]) b);
        }
        else if (a instanceof float[] && b instanceof float[]) {
            return Arrays.equals((float[]) a, (float[]) b);
        }
        else if (a instanceof int[] && b instanceof int[]) {
            return Arrays.equals((int[]) a, (int[]) b);
        }
        else if (a instanceof long[] && b instanceof long[]) {
            return Arrays.equals((long[]) a, (long[]) b);
        }
        else if (a instanceof short[] && b instanceof short[]) {
            return Arrays.equals((short[]) a, (short[]) b);
        }
        return a.equals(b);
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ScanCallbackCompat.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
/*
 * Copyright (C) 2014 The Android Open Source Project
 * Copyright (C) 2015 Joe Rogers
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat;
import java.util.List;
/**
 * Bluetooth LE scan callbacks. Scan results are reported using these callbacks.
 *
 * @see BluetoothLeScannerCompat#startScan
 */
public abstract class ScanCallbackCompat {
    /**
     * Fails to start scan as BLE scan with the same settings is already started by the app.
     */
    public static final int SCAN_FAILED_ALREADY_STARTED = 1;
    /**
     * Fails to start scan as app cannot be registered.
     */
    public static final int SCAN_FAILED_APPLICATION_REGISTRATION_FAILED = 2;
    /**
     * Fails to start scan due an internal error
     */
    public static final int SCAN_FAILED_INTERNAL_ERROR = 3;
    /**
     * Fails to start power optimized scan as this feature is not supported.
     */
    public static final int SCAN_FAILED_FEATURE_UNSUPPORTED = 4;
    /**
     * new error code, Fails to scan as location permission is forbid()
     */
    public static final int SCAN_FAILED_LOCATION_PERMISSION_FORBID = 5;
    /**
     * use SDK>=23, The location is not open(if sdk>=23, will not scan any device)
     * http://stackoverflow.com/questions/33043582/bluetooth-low-energy-startscan-on-android-6-0-does-not-find-devices/33045489#33045489
     */
    public static final int SCAN_FAILED_LOCATION_CLOSE = 6;
    /**
     * Callback when a BLE advertisement has been found.
     *
     * @param callbackType Determines how this callback was triggered. Currently could only be
     *                     {@link android.bluetooth.le.ScanSettings#CALLBACK_TYPE_ALL_MATCHES}.
     * @param result       A Bluetooth LE scan result.
     */
    @SuppressWarnings("EmptyMethod")
    public void onScanResult(int callbackType, ScanResultCompat result) {
        // no implementation
    }
    /**
     * Callback when batch results are delivered.
     *
     * @param results List of scan results that are previously scanned.
     */
    @SuppressWarnings("EmptyMethod")
    public void onBatchScanResults(List<ScanResultCompat> results) {
    }
    /**
     * Callback when scan could not be started.
     *
     * @param errorCode Error code (one of SCAN_FAILED_*) for scan failure.
     */
    @SuppressWarnings("EmptyMethod")
    public void onScanFailed(int errorCode) {
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ScanFilterCompat.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,627 @@
/*
 * Copyright (C) 2014 The Android Open Source Project
 * Copyright (C) 2015 Joe Rogers
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.os.Build;
import android.os.Parcel;
import android.os.ParcelUuid;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
 * Criteria for filtering result from Bluetooth LE scans. A {@link ScanFilterCompat} allows clients to
 * restrict scan results to only those that are of interest to them.
 * <p/>
 * Current filtering on the following fields are supported:
 * <li>Service UUIDs which identify the bluetooth gatt services running on the device.
 * <li>Name of remote Bluetooth LE device.
 * <li>Mac address of the remote device.
 * <li>Service data which is the data associated with a service.
 * <li>Manufacturer specific data which is the data associated with a particular manufacturer.
 *
 * @see ScanResult
 * @see android.bluetooth.le.BluetoothLeScanner
 */
public class ScanFilterCompat implements Parcelable {
    @Nullable
    private final String mDeviceName;
    @Nullable
    private final String mDeviceAddress;
    @Nullable
    private final ParcelUuid mServiceUuid;
    @Nullable
    private final ParcelUuid mServiceUuidMask;
    @Nullable
    private final ParcelUuid mServiceDataUuid;
    @Nullable
    private final byte[] mServiceData;
    @Nullable
    private final byte[] mServiceDataMask;
    private final int mManufacturerId;
    @Nullable
    private final byte[] mManufacturerData;
    @Nullable
    private final byte[] mManufacturerDataMask;
    private static final ScanFilterCompat EMPTY = new ScanFilterCompat.Builder().build() ;
    private ScanFilterCompat(@Nullable String name, @Nullable String deviceAddress, @Nullable ParcelUuid uuid,
                             @Nullable ParcelUuid uuidMask, @Nullable ParcelUuid serviceDataUuid,
                             @Nullable byte[] serviceData, @Nullable byte[] serviceDataMask,
                             int manufacturerId, @Nullable byte[] manufacturerData, @Nullable byte[] manufacturerDataMask) {
        mDeviceName = name;
        mServiceUuid = uuid;
        mServiceUuidMask = uuidMask;
        mDeviceAddress = deviceAddress;
        mServiceDataUuid = serviceDataUuid;
        mServiceData = serviceData;
        mServiceDataMask = serviceDataMask;
        mManufacturerId = manufacturerId;
        mManufacturerData = manufacturerData;
        mManufacturerDataMask = manufacturerDataMask;
    }
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public ScanFilter toApi21() {
        ScanFilter.Builder builder = new ScanFilter.Builder();
        if (mDeviceName != null) {
            builder.setDeviceName(mDeviceName);
        }
        if (mServiceUuid != null) {
            builder.setServiceUuid(mServiceUuid, mServiceUuidMask);
        }
        if (mDeviceAddress != null) {
            builder.setDeviceAddress(mDeviceAddress);
        }
        if (mServiceDataUuid != null) {
            builder.setServiceData(mServiceDataUuid, mServiceData, mServiceDataMask);
        }
        if (mManufacturerId < 0) {
            builder.setManufacturerData(mManufacturerId, mManufacturerData, mManufacturerDataMask);
        }
        return builder.build();
    }
    @Override
    public int describeContents() {
        return 0;
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mDeviceName == null ? 0 : 1);
        if (mDeviceName != null) {
            dest.writeString(mDeviceName);
        }
        dest.writeInt(mDeviceAddress == null ? 0 : 1);
        if (mDeviceAddress != null) {
            dest.writeString(mDeviceAddress);
        }
        dest.writeInt(mServiceUuid == null ? 0 : 1);
        if (mServiceUuid != null) {
            dest.writeParcelable(mServiceUuid, flags);
            dest.writeInt(mServiceUuidMask == null ? 0 : 1);
            if (mServiceUuidMask != null) {
                dest.writeParcelable(mServiceUuidMask, flags);
            }
        }
        dest.writeInt(mServiceDataUuid == null ? 0 : 1);
        if (mServiceDataUuid != null) {
            dest.writeParcelable(mServiceDataUuid, flags);
            dest.writeInt(mServiceData == null ? 0 : 1);
            if (mServiceData != null) {
                dest.writeInt(mServiceData.length);
                dest.writeByteArray(mServiceData);
                dest.writeInt(mServiceDataMask == null ? 0 : 1);
                if (mServiceDataMask != null) {
                    dest.writeInt(mServiceDataMask.length);
                    dest.writeByteArray(mServiceDataMask);
                }
            }
        }
        dest.writeInt(mManufacturerId);
        dest.writeInt(mManufacturerData == null ? 0 : 1);
        if (mManufacturerData != null) {
            dest.writeInt(mManufacturerData.length);
            dest.writeByteArray(mManufacturerData);
            dest.writeInt(mManufacturerDataMask == null ? 0 : 1);
            if (mManufacturerDataMask != null) {
                dest.writeInt(mManufacturerDataMask.length);
                dest.writeByteArray(mManufacturerDataMask);
            }
        }
    }
    /**
     * A {@link Creator} to create {@link ScanFilterCompat} from parcel.
     */
    public static final Creator<ScanFilterCompat>
            CREATOR = new Creator<ScanFilterCompat>() {
        @Override
        public ScanFilterCompat[] newArray(int size) {
            return new ScanFilterCompat[size];
        }
        @Override
        public ScanFilterCompat createFromParcel(Parcel in) {
            Builder builder = new Builder();
            if (in.readInt() == 1) {
                builder.setDeviceName(in.readString());
            }
            if (in.readInt() == 1) {
                builder.setDeviceAddress(in.readString());
            }
            if (in.readInt() == 1) {
                ParcelUuid uuid = in.readParcelable(ParcelUuid.class.getClassLoader());
                builder.setServiceUuid(uuid);
                if (in.readInt() == 1) {
                    ParcelUuid uuidMask = in.readParcelable(
                            ParcelUuid.class.getClassLoader());
                    builder.setServiceUuid(uuid, uuidMask);
                }
            }
            if (in.readInt() == 1) {
                ParcelUuid servcieDataUuid =
                        in.readParcelable(ParcelUuid.class.getClassLoader());
                if (in.readInt() == 1) {
                    int serviceDataLength = in.readInt();
                    byte[] serviceData = new byte[serviceDataLength];
                    in.readByteArray(serviceData);
                    if (in.readInt() == 0) {
                        builder.setServiceData(servcieDataUuid, serviceData);
                    }
                    else {
                        int serviceDataMaskLength = in.readInt();
                        byte[] serviceDataMask = new byte[serviceDataMaskLength];
                        in.readByteArray(serviceDataMask);
                        builder.setServiceData(
                                servcieDataUuid, serviceData, serviceDataMask);
                    }
                }
            }
            int manufacturerId = in.readInt();
            if (in.readInt() == 1) {
                int manufacturerDataLength = in.readInt();
                byte[] manufacturerData = new byte[manufacturerDataLength];
                in.readByteArray(manufacturerData);
                if (in.readInt() == 0) {
                    builder.setManufacturerData(manufacturerId, manufacturerData);
                }
                else {
                    int manufacturerDataMaskLength = in.readInt();
                    byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength];
                    in.readByteArray(manufacturerDataMask);
                    builder.setManufacturerData(manufacturerId, manufacturerData,
                            manufacturerDataMask);
                }
            }
            return builder.build();
        }
    };
    /**
     * Returns the filter set the device name field of Bluetooth advertisement data.
     */
    @Nullable
    public String getDeviceName() {
        return mDeviceName;
    }
    /**
     * Returns the filter set on the service uuid.
     */
    @Nullable
    public ParcelUuid getServiceUuid() {
        return mServiceUuid;
    }
    @Nullable
    public ParcelUuid getServiceUuidMask() {
        return mServiceUuidMask;
    }
    @Nullable
    public String getDeviceAddress() {
        return mDeviceAddress;
    }
    @Nullable
    public byte[] getServiceData() {
        return mServiceData;
    }
    @Nullable
    public byte[] getServiceDataMask() {
        return mServiceDataMask;
    }
    @Nullable
    public ParcelUuid getServiceDataUuid() {
        return mServiceDataUuid;
    }
    /**
     * Returns the manufacturer id. -1 if the manufacturer filter is not set.
     */
    public int getManufacturerId() {
        return mManufacturerId;
    }
    @Nullable
    public byte[] getManufacturerData() {
        return mManufacturerData;
    }
    @Nullable
    public byte[] getManufacturerDataMask() {
        return mManufacturerDataMask;
    }
    /**
     * Check if the scan filter matches a {@code scanResultCompat}. A scan result is considered as a match
     * if it matches all the field filters.
     */
    public boolean matches(ScanResultCompat scanResult) {
        if (scanResult == null) {
            return false;
        }
        BluetoothDevice device = scanResult.getDevice();
        // Device match.
        if (mDeviceAddress != null
                && (device == null || !mDeviceAddress.equals(device.getAddress()))) {
            return false;
        }
        ScanRecordCompat scanRecord = scanResult.getScanRecord();
        // Scan record is null but there exist filters on it.
        if (scanRecord == null
                && (mDeviceName != null || mServiceUuid != null || mManufacturerData != null
                || mServiceData != null || mServiceDataUuid != null || mManufacturerId >= 0)) {
            return false;
        }
        // Local name match.
        if (mDeviceName != null && !mDeviceName.equals(scanRecord.getDeviceName())) {
            return false;
        }
        // UUID match.
        if (mServiceUuid != null && !matchesServiceUuids(mServiceUuid, mServiceUuidMask,
                scanRecord.getServiceUuids())) {
            return false;
        }
        // Service data match
        if (mServiceDataUuid != null) {
            if (!matchesPartialData(mServiceData, mServiceDataMask,
                    scanRecord.getServiceData(mServiceDataUuid))) {
                return false;
            }
        }
        // Manufacturer data match.
        if (mManufacturerId >= 0 && scanRecord != null) {
            if (!matchesPartialData(mManufacturerData, mManufacturerDataMask,
                    scanRecord.getManufacturerSpecificData(mManufacturerId))) {
                return false;
            }
        }
        // All filters match.
        return true;
    }
    // Check if the uuid pattern is contained in a list of parcel uuids.
    private boolean matchesServiceUuids(ParcelUuid uuid, ParcelUuid parcelUuidMask,
                                        List<ParcelUuid> uuids) {
        if (uuid == null) {
            return true;
        }
        if (uuids == null) {
            return false;
        }
        for (ParcelUuid parcelUuid : uuids) {
            UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
            if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) {
                return true;
            }
        }
        return false;
    }
    // Check if the uuid pattern matches the particular service uuid.
    private boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) {
        if (mask == null) {
            return uuid.equals(data);
        }
        if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits()) !=
                (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) {
            return false;
        }
        return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits()) ==
                (data.getMostSignificantBits() & mask.getMostSignificantBits()));
    }
    // Check whether the data pattern matches the parsed data.
    private boolean matchesPartialData(byte[] data, byte[] dataMask, byte[] parsedData) {
        if (parsedData == null || parsedData.length < data.length) {
            return false;
        }
        if (dataMask == null) {
            for (int i = 0; i < data.length; ++i) {
                if (parsedData[i] != data[i]) {
                    return false;
                }
            }
            return true;
        }
        for (int i = 0; i < data.length; ++i) {
            if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) {
                return false;
            }
        }
        return true;
    }
    @Override
    public String toString() {
        return "BluetoothLeScanFilter [mDeviceName=" + mDeviceName + ", mDeviceAddress="
                + mDeviceAddress
                + ", mUuid=" + mServiceUuid + ", mUuidMask=" + mServiceUuidMask
                + ", mServiceDataUuid=" + ObjectsCompat.toString(mServiceDataUuid) + ", mServiceData="
                + Arrays.toString(mServiceData) + ", mServiceDataMask="
                + Arrays.toString(mServiceDataMask) + ", mManufacturerId=" + mManufacturerId
                + ", mManufacturerData=" + Arrays.toString(mManufacturerData)
                + ", mManufacturerDataMask=" + Arrays.toString(mManufacturerDataMask) + "]";
    }
    @Override
    public int hashCode() {
        return ObjectsCompat.hash(mDeviceName, mDeviceAddress, mManufacturerId, mManufacturerData,
                mManufacturerDataMask, mServiceDataUuid, mServiceData, mServiceDataMask,
                mServiceUuid, mServiceUuidMask);
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        ScanFilterCompat other = (ScanFilterCompat) obj;
        return (ObjectsCompat.equals(mDeviceName, other.mDeviceName) &&
                ObjectsCompat.equals(mDeviceAddress, other.mDeviceAddress) &&
                mManufacturerId == other.mManufacturerId &&
                ObjectsCompat.deepEquals(mManufacturerData, other.mManufacturerData) &&
                ObjectsCompat.deepEquals(mManufacturerDataMask, other.mManufacturerDataMask) &&
                ObjectsCompat.deepEquals(mServiceDataUuid, other.mServiceDataUuid) &&
                ObjectsCompat.deepEquals(mServiceData, other.mServiceData) &&
                ObjectsCompat.deepEquals(mServiceDataMask, other.mServiceDataMask) &&
                ObjectsCompat.equals(mServiceUuid, other.mServiceUuid) &&
                ObjectsCompat.equals(mServiceUuidMask, other.mServiceUuidMask));
    }
    /**
     * Checks if the scanfilter is empty
     * @hide
     */
    public boolean isAllFieldsEmpty() {
        return EMPTY.equals(this);
    }
    /**
     * Builder class for {@link ScanFilterCompat}.
     */
    public static final class Builder {
        private String mDeviceName;
        private String mDeviceAddress;
        private ParcelUuid mServiceUuid;
        private ParcelUuid mUuidMask;
        private ParcelUuid mServiceDataUuid;
        private byte[] mServiceData;
        private byte[] mServiceDataMask;
        private int mManufacturerId = -1;
        private byte[] mManufacturerData;
        private byte[] mManufacturerDataMask;
        /**
         * Set filter on device name.
         */
        public Builder setDeviceName(String deviceName) {
            mDeviceName = deviceName;
            return this;
        }
        /**
         * Set filter on device address.
         *
         * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
         *                      format of "01:02:03:AB:CD:EF". The device address can be validated using
         *                      {@link BluetoothAdapter#checkBluetoothAddress}.
         * @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
         */
        public Builder setDeviceAddress(String deviceAddress) {
            if (deviceAddress != null && !BluetoothAdapter.checkBluetoothAddress(deviceAddress)) {
                throw new IllegalArgumentException("invalid device address " + deviceAddress);
            }
            mDeviceAddress = deviceAddress;
            return this;
        }
        /**
         * Set filter on service uuid.
         */
        public Builder setServiceUuid(ParcelUuid serviceUuid) {
            mServiceUuid = serviceUuid;
            mUuidMask = null; // clear uuid mask
            return this;
        }
        /**
         * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the
         * {@code serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the
         * bit in {@code serviceUuid}, and 0 to ignore that bit.
         *
         * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but
         *                                  {@code uuidMask} is not {@code null}.
         */
        public Builder setServiceUuid(ParcelUuid serviceUuid, ParcelUuid uuidMask) {
            if (mUuidMask != null && mServiceUuid == null) {
                throw new IllegalArgumentException("uuid is null while uuidMask is not null!");
            }
            mServiceUuid = serviceUuid;
            mUuidMask = uuidMask;
            return this;
        }
        /**
         * Set filtering on service data.
         *
         * @throws IllegalArgumentException If {@code serviceDataUuid} is null.
         */
        public Builder setServiceData(ParcelUuid serviceDataUuid, byte[] serviceData) {
            if (serviceDataUuid == null) {
                throw new IllegalArgumentException("serviceDataUuid is null");
            }
            mServiceDataUuid = serviceDataUuid;
            mServiceData = serviceData;
            mServiceDataMask = null; // clear service data mask
            return this;
        }
        /**
         * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to
         * match the one in service data, otherwise set it to 0 to ignore that bit.
         * <p/>
         * The {@code serviceDataMask} must have the same length of the {@code serviceData}.
         *
         * @throws IllegalArgumentException If {@code serviceDataUuid} is null or
         *                                  {@code serviceDataMask} is {@code null} while {@code serviceData} is not or
         *                                  {@code serviceDataMask} and {@code serviceData} has different length.
         */
        public Builder setServiceData(ParcelUuid serviceDataUuid,
                                      byte[] serviceData, byte[] serviceDataMask) {
            if (serviceDataUuid == null) {
                throw new IllegalArgumentException("serviceDataUuid is null");
            }
            if (mServiceDataMask != null) {
                if (mServiceData == null) {
                    throw new IllegalArgumentException(
                            "serviceData is null while serviceDataMask is not null");
                }
                // Since the mServiceDataMask is a bit mask for mServiceData, the lengths of the two
                // byte array need to be the same.
                if (mServiceData.length != mServiceDataMask.length) {
                    throw new IllegalArgumentException(
                            "size mismatch for service data and service data mask");
                }
            }
            mServiceDataUuid = serviceDataUuid;
            mServiceData = serviceData;
            mServiceDataMask = serviceDataMask;
            return this;
        }
        /**
         * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id.
         * <p/>
         * Note the first two bytes of the {@code manufacturerData} is the manufacturerId.
         *
         * @throws IllegalArgumentException If the {@code manufacturerId} is invalid.
         */
        public Builder setManufacturerData(int manufacturerId, byte[] manufacturerData) {
            if (manufacturerData != null && manufacturerId < 0) {
                throw new IllegalArgumentException("invalid manufacture id");
            }
            mManufacturerId = manufacturerId;
            mManufacturerData = manufacturerData;
            mManufacturerDataMask = null; // clear manufacturer data mask
            return this;
        }
        /**
         * Set filter on partial manufacture data. For any bit in the mask, set it the 1 if it needs
         * to match the one in manufacturer data, otherwise set it to 0.
         * <p/>
         * The {@code manufacturerDataMask} must have the same length of {@code manufacturerData}.
         *
         * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or
         *                                  {@code manufacturerData} is null while {@code manufacturerDataMask} is not,
         *                                  or {@code manufacturerData} and {@code manufacturerDataMask} have different
         *                                  length.
         */
        public Builder setManufacturerData(int manufacturerId, byte[] manufacturerData,
                                           byte[] manufacturerDataMask) {
            if (manufacturerData != null && manufacturerId < 0) {
                throw new IllegalArgumentException("invalid manufacture id");
            }
            if (mManufacturerDataMask != null) {
                if (mManufacturerData == null) {
                    throw new IllegalArgumentException(
                            "manufacturerData is null while manufacturerDataMask is not null");
                }
                // Since the mManufacturerDataMask is a bit mask for mManufacturerData, the lengths
                // of the two byte array need to be the same.
                if (mManufacturerData.length != mManufacturerDataMask.length) {
                    throw new IllegalArgumentException(
                            "size mismatch for manufacturerData and manufacturerDataMask");
                }
            }
            mManufacturerId = manufacturerId;
            mManufacturerData = manufacturerData;
            mManufacturerDataMask = manufacturerDataMask;
            return this;
        }
        /**
         * Build {@link ScanFilterCompat}.
         *
         * @throws IllegalArgumentException If the filter cannot be built.
         */
        public ScanFilterCompat build() {
            return new ScanFilterCompat(mDeviceName, mDeviceAddress,
                    mServiceUuid, mUuidMask,
                    mServiceDataUuid, mServiceData, mServiceDataMask,
                    mManufacturerId, mManufacturerData, mManufacturerDataMask);
        }
    }
}
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ScanRecordCompat.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,353 @@
/*
 * Copyright (C) 2014 The Android Open Source Project
 * Copyright (C) 2015 Joe Rogers
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.blakequ.bluetooth_manager_lib.scan.bluetoothcompat;
import android.annotation.TargetApi;
import android.bluetooth.le.ScanRecord;
import android.os.Build;
import android.os.ParcelUuid;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
import android.util.Log;
import android.util.SparseArray;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
 * Compatible version so the ScanRecord
 */
public class ScanRecordCompat {
    private static final String TAG = "ScanRecordCompat";
    // The following data type values are assigned by Bluetooth SIG.
    // For more details refer to Bluetooth 4.1 specification, Volume 3, Part C, Section 18.
    //https://devzone.nordicsemi.com/documentation/nrf51/4.2.0/html/group___b_l_e___g_a_p___a_d___t_y_p_e___d_e_f_i_n_i_t_i_o_n_s.html
    private static final int DATA_TYPE_FLAGS = 0x01;
    private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL = 0x02;
    private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE = 0x03;
    private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL = 0x04;
    private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE = 0x05;
    private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL = 0x06;
    private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07;
    private static final int DATA_TYPE_LOCAL_NAME_SHORT = 0x08;
    private static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09;
    private static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A;
    private static final int DATA_TYPE_SERVICE_DATA = 0x16;
    private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
    // Flags of the advertising data.
    private final int mAdvertiseFlags;
    @Nullable
    private final List<ParcelUuid> mServiceUuids;
    private final SparseArray<byte[]> mManufacturerSpecificData;
    private final Map<ParcelUuid, byte[]> mServiceData;
    // Transmission power level(in dB).
    private final int mTxPowerLevel;
    // Local name of the Bluetooth LE device.
    private final String mDeviceName;
    // Raw bytes of scan record.
    private final byte[] mBytes;
    /**
     * Returns the advertising flags indicating the discoverable mode and capability of the device.
     * Returns -1 if the flag field is not set.
     */
    public int getAdvertiseFlags() {
        return mAdvertiseFlags;
    }
    /**
     * Returns a list of service UUIDs within the advertisement that are used to identify the
     * bluetooth GATT services.
     */
    public List<ParcelUuid> getServiceUuids() {
        return mServiceUuids;
    }
    /**
     * Returns a sparse array of manufacturer identifier and its corresponding manufacturer specific
     * data.
     */
    public SparseArray<byte[]> getManufacturerSpecificData() {
        return mManufacturerSpecificData;
    }
    /**
     * Returns the manufacturer specific data associated with the manufacturer id. Returns
     * {@code null} if the {@code manufacturerId} is not found.
     */
    @Nullable
    public byte[] getManufacturerSpecificData(int manufacturerId) {
        return mManufacturerSpecificData.get(manufacturerId);
    }
    /**
     * Returns a map of service UUID and its corresponding service data.
     */
    public Map<ParcelUuid, byte[]> getServiceData() {
        return mServiceData;
    }
    /**
     * Returns the service data byte array associated with the {@code serviceUuid}. Returns
     * {@code null} if the {@code serviceDataUuid} is not found.
     */
    @Nullable
    public byte[] getServiceData(ParcelUuid serviceDataUuid) {
        if (serviceDataUuid == null) {
            return null;
        }
        return mServiceData.get(serviceDataUuid);
    }
    /**
     * Returns the transmission power level of the packet in dBm. Returns {@link Integer#MIN_VALUE}
     * if the field is not set. This value can be used to calculate the path loss of a received
     * packet using the following equation:
     * <p>
     * <code>pathloss = txPowerLevel - rssi</code>
     */
    public int getTxPowerLevel() {
        return mTxPowerLevel;
    }
    /**
     * Returns the local name of the BLE device. The is a UTF-8 encoded string.
     */
    @Nullable
    public String getDeviceName() {
        return mDeviceName;
    }
    /**
     * Returns raw bytes of scan record.
     */
    public byte[] getBytes() {
        return mBytes;
    }
    private ScanRecordCompat(@Nullable List<ParcelUuid> serviceUuids,
                       SparseArray<byte[]> manufacturerData,
                       Map<ParcelUuid, byte[]> serviceData,
                       int advertiseFlags, int txPowerLevel,
                       String localName, byte[] bytes) {
        mServiceUuids = serviceUuids;
        mManufacturerSpecificData = manufacturerData;
        mServiceData = serviceData;
        mDeviceName = localName;
        mAdvertiseFlags = advertiseFlags;
        mTxPowerLevel = txPowerLevel;
        mBytes = bytes;
    }
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    ScanRecordCompat(ScanRecord record) {
        mServiceUuids = record.getServiceUuids();
        mManufacturerSpecificData = record.getManufacturerSpecificData();
        mServiceData = record.getServiceData();
        mDeviceName = record.getDeviceName();
        mAdvertiseFlags = record.getAdvertiseFlags();
        mTxPowerLevel = record.getTxPowerLevel();
        mBytes = record.getBytes();
    }
    /**
     * Parse scan record bytes to {@link ScanRecordCompat}.
     * <p>
     * The format is defined in Bluetooth 4.1 specification, Volume 3, Part C, Section 11 and 18.
     * <p>
     * All numerical multi-byte entities and values shall use little-endian <strong>byte</strong>
     * order.
     *
     * @param scanRecord The scan record of Bluetooth LE advertisement and/or scan response.
     */
    public static ScanRecordCompat parseFromBytes(byte[] scanRecord) {
        if (scanRecord == null) {
            return null;
        }
        int currentPos = 0;
        int advertiseFlag = -1;
        List<ParcelUuid> serviceUuids = new ArrayList<>();
        String localName = null;
        int txPowerLevel = Integer.MIN_VALUE;
        SparseArray<byte[]> manufacturerData = new SparseArray<>();
        Map<ParcelUuid, byte[]> serviceData = new ArrayMap<>();
        try {
            while (currentPos < scanRecord.length) {
                // length is unsigned int.
                int length = scanRecord[currentPos++] & 0xFF;
                if (length == 0) {
                    break;
                }
                // Note the length includes the length of the field type itself.
                int dataLength = length - 1;
                // fieldType is unsigned int.
                int fieldType = scanRecord[currentPos++] & 0xFF;
                switch (fieldType) {
                    case DATA_TYPE_FLAGS:
                        advertiseFlag = scanRecord[currentPos] & 0xFF;
                        break;
                    case DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL:
                    case DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE:
                        parseServiceUuid(scanRecord, currentPos,
                                dataLength, BluetoothUuidCompat.UUID_BYTES_16_BIT, serviceUuids);
                        break;
                    case DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL:
                    case DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE:
                        parseServiceUuid(scanRecord, currentPos, dataLength,
                                BluetoothUuidCompat.UUID_BYTES_32_BIT, serviceUuids);
                        break;
                    case DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL:
                    case DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE:
                        parseServiceUuid(scanRecord, currentPos, dataLength,
                                BluetoothUuidCompat.UUID_BYTES_128_BIT, serviceUuids);
                        break;
                    case DATA_TYPE_LOCAL_NAME_SHORT:
                    case DATA_TYPE_LOCAL_NAME_COMPLETE:
                        localName = new String(
                                extractBytes(scanRecord, currentPos, dataLength));
                        break;
                    case DATA_TYPE_TX_POWER_LEVEL:
                        txPowerLevel = scanRecord[currentPos];
                        break;
                    case DATA_TYPE_SERVICE_DATA:
                        // The first two bytes of the service data are service data UUID in little
                        // endian. The rest bytes are service data.
                        int serviceUuidLength = BluetoothUuidCompat.UUID_BYTES_16_BIT;
                        byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos,
                                serviceUuidLength);
                        ParcelUuid serviceDataUuid = BluetoothUuidCompat.parseUuidFrom(
                                serviceDataUuidBytes);
                        byte[] serviceDataArray = extractBytes(scanRecord,
                                currentPos + serviceUuidLength, dataLength - serviceUuidLength);
                        serviceData.put(serviceDataUuid, serviceDataArray);
                        break;
                    case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA:
                        // The first two bytes of the manufacturer specific data are
                        // manufacturer ids in little endian.
                        int manufacturerId = ((scanRecord[currentPos + 1] & 0xFF) << 8) +
                                (scanRecord[currentPos] & 0xFF);
                        byte[] manufacturerDataBytes = extractBytes(scanRecord, currentPos + 2,
                                dataLength - 2);
                        manufacturerData.put(manufacturerId, manufacturerDataBytes);
                        break;
                    default:
                        // Just ignore, we don't handle such data type.
                        break;
                }
                currentPos += dataLength;
            }
            if (serviceUuids.isEmpty()) {
                serviceUuids = null;
            }
            return new ScanRecordCompat(serviceUuids, manufacturerData, serviceData,
                    advertiseFlag, txPowerLevel, localName, scanRecord);
        } catch (Exception e) {
            Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord));
            // As the record is invalid, ignore all the parsed results for this packet
            // and return an empty record with raw scanRecord bytes in results
            return new ScanRecordCompat(null, null, null, -1, Integer.MIN_VALUE, null, scanRecord);
        }
    }
    @Override
    public String toString() {
        return "ScanRecord [mAdvertiseFlags=" + mAdvertiseFlags + ", mServiceUuids=" + mServiceUuids
                + ", mManufacturerSpecificData=" + toString(mManufacturerSpecificData)
                + ", mServiceData=" + toString(mServiceData)
                + ", mTxPowerLevel=" + mTxPowerLevel + ", mDeviceName=" + mDeviceName + "]";
    }
    // Parse service UUIDs.
    private static int parseServiceUuid(byte[] scanRecord, int currentPos, int dataLength,
                                        int uuidLength, List<ParcelUuid> serviceUuids) {
        while (dataLength > 0) {
            byte[] uuidBytes = extractBytes(scanRecord, currentPos,
                    uuidLength);
            serviceUuids.add(BluetoothUuidCompat.parseUuidFrom(uuidBytes));
            dataLength -= uuidLength;
            currentPos += uuidLength;
        }
        return currentPos;
    }
    // Helper method to extract bytes from byte array.
    private static byte[] extractBytes(byte[] scanRecord, int start, int length) {
        byte[] bytes = new byte[length];
        System.arraycopy(scanRecord, start, bytes, 0, length);
        return bytes;
    }
    /**
     * Returns a string composed from a {@link SparseArray}.
     */
    static String toString(SparseArray<byte[]> array) {
        if (array == null) {
            return "null";
        }
        if (array.size() == 0) {
            return "{}";
        }
        StringBuilder buffer = new StringBuilder();
        buffer.append('{');
        for (int i = 0; i < array.size(); ++i) {
            buffer.append(array.keyAt(i)).append("=").append(Arrays.toString(array.valueAt(i)));
        }
        buffer.append('}');
        return buffer.toString();
    }
    /**
     * Returns a string composed from a {@link Map}.
     */
    static <T> String toString(Map<T, byte[]> map) {
        if (map == null) {
            return "null";
        }
        if (map.isEmpty()) {
            return "{}";
        }
        StringBuilder buffer = new StringBuilder();
        buffer.append('{');
        Iterator<Map.Entry<T, byte[]>> it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<T, byte[]> entry = it.next();
            T key = entry.getKey();
            buffer.append(key).append("=").append(Arrays.toString(map.get(key)));
            if (it.hasNext()) {
                buffer.append(", ");
            }
        }
        buffer.append('}');
        return buffer.toString();
    }
}
在上述文件截断后对比
bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ScanResultCompat.java bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/scan/bluetoothcompat/ScanSettingsCompat.java bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/util/BluetoothUtils.java bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/util/ByteUtils.java bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/util/LimitedLinkHashMap.java bluetooth-manager-lib/src/main/res/values/strings.xml build.gradle comb.jks gradle.properties settings.gradle