├── .gitignore ├── .idea ├── .name ├── codeStyles │ └── Project.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output-metadata.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── h13studio │ │ └── fpv │ │ ├── AdvancedSettings.java │ │ ├── AdvancedSettingsAdapter.java │ │ ├── BluetoothActivity.java │ │ ├── BluetoothClient.java │ │ ├── BluetoothRecyclerAdapter.java │ │ ├── CheckUpdate.java │ │ ├── ControlAddressTextWatcher.java │ │ ├── ControlModeItemSelected.java │ │ ├── ControllerOnCheckedChanged.java │ │ ├── FPVAddressTextWatcher.java │ │ ├── FPVModeItemSelected.java │ │ ├── FileUtil.java │ │ ├── MainActivity.java │ │ ├── MsgObject.java │ │ ├── PingTask.java │ │ ├── Settings.java │ │ ├── SwitchOnCheckedChanged.java │ │ ├── TCPClient.java │ │ ├── UnpairedBluetoothRecyclerAdapter.java │ │ └── fpvActivity.java │ └── res │ ├── drawable │ ├── advanced_setting_shape1.xml │ ├── fullsizeicon.png │ ├── function_button_ripple.xml │ ├── ic_baseline_arrow_back_24.xml │ ├── ic_baseline_autorenew_24.xml │ ├── ic_baseline_book_24.xml │ ├── ic_baseline_code_24.xml │ ├── ic_baseline_dehaze_24.xml │ ├── ic_baseline_favorite_24.xml │ ├── ic_baseline_lock_24.xml │ ├── ic_baseline_lock_open_24.xml │ ├── ic_baseline_person_24.xml │ ├── ic_baseline_rotate_right_24.xml │ ├── ic_baseline_settings_24.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_gray_background.xml │ ├── icon.png │ ├── rocker.png │ ├── rockeronfocused.png │ ├── rockerunfocused.png │ ├── side_nav_bar.xml │ └── start_button_ripple.xml │ ├── layout-land │ └── fpv_web.xml │ ├── layout │ ├── activity_advanced_settings.xml │ ├── activity_bluetooth.xml │ ├── activity_main.xml │ ├── advanced_settings_controller.xml │ ├── advanced_settings_edittextview.xml │ ├── advanced_settings_modeselect.xml │ ├── advanced_settings_switch.xml │ ├── bluetooth_item_layout.xml │ ├── fragment_gallery.xml │ ├── nav_header_main.xml │ └── unpaired_bluetooth_item_layout.xml │ ├── menu │ └── activity_main_drawer.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ ├── ic_launcher_round.xml │ └── ic_launcher_round_gray.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── array.xml │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | fpv -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fpv-Remote-Control 2 | 一个可以用4G/WIFI图传和遥控飞行器的上位机APP(支持蓝牙遥控) 3 | # 下载 4 | 5 | https://github.com/h13-0/fpv-Remote-Control/releases 6 | https://www.coolapk.com/apk/com.h13studio.fpv 7 | 8 | # 图传部分 9 | 针对图传部分,我只提供 Linux开发板 的配置方法,对于使用'esp-eyes'的请自行按照http/udp协议使用。 10 | 11 | 截止2020-08-10,无论是小白还是大佬,都推荐用 `Motion` 进行http图传。 12 | 13 | ## http图传环境搭建 14 | **实测延迟0.2秒** 15 | 这里以`Motion`为例,在Linux的Shell环境中: 16 | 先尝试: 17 | ``` 18 | sudo apt-get install motion 19 | ``` 20 | 如果没有这个库的话 需要用源码安装 21 | https://motion-project.github.io 22 | ``` 23 | make -j4 24 | sudo make install 25 | ``` 26 | 27 | 然后去`/etc/motion/motion.conf`里面正确选择cameraID, 28 | 输入: 29 | ``` 30 | sudo nano /etc/motion/motion.conf 31 | ``` 32 | 没有`nano`文本编辑器的可自行安装或用`vim`替换 33 | 34 | 然后找到下文,配置Camera ID: 35 | ``` 36 | ########################################################### 37 | # Capture device options 38 | ############################################################ 39 | 40 | # Videodevice to be used for capturing (default /dev/video0) 41 | # for FreeBSD default is /dev/bktr0 42 | videodevice /dev/video0 43 | ``` 44 | 45 | 然后配置分辨率 帧率 端口等。 46 | 随后在内网环境中,用浏览器打开对应的网址 47 | eg: 48 | ``` 49 | http://192.168.1.100:8081 50 | ``` 51 | 然后如果能正常显示图传图像,则http图传配置部分完成。 52 | 53 | ## UDP图传配置 54 | **目前无论是APP端还是Linux端UDP图传均未调试优化完毕,以下方案目前延迟在0.8秒左右** 55 | 先说一下截止2020-08-10的udp图传方案吧。 56 | Linux端主动向APP端发送UDP数据,然后APP端读取本地端口的UDP视频流播放。 57 | 但是Sever端主动向Client端发数据并不是很好的解决办法,不过这个问题以后再解决,目前主要需要先优化UDP图传的延迟和质量。 58 | ### ffmpeg环境搭建 59 | ffmpeg官网: 60 | https://ffmpeg.org/ 61 | 62 | 先试试你是否已经完全安装ffmpeg 63 | ``` 64 | ffmpeg -f video4linux2 -s 640*480 -i /dev/video0 -vcodec h264 -preset ultrafast -tune zerolatency -r 30 -b:v 1024K -movflags +faststart -f mpegts udp://127.0.0.1:8888 65 | ``` 66 | 注意, `/dev/video0` 是你摄像头的文件位置。 67 | 如果报错,请按照以下步骤执行。如果没有报错,请直接跳到UDP环节的最后一个步骤。 68 | 69 | #### 源码安装 70 | 71 | 建议从Github上下载源码到电脑再传到到Linux板子上并解压 72 | https://github.com/FFmpeg/FFmpeg 73 | ,然后: 74 | ``` 75 | cd ffmpeg 76 | ./configure --enable-shared --enable-libx264 --enable-gpl 77 | make #请自行根据Linux开发板性能开启多线程编译 78 | sudo make install 79 | ``` 80 | 如果肉身在墙外或者Linux开发板自带梯子的话,推荐直接使用官方git源 81 | ``` 82 | git clone https://git.ffmpeg.org/ffmpeg.git 83 | ``` 84 | 来代替github下载步骤。 85 | 如果不知道怎么代替的话,建议忽略这一部分。 86 | 87 | 然后执行: 88 | ``` 89 | sudo ffmpeg -f video4linux2 -s 640*480 -i /dev/video0 -vcodec h264 -preset ultrafast -tune zerolatency -r 30 -b:v 1024K -movflags +faststart -f mpegts udp://你手机IP:8888 90 | ``` 91 | 92 | #### 报错&解决方案: 93 | ##### libavdevice.so.58: 94 | 即报错: 95 | ``` 96 | ffmpeg: error while loading shared libraries: libavdevice.so.58: cannot open shared object file: No such file or directory 97 | ``` 98 | 则原因为 未将 `libavdevice.so.58` 等依赖文件添加到path中。 99 | 输入: 100 | ``` 101 | ldd ffmpeg 102 | ``` 103 | 查看对应缺失的依赖 104 | ``` 105 | ldd ffmpeg 106 | libavdevice.so.58 => not found 107 | libavfilter.so.7 => not found 108 | libavformat.so.58 => not found 109 | libavcodec.so.58 => not found 110 | libpostproc.so.55 => not found 111 | libswresample.so.3 => not found 112 | libswscale.so.5 => not found 113 | libavutil.so.56 => not found 114 | libm.so.6 => /lib/arm-linux-gnueabihf/libm.so.6 (0xb6ed8000) 115 | libpthread.so.0 => /lib/arm-linux-gnueabihf/libpthread.so.0 (0xb6eb4000) 116 | libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0xb6dc7000) 117 | /lib/ld-linux-armhf.so.3 (0xb6f76000) 118 | ``` 119 | 然后 120 | ``` 121 | ls /usr/local/lib/libavdevice.so.58 122 | ``` 123 | 一般上述文件都在这个目录里。 124 | 125 | 验证: 126 | ``` 127 | export LD_LIBRARY_PATH=/usr/local/lib/ 128 | ``` 129 | 这一个命令只是暂时性的把依赖目录加入当前Shell的环境变量中,重新打开Shell即失效,较为安全。 130 | 然后再次输入 131 | ``` 132 | ffmpeg 133 | ``` 134 | 如果没有报错,请按照以下步骤执行: 135 | ``` 136 | sudo nano /etc/ld.so.conf 137 | ``` 138 | 在文件中**添加**路径: 139 | ``` 140 | /usr/local/lib 141 | sudo ldconfig 142 | ``` 143 | 144 | 接下来加入全局环境变量路径: 145 | ``` 146 | sudo nano /etc/profile 147 | ``` 148 | **下列命令具有非常高危险性,请仔细核对后再操作** 149 | 在文档中加入: 150 | ``` 151 | export PATH="/usr/local/ffmpeg/bin:$PATH" 152 | ``` 153 | **一定要注意加上最后的 “:$PATH” 不然你会丢失所有环境变量** 154 | 然后保存并运行 155 | ``` 156 | source /etc/profile 157 | ``` 158 | 丢失环境变量后的解决方法: 159 | **一定不要重启,一定不要关闭当前Shell** 160 | ``` 161 | /usr/bin/vim /etc/profile 162 | ``` 163 | 然后正确修改和保存后, 164 | ``` 165 | source /etc/profile 166 | ``` 167 | 168 | ##### ERROR: libx264 not found: 169 | 170 | ``` 171 | ERROR: libx264 not found 172 | ``` 173 | 则需要安装 `libx264` 编解码器。 174 | 175 | 则: 176 | ``` 177 | git clone git@code.videolan.org:videolan/x264.git 178 | ./configure --enable-shared --enable-pthread --enable-pic 179 | make 180 | sudo make install 181 | ``` 182 | 然后再次执行回到ffmpeg目录再次从 `./configure --enable-shared --enable-libx264 --enable-gpl` 开始执行即可。 183 | 184 | ##### can't configure encoder 185 | 大概率是你 `configure` 的时候没有设置 `--enable-libx264` 186 | 187 | ##### make时报错:recompile with -fPIC 188 | 出现该现象的大致有两种原因 189 | 1.依赖库没有安装好 190 | 2.更改ffmpeg的 `./configure` 后未清理上次编译缓存 191 | 192 | 对于1 请自行排坑 193 | 对于2 建议先执行 `make clean` 后再 `make` 194 | 195 | #### 对于源码安装的一些建议 196 | 并不是所有库都提供了可靠的卸载方式,所以建议将所有 `sudo make install` 替换为 `checkinstall` 197 | 他会自动帮你编译好的文件打包为 deb 或者 rpm 包,再用对应包管理器进行安装,方便卸载。 198 | checkinstall安装: 199 | ``` 200 | apt install auto-apt checkinstall 201 | ``` 202 | 使用: 203 | ``` 204 | ./configure --enable-shared --enable-pthread --enable-pic 205 | make 206 | checkinstall 207 | sudo dpkg -i xxx.deb 208 | ``` 209 | 210 | #### ffmpeg命令的官方文档 211 | https://trac.ffmpeg.org/wiki/StreamingGuide 212 | 213 | 给几个昨晚我从里面扒到的几个比较有用的设置吧 214 | 215 | 一个最精简的UDP图传指令是这样的 216 | ``` 217 | ffmpeg -f video4linux2 -i /dev/video0 -vcodec h264 -acodec copy -f mpegts udp://你手机IP:8888 218 | ``` 219 | 然后可以这样拆分: 220 | `ffmpeg` `-f video4linux2` `-i /dev/video0` `-vcodec h264` `-f mpegts udp://你手机IP:8888` 221 | 222 | **重要配置:** 223 | 编码器零延迟,应加到 `-vcodec h264` 后 224 | ``` 225 | -tune zerolatency 226 | ``` 227 | 228 | 预设超快速,应加到 `-vcodec h264` 后 229 | ``` 230 | -preset ultrafast 231 | ``` 232 | 233 | 分辨率设置,应加到 `-f video4linux2` 后 234 | ``` 235 | -s 640*480 236 | ``` 237 | 238 | 码率设置,应加到 `-vcodec h264` 后 239 | ``` 240 | -b:v 1024K 241 | ``` 242 | 243 | 帧率设置,应加到 `-vcodec h264` 后 244 | ``` 245 | -r 30 246 | ``` 247 | 248 | 允许快速连接,他会将一些重要的信息移动到文件头,可以让你在完全下载之前就开始播放, 249 | ``` 250 | -movflags +faststart 251 | ``` 252 | 253 | 不过,就算这样折腾完,仍然有0.8秒的延迟... 254 | 255 | **中等重要配置:** 256 | I帧设置: 257 | ``` 258 | -keyint_min 15 -g 15 -sc_threshold 0 259 | ``` 260 | 261 | 最后,命令也就成了这样 262 | ``` 263 | sudo ffmpeg -f video4linux2 -s 640*480 -i /dev/video0 -vcodec h264 -preset ultrafast -tune zerolatency -r 30 -b:v 1024K -movflags +faststart -f mpegts udp://你手机IP:8888 264 | ``` 265 | 266 | 最后,送你一个 rtp 图传的配置,rtp也是基于udp,如果用rtp的话,接收端配置会复杂一些,并且效果和udp也没有明显区别,故这里不推荐 267 | 268 | ``` 269 | sudo ffmpeg -f video4linux2 -s 320*240 -i /dev/video0 -vcodec h264 -preset ultrafast -tune zerolatency -r 30 -b:v 1024K -movflags +faststart -f rtp rtp://192.168.1.154:8888 -itsscale 1 270 | ``` 271 | 272 | # 网络连接部分 273 | **本APP支持任何形式的网络连接,包括4G,WIFI等** 274 | ## 4G连接 275 | 这里推荐使用Zerotier进行内网穿透连接而不推荐使用ipv6直连。 276 | ### Zerotier 277 | Zerotier官网: 278 | https://www.zerotier.com/ 279 | #### Linux端 280 | ``` 281 | curl -s https://install.zerotier.com | sudo bash 282 | ``` 283 | 然后创建和加入虚拟局域网。 284 | #### APP接收端 285 | 去上文提到的'Zerotier'官网下载对应APP,创建和加入虚拟局域网即可。 286 | ### ipv6访问 287 | 因为ipv6是动态变化的,并且ipv6的资源在ipv4下无法访问,故不推荐使用。 288 | 如果迫不得已一定要使用ipv6,则推荐相应DDNS工具绑定域名。 289 | 290 | # 远程控制部分 291 | 本APP提供两种远控方案,TCP和蓝牙串口。其中Linux开发板上请使用TCP方式,单片机上可以配合蓝牙串口模块使用(推荐)也可以配合esp8266等使用TCP控制。 292 | ## 回传数据格式V1.0 293 | 因为考虑到要使用蓝牙传输数据,故这里使用二进制数据包传递。 294 | 数据包基础格式: 295 | '固定包头 0x66','ID','value xn'...,'固定包尾 0x70','固定包尾 0x76' 296 | 其中 0x66 0x70 0x76 分别为字符 'f' 'p' 'v'的二进制值,即本app包名。 297 | ### RockerView摇杆 298 | 'f','ID:0x0x','distance','value','value','p','v' 299 | 其中 'ID:0x0x' 则表示 '0x00' '0x01' '0x02'等 都有可能是摇杆控件的标识字节 300 | ``` 301 | eg: 302 | 'f','0x00','0x07','0x01','0x05','p','v' 303 | 则表示: 304 | 第 1 个摇杆返回的数据,其: 305 | 'distance' = '0x00' //distance为摇杆距离圆心的距离(暂不支持) 306 | 'value' = '0x0105' //value为摇杆角度,拆分为低八位和高八位进行传输 307 | 308 | eg: 309 | 'f','0x01','0x10','0x00','0xf5','p','v' 310 | 则表示: 311 | 第 2 个摇杆返回的数据,其: 312 | 'distance' = '0x10' //distance为摇杆距离圆心的距离(暂不支持) 313 | 'value' = '0x00f5' //value为摇杆角度,拆分为低八位和高八位进行传输 314 | ``` 315 | distance特性暂不支持 316 | 317 | ### Slider滑杆 318 | 'f','ID:0x1x','value','p','v' 319 | 320 | ``` 321 | eg: 322 | 'f','0x10','0x55','p','v' 323 | 则表示: 324 | 第 1 个滑杆返回的数据,其: 325 | 'value' = '0x55' //摇杆的有效值为0x55 326 | ``` 327 | ### Button按钮 328 | 'f','0x2x','value','p','v' 329 | ``` 330 | eg: 331 | 'f','0x22','0x01','p','v' 332 | 则表示: 333 | 写着 3 的按钮被按下。(因为按钮上的文字是从1开始的) 334 | ``` 335 | ## 接收数据格式 336 | **因为数据收到后将直接展示到UI界面,故请回传字符串,并以"\r\n"结尾** 337 | 好了,就这么多。 338 | 339 | # 解析库文件 340 | 咕咕咕,过两天可能顺便更新一下协议再放出。 341 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 30 5 | buildToolsVersion "30.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.h13studio.fpv" 9 | minSdkVersion 23 10 | targetSdkVersion 30 11 | //versionCode 1 12 | //versionName "1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: "libs", include: ["*.jar"]) 27 | //noinspection GradleCompatible 28 | implementation 'com.android.support:recyclerview-v7:29.0.3' 29 | implementation 'androidx.appcompat:appcompat:1.1.0' 30 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 31 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 32 | implementation 'com.google.android.material:material:1.0.0' 33 | implementation 'androidx.navigation:navigation-fragment:2.1.0' 34 | implementation 'androidx.navigation:navigation-ui:2.1.0' 35 | implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' 36 | implementation 'com.github.kongqw:AndroidRocker:1.0.1' 37 | implementation 'androidx.recyclerview:recyclerview:1.1.0' 38 | implementation 'com.github.GcsSloop:Rocker:v1.1.1' 39 | implementation 'com.alibaba:fastjson:1.1.70.android' 40 | compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.0' 41 | testImplementation 'junit:junit:4.12' 42 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 43 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 44 | 45 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.h13studio.fpv", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "properties": [], 14 | "versionCode": 9, 15 | "versionName": "1.4.5.20200903", 16 | "enabled": true, 17 | "outputFile": "app-release.apk" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/AdvancedSettings.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | import androidx.appcompat.widget.AppCompatSeekBar; 5 | import androidx.appcompat.widget.SwitchCompat; 6 | import androidx.recyclerview.widget.DefaultItemAnimator; 7 | import androidx.recyclerview.widget.LinearLayoutManager; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | import android.app.Activity; 11 | import android.bluetooth.le.ScanCallback; 12 | import android.bluetooth.le.ScanResult; 13 | import android.content.Intent; 14 | import android.content.SharedPreferences; 15 | import android.os.Bundle; 16 | import android.view.View; 17 | import android.widget.CompoundButton; 18 | 19 | import androidx.appcompat.widget.Toolbar; 20 | 21 | import com.kongqw.rockerlibrary.view.RockerView; 22 | 23 | public class AdvancedSettings extends AppCompatActivity { 24 | private RecyclerView recyclerView; 25 | private Toolbar toolbar; 26 | private AdvancedSettingsAdapter recycleradapter; 27 | private SwitchCompat switchl,switchr; 28 | private RockerView rockerviewl,rockerviewr; 29 | private AppCompatSeekBar seekbarl,seekbarr; 30 | 31 | private Settings settings; 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_advanced_settings); 37 | 38 | recyclerView = findViewById(R.id.AdvancedSettingsRecyclerview); 39 | toolbar = findViewById(R.id.AdvancedSettingsToolBar); 40 | 41 | settings = new Settings(getSharedPreferences("Settings",MODE_PRIVATE)); 42 | 43 | //初始化Toolbar 44 | setSupportActionBar(toolbar); 45 | toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24); 46 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 47 | @Override 48 | public void onClick(View view) { 49 | finish(); 50 | } 51 | }); 52 | 53 | recycleradapter = new AdvancedSettingsAdapter(settings); 54 | LinearLayoutManager layoutManager = new LinearLayoutManager(this ); 55 | recyclerView.setLayoutManager(layoutManager); 56 | //设置增加或删除条目的动画 57 | recyclerView.setItemAnimator( new DefaultItemAnimator()); 58 | recyclerView.setAdapter(recycleradapter); 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/AdvancedSettingsAdapter.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.text.TextWatcher; 5 | import android.util.Log; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.AdapterView; 10 | import android.widget.CompoundButton; 11 | import android.widget.EditText; 12 | import android.widget.LinearLayout; 13 | import android.widget.Spinner; 14 | import android.widget.Switch; 15 | import android.widget.TextView; 16 | 17 | import androidx.annotation.NonNull; 18 | import androidx.appcompat.widget.AppCompatSeekBar; 19 | import androidx.appcompat.widget.SwitchCompat; 20 | import androidx.recyclerview.widget.RecyclerView; 21 | 22 | import com.kongqw.rockerlibrary.view.RockerView; 23 | 24 | public class AdvancedSettingsAdapter extends RecyclerView.Adapter { 25 | private static final int ControllerSettings = 0; 26 | private static final int ModeSettings = 1; 27 | private static final int SwitchSettings = 2; 28 | 29 | private Settings settings; 30 | 31 | public AdvancedSettingsAdapter(Settings settings){ 32 | this.settings = settings; 33 | } 34 | 35 | @NonNull 36 | @Override 37 | public AdvancedSettingsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 38 | LayoutInflater mInflater = LayoutInflater.from(parent.getContext()); 39 | AdvancedSettingsAdapter.ViewHolder holder = null; 40 | 41 | 42 | switch (viewType){ 43 | case ControllerSettings:{ 44 | View v = mInflater.inflate(R.layout.advanced_settings_controller,parent,false); 45 | holder = new ControlViewHolder(v); 46 | break; 47 | } 48 | 49 | case ModeSettings:{ 50 | View v = mInflater.inflate(R.layout.advanced_settings_modeselect,parent,false); 51 | holder = new ModeViewHolder(v); 52 | break; 53 | } 54 | 55 | case SwitchSettings:{ 56 | View v = mInflater.inflate(R.layout.advanced_settings_switch,parent,false); 57 | holder = new SwitchHolder(v); 58 | break; 59 | } 60 | default:{ 61 | return null; 62 | } 63 | } 64 | return holder; 65 | } 66 | 67 | @Override 68 | public int getItemViewType(int position) { 69 | switch (position){ 70 | case 0:{ 71 | return ControllerSettings; 72 | } 73 | 74 | case 1:{ 75 | return ModeSettings; 76 | } 77 | 78 | case 2:{ 79 | return SwitchSettings; 80 | } 81 | 82 | default:{ 83 | return -1; 84 | } 85 | } 86 | } 87 | 88 | @Override 89 | public void onBindViewHolder(@NonNull final AdvancedSettingsAdapter.ViewHolder holder, int position) { 90 | if(holder instanceof ControlViewHolder){ 91 | 92 | //Mode为Fales则为摇杆,为True则为滑杆 93 | if (settings.getModeLeft()) { 94 | ((ControlViewHolder) holder).rockerviewl.setVisibility(View.INVISIBLE); 95 | ((ControlViewHolder) holder).seekbarl.setVisibility(View.VISIBLE); 96 | ((ControlViewHolder) holder).switchl.setChecked(true); 97 | ((ControlViewHolder) holder).model.setText("滑杆"); 98 | } else { 99 | ((ControlViewHolder) holder).rockerviewl.setVisibility(View.VISIBLE); 100 | ((ControlViewHolder) holder).seekbarl.setVisibility(View.INVISIBLE); 101 | ((ControlViewHolder) holder).switchl.setChecked(false); 102 | ((ControlViewHolder) holder).model.setText("摇杆"); 103 | } 104 | 105 | if (settings.getModeRight()) { 106 | ((ControlViewHolder) holder).rockerviewr.setVisibility(View.INVISIBLE); 107 | ((ControlViewHolder) holder).seekbarr.setVisibility(View.VISIBLE); 108 | ((ControlViewHolder) holder).switchr.setChecked(true); 109 | ((ControlViewHolder) holder).moder.setText("滑杆"); 110 | } else { 111 | ((ControlViewHolder) holder).rockerviewr.setVisibility(View.VISIBLE); 112 | ((ControlViewHolder) holder).seekbarr.setVisibility(View.INVISIBLE); 113 | ((ControlViewHolder) holder).switchr.setChecked(false); 114 | ((ControlViewHolder) holder).moder.setText("滑杆"); 115 | } 116 | 117 | //注册监听事件 118 | ControllerOnCheckedChanged controllerOnCheckedChanged = new ControllerOnCheckedChanged((ControlViewHolder) holder,settings); 119 | ((ControlViewHolder) holder).switchl.setTag("SwitchLeft"); 120 | ((ControlViewHolder) holder).switchl.setOnCheckedChangeListener(controllerOnCheckedChanged); 121 | ((ControlViewHolder) holder).switchr.setTag("SwitchRight"); 122 | ((ControlViewHolder) holder).switchr.setOnCheckedChangeListener(controllerOnCheckedChanged); 123 | 124 | }else if(holder instanceof ModeViewHolder){ 125 | 126 | //初始化UI 127 | ((ModeViewHolder) holder).FPVMode.setSelection(settings.getFPVMode()); 128 | ((ModeViewHolder) holder).ControlMode.setSelection(settings.getControlMode()); 129 | 130 | switch (settings.getFPVMode()){ 131 | case 1:{ 132 | ((ModeViewHolder) holder).FPVAddress.setText(settings.gethttpAddress()); 133 | break; 134 | } 135 | 136 | case 2:{ 137 | ((ModeViewHolder) holder).FPVAddress.setText(settings.getUDPAddress()); 138 | break; 139 | } 140 | 141 | case 3:{ 142 | ((ModeViewHolder) holder).FPVAddress.setText(settings.getPhotoAddress()); 143 | break; 144 | } 145 | 146 | default:{ 147 | break; 148 | } 149 | } 150 | 151 | switch (settings.getControlMode()){ 152 | case 0:{ 153 | ((ModeViewHolder) holder).ControlAddress.setText(settings.getTCPAddress()); 154 | break; 155 | } 156 | 157 | case 1:{ 158 | ((ModeViewHolder) holder).ControlAddress.setText(settings.getBluetoothAddress()); 159 | break; 160 | } 161 | 162 | default:{ 163 | break; 164 | } 165 | } 166 | 167 | //注册监听事件 168 | FPVModeItemSelected fpvModeItemSelected = new FPVModeItemSelected((ModeViewHolder) holder,settings); 169 | ((ModeViewHolder) holder).FPVMode.setOnItemSelectedListener(fpvModeItemSelected); 170 | 171 | ControlModeItemSelected controlModeItemSelected = new ControlModeItemSelected((ModeViewHolder) holder,settings); 172 | ((ModeViewHolder) holder).ControlMode.setOnItemSelectedListener(controlModeItemSelected); 173 | 174 | FPVAddressTextWatcher fpvAddressTextWatcher = new FPVAddressTextWatcher((ModeViewHolder) holder,settings); 175 | ((ModeViewHolder) holder).FPVAddress.addTextChangedListener(fpvAddressTextWatcher); 176 | 177 | ControlAddressTextWatcher controlAddressTextWatcher = new ControlAddressTextWatcher((ModeViewHolder) holder,settings); 178 | ((ModeViewHolder) holder).ControlAddress.addTextChangedListener(controlAddressTextWatcher); 179 | 180 | }else if (holder instanceof SwitchHolder){ 181 | 182 | //初始化UI 183 | ((SwitchHolder) holder).CheckConfig.setChecked(settings.getCheckConfig()); 184 | ((SwitchHolder) holder).CheckUpdate.setChecked(settings.getCheckUpdate()); 185 | 186 | //注册监听事件 187 | SwitchOnCheckedChanged switchOnCheckedChanged = new SwitchOnCheckedChanged((SwitchHolder) holder,settings); 188 | ((SwitchHolder) holder).CheckConfig.setOnCheckedChangeListener(switchOnCheckedChanged); 189 | ((SwitchHolder) holder).CheckUpdate.setOnCheckedChangeListener(switchOnCheckedChanged); 190 | 191 | } 192 | } 193 | 194 | 195 | 196 | @Override 197 | public int getItemCount() { 198 | return 3; 199 | } 200 | 201 | class ViewHolder extends RecyclerView.ViewHolder { 202 | 203 | @SuppressLint("ResourceType") 204 | public ViewHolder(@NonNull View itemView) { 205 | super(itemView); 206 | 207 | } 208 | } 209 | 210 | class ControlViewHolder extends AdvancedSettingsAdapter.ViewHolder { 211 | RockerView rockerviewl,rockerviewr; 212 | AppCompatSeekBar seekbarl,seekbarr; 213 | SwitchCompat switchl,switchr; 214 | TextView model,moder; 215 | 216 | @SuppressLint("ResourceType") 217 | public ControlViewHolder(@NonNull View itemView) { 218 | super(itemView); 219 | rockerviewl = itemView.findViewById(R.id.rockerViewdemol); 220 | rockerviewr = itemView.findViewById(R.id.rockerViewdemor); 221 | seekbarl = itemView.findViewById(R.id.seekbardemol); 222 | seekbarr = itemView.findViewById(R.id.seekbardemor); 223 | switchl = itemView.findViewById(R.id.Switchl); 224 | switchr = itemView.findViewById(R.id.Switchr); 225 | model = itemView.findViewById(R.id.model); 226 | moder = itemView.findViewById(R.id.moder); 227 | } 228 | } 229 | 230 | class ModeViewHolder extends AdvancedSettingsAdapter.ViewHolder { 231 | Spinner FPVMode,ControlMode; 232 | EditText FPVAddress,ControlAddress; 233 | 234 | @SuppressLint("ResourceType") 235 | public ModeViewHolder(@NonNull View itemView) { 236 | super(itemView); 237 | FPVMode = itemView.findViewById(R.id.DefaultFPVMode); 238 | ControlMode = itemView.findViewById(R.id.DefaultControlMode); 239 | FPVAddress = itemView.findViewById(R.id.DefaultFPVAddress); 240 | ControlAddress = itemView.findViewById(R.id.DefaultControlAddress); 241 | } 242 | } 243 | 244 | class SwitchHolder extends AdvancedSettingsAdapter.ViewHolder { 245 | Switch CheckConfig,CheckUpdate; 246 | 247 | @SuppressLint("ResourceType") 248 | public SwitchHolder(@NonNull View itemView) { 249 | super(itemView); 250 | CheckUpdate = itemView.findViewById(R.id.CheckUpdate); 251 | CheckConfig = itemView.findViewById(R.id.CheckConfig); 252 | } 253 | } 254 | 255 | } 256 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/BluetoothActivity.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import androidx.appcompat.app.AlertDialog; 4 | import androidx.appcompat.app.AppCompatActivity; 5 | import androidx.core.view.GravityCompat; 6 | import androidx.recyclerview.widget.DefaultItemAnimator; 7 | import androidx.recyclerview.widget.LinearLayoutManager; 8 | import androidx.recyclerview.widget.OrientationHelper; 9 | import androidx.recyclerview.widget.RecyclerView; 10 | 11 | import android.annotation.SuppressLint; 12 | import android.bluetooth.BluetoothAdapter; 13 | import android.bluetooth.BluetoothDevice; 14 | import android.bluetooth.BluetoothManager; 15 | import android.bluetooth.le.BluetoothLeScanner; 16 | import android.bluetooth.le.ScanCallback; 17 | import android.bluetooth.le.ScanResult; 18 | import android.content.Context; 19 | import android.content.Intent; 20 | import android.content.pm.PackageManager; 21 | import android.os.Bundle; 22 | import android.telephony.RadioAccessSpecifier; 23 | import android.util.Log; 24 | import android.view.KeyEvent; 25 | import android.view.View; 26 | import android.widget.Adapter; 27 | import android.widget.Toast; 28 | import androidx.appcompat.widget.Toolbar; 29 | 30 | import java.util.ArrayList; 31 | import java.util.Iterator; 32 | import java.util.List; 33 | import java.util.Set; 34 | import java.util.logging.Handler; 35 | 36 | public class BluetoothActivity extends AppCompatActivity { 37 | private RecyclerView targetlist; 38 | private RecyclerView unpariedtargetlist; 39 | private BluetoothRecyclerAdapter targetadapter; 40 | private BluetoothRecyclerAdapter unpairedtargetadapter; 41 | private Toolbar toolbar; 42 | private boolean checkpass = true; 43 | private List unpairedmac = new ArrayList(); 44 | private BluetoothLeScanner scanner; 45 | 46 | @SuppressLint("WrongConstant") 47 | @Override 48 | protected void onCreate(Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | setContentView(R.layout.activity_bluetooth); 51 | 52 | targetadapter = new BluetoothRecyclerAdapter(); 53 | unpairedtargetadapter = new BluetoothRecyclerAdapter(); 54 | 55 | targetlist = findViewById(R.id.bluetoothlist); 56 | unpariedtargetlist = findViewById(R.id.unpaired_bluetooth_list); 57 | toolbar = findViewById(R.id.bluetoothbar); 58 | 59 | //初始化Toolbar 60 | setSupportActionBar(toolbar); 61 | toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24); 62 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 63 | @Override 64 | public void onClick(View view) { 65 | scanner.stopScan(new ScanCallback() { 66 | @Override 67 | public void onScanResult(int callbackType, ScanResult result) { 68 | super.onScanResult(callbackType, result); 69 | } 70 | }); 71 | finish(); 72 | } 73 | }); 74 | 75 | LinearLayoutManager layoutManager = new LinearLayoutManager(this ); 76 | //设置布局管理器 77 | unpariedtargetlist.setLayoutManager(layoutManager); 78 | //设置为垂直布局,这也是默认的 79 | layoutManager.setOrientation(OrientationHelper. VERTICAL); 80 | //设置增加或删除条目的动画 81 | targetlist.setItemAnimator( new DefaultItemAnimator()); 82 | targetlist.setAdapter(targetadapter); 83 | 84 | LinearLayoutManager unpairedlayoutManager = new LinearLayoutManager(this ); 85 | //设置为垂直布局,这也是默认的 86 | unpairedlayoutManager.setOrientation(OrientationHelper. VERTICAL); 87 | targetlist.setLayoutManager(unpairedlayoutManager); 88 | 89 | unpariedtargetlist.setItemAnimator( new DefaultItemAnimator()); 90 | unpariedtargetlist.setAdapter(unpairedtargetadapter); 91 | 92 | BluetoothAdapter bluetoothAdapter; 93 | int REQUEST_ENABLE_BT = 1; 94 | 95 | // 判断手机硬件支持蓝牙 96 | if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { 97 | Toast.makeText(getApplicationContext(), "这台手机不支持蓝牙串口,砸了吧", Toast.LENGTH_SHORT).show(); 98 | checkpass = false; 99 | } 100 | 101 | //获取手机本地的蓝牙适配器 102 | 103 | final BluetoothManager bluetoothManager = 104 | (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); 105 | 106 | bluetoothAdapter = bluetoothManager.getAdapter(); 107 | 108 | 109 | //将已配对设备加入列表 110 | Set devices = bluetoothAdapter.getBondedDevices(); 111 | for(Iterator iter = devices.iterator();iter.hasNext();) 112 | { 113 | BluetoothDevice device = iter.next(); 114 | targetadapter.AddItem(device.getName(),device.getAddress()); 115 | } 116 | 117 | //adapter添加监听事件 118 | targetadapter.setOnclick(new BluetoothRecyclerAdapter.OnClick(){ 119 | public void onClick(View view) { 120 | Log.i("ClickID", (String) view.getTag()); 121 | Intent intent = getIntent(); 122 | //这里使用bundle来传输数据 123 | Bundle bundle = new Bundle(); 124 | //传输的内容仍然是键值对的形式 125 | bundle.putString("Mac",(String) view.getTag()); 126 | intent.putExtras(bundle); 127 | setResult(RESULT_OK,intent); 128 | finish(); 129 | } 130 | }); 131 | 132 | //开始扫描未配对设备 133 | scanner = bluetoothAdapter.getBluetoothLeScanner(); 134 | scanner.startScan(new ScanCallback() { 135 | @Override 136 | public void onScanResult(int callbackType, ScanResult result) { 137 | super.onScanResult(callbackType, result); 138 | BluetoothDevice device = result.getDevice(); 139 | Log.i("Unpaired",device.getAddress()); 140 | if(!unpairedmac.contains(device.getAddress())){ 141 | unpairedmac.add(device.getAddress()); 142 | unpairedtargetadapter.AddItem(device.getName(),device.getAddress()); 143 | } 144 | } 145 | }); 146 | } 147 | 148 | @Override 149 | public boolean onKeyDown(int keyCode, KeyEvent event) { 150 | if (keyCode == KeyEvent.KEYCODE_BACK) { 151 | scanner.stopScan(new ScanCallback() { 152 | @Override 153 | public void onScanResult(int callbackType, ScanResult result) { 154 | super.onScanResult(callbackType, result); 155 | } 156 | }); 157 | finish(); 158 | } 159 | return super.onKeyDown(keyCode,event); 160 | } 161 | } -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/BluetoothClient.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.bluetooth.BluetoothAdapter; 4 | import android.bluetooth.BluetoothDevice; 5 | import android.bluetooth.BluetoothManager; 6 | import android.bluetooth.BluetoothSocket; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.pm.PackageManager; 10 | import android.util.Log; 11 | import android.widget.Toast; 12 | 13 | 14 | import java.io.BufferedReader; 15 | import java.io.IOException; 16 | import java.io.InputStreamReader; 17 | import java.lang.reflect.Method; 18 | import java.net.Socket; 19 | import java.util.UUID; 20 | 21 | public class BluetoothClient { 22 | private BluetoothSocket socket = null; 23 | private BluetoothDevice device; 24 | private BluetoothAdapter bluetoothAdapter; 25 | private boolean checkpass = true; 26 | private String mac; 27 | private boolean dataoccuping,MSGoccuping; 28 | private String MSG = new String(); 29 | private byte[] data; 30 | private OnMainCallBack mOnMainCallBack; 31 | 32 | public BluetoothClient(String Mac, OnMainCallBack mainCallBack){ 33 | mac = Mac; 34 | Log.i("Mac",mac); 35 | // 蓝牙串口服务对应的UUID。如使用的是其它蓝牙服务,需更改下面的字符串 36 | UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); 37 | 38 | bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 39 | mOnMainCallBack = mainCallBack; 40 | if(bluetoothAdapter == null) 41 | { 42 | Log.w("Adapetr","BluetoothAdapter is null."); 43 | } 44 | 45 | device = bluetoothAdapter.getRemoteDevice(mac); 46 | 47 | try { 48 | socket = device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")); 49 | }catch (IOException e){ 50 | Log.w("Bluetooth",e.toString()); 51 | } 52 | 53 | ConnectDevice(); 54 | 55 | //发送线程 56 | new Thread() { 57 | @Override 58 | public void run() { 59 | while (true) { 60 | if (socket.isConnected()) { 61 | try { 62 | SendtoSever(); 63 | } catch (IOException e) { 64 | e.printStackTrace(); 65 | } 66 | } else { 67 | Log.w("Bluetooth", "Bluetooth Client lose connection."); 68 | mOnMainCallBack.onMainCallBack("Bluetooth Client lose connection.\r\n"); 69 | if (socket != null) { 70 | ConnectDevice(); 71 | } 72 | } 73 | } 74 | } 75 | }.start(); 76 | 77 | 78 | //接收线程 79 | new Thread() { 80 | @Override 81 | public void run() { 82 | while (true) { 83 | if (socket.isConnected()) { 84 | try { 85 | BufferedReader msg = new BufferedReader(new InputStreamReader(socket.getInputStream())); 86 | if ((msg != null) && (msg.readLine().length() != 0)) { 87 | mOnMainCallBack.onMainCallBack(msg.readLine() + "\r\n"); 88 | } 89 | } catch (IOException e) { 90 | e.printStackTrace(); 91 | } 92 | } else { 93 | try { 94 | Thread.sleep(300); 95 | } catch (InterruptedException e) { 96 | e.printStackTrace(); 97 | } 98 | } 99 | } 100 | } 101 | }.start(); 102 | } 103 | 104 | //发送 105 | private void SendtoSever() throws IOException { 106 | while (true) { 107 | if (socket.isConnected()) { 108 | if (MSG != "") { 109 | MSGoccuping = true; 110 | socket.getOutputStream().write(MSG.getBytes()); 111 | socket.getOutputStream().flush(); 112 | MSG = ""; 113 | MSGoccuping = false; 114 | } 115 | 116 | if (data != null) { 117 | dataoccuping = true; 118 | socket.getOutputStream().write(data); 119 | socket.getOutputStream().flush(); 120 | data = null; 121 | dataoccuping = false; 122 | } 123 | } 124 | } 125 | } 126 | 127 | //连接到目标蓝牙设备 128 | protected void ConnectDevice() { 129 | try { 130 | // 连接建立之前的先配对 131 | if (device.getBondState() == BluetoothDevice.BOND_NONE) { 132 | Method creMethod = BluetoothDevice.class 133 | .getMethod("createBond"); 134 | Log.e("TAG", "开始配对"); 135 | creMethod.invoke(device); 136 | } else { 137 | } 138 | } catch (Exception e) { 139 | Log.e("Bluetooth","配对失败"); 140 | e.printStackTrace(); 141 | } 142 | bluetoothAdapter.cancelDiscovery(); 143 | try { 144 | if(socket != null) { 145 | socket.connect(); 146 | } 147 | Log.i("Connect","OK"); 148 | mOnMainCallBack.onMainCallBack("Bluetooth Client Connected.\r\n"); 149 | } catch (IOException e) { 150 | e.printStackTrace(); 151 | } 152 | } 153 | 154 | public void disconnect(){ 155 | try { 156 | socket.close(); 157 | }catch (IOException e){ 158 | Log.i("Connect",e.toString()); 159 | } 160 | } 161 | 162 | public void SenMsg(byte[] Data){ 163 | if(!dataoccuping) { 164 | data = Data; 165 | } 166 | } 167 | 168 | public void SenMsg(String string){ 169 | if(!MSGoccuping){ 170 | MSG = string; 171 | } 172 | } 173 | 174 | /**主线程回调接口*/ 175 | public interface OnMainCallBack{ 176 | void onMainCallBack(String data); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/BluetoothRecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.res.Resources; 5 | import android.util.Log; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.Adapter; 10 | import android.widget.LinearLayout; 11 | import android.widget.TextView; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.recyclerview.widget.RecyclerView; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | public class BluetoothRecyclerAdapter extends RecyclerView.Adapter { 20 | private List mac; 21 | private List name; 22 | private OnClick onclick; 23 | 24 | @NonNull 25 | @Override 26 | public BluetoothRecyclerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, final int viewType) { 27 | @SuppressLint("ResourceType") View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.bluetooth_item_layout,parent,false); 28 | return new ViewHolder(view); 29 | } 30 | 31 | @Override 32 | public void onBindViewHolder(@NonNull BluetoothRecyclerAdapter.ViewHolder holder, final int position) { 33 | if(this.getItemCount() != 0) { 34 | holder.bluetoothmac.setText(mac.get(position)); 35 | holder.bluetoothname.setText(name.get(position)); 36 | holder.linearlayout.setOnClickListener(onclick); 37 | holder.linearlayout.setTag(mac.get(position)); 38 | } 39 | } 40 | 41 | @Override 42 | public int getItemCount() { 43 | return name.size(); 44 | } 45 | 46 | class ViewHolder extends RecyclerView.ViewHolder { 47 | LinearLayout linearlayout; 48 | TextView bluetoothname,bluetoothmac; 49 | 50 | @SuppressLint("ResourceType") 51 | public ViewHolder(@NonNull View itemView) { 52 | super(itemView); 53 | 54 | linearlayout = (LinearLayout) itemView.findViewById(R.id.bluetoothitem); 55 | bluetoothname = (TextView) itemView.findViewById(R.id.bluetoothname); 56 | bluetoothmac = (TextView) itemView.findViewById(R.id.bluetoothmac); 57 | } 58 | } 59 | 60 | public void AddItem(String Name,String Mac){ 61 | name.add(Name); 62 | mac.add(Mac); 63 | } 64 | 65 | public BluetoothRecyclerAdapter(){ 66 | mac = new ArrayList(); 67 | name = new ArrayList(); 68 | } 69 | 70 | public static class OnClick implements View.OnClickListener{ 71 | @Override 72 | public void onClick(View view) { 73 | //Log.i("ClickID",""); 74 | } 75 | } 76 | 77 | public void setOnclick(OnClick onClick){ 78 | onclick = onClick; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/CheckUpdate.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.content.pm.PackageInfo; 4 | import android.content.pm.PackageManager; 5 | import android.os.Looper; 6 | import android.os.Trace; 7 | import android.util.Log; 8 | 9 | import com.alibaba.fastjson.JSON; 10 | import com.alibaba.fastjson.JSONObject; 11 | import com.alibaba.fastjson.annotation.JSONField; 12 | 13 | import org.apache.commons.lang3.StringEscapeUtils; 14 | 15 | import java.io.BufferedReader; 16 | import java.io.IOException; 17 | import java.io.InputStreamReader; 18 | import java.net.HttpURLConnection; 19 | import java.net.URL; 20 | 21 | import javax.crypto.spec.PSource; 22 | 23 | public class CheckUpdate extends Thread { 24 | private CheckUpdate.OnMainCallBack mOnMainCallBack; 25 | 26 | public CheckUpdate(final int VersionCode, final CheckUpdate.OnMainCallBack mainCallBack){ 27 | new Thread(){ 28 | public void run() { 29 | int failedtimes = 0; 30 | boolean NeedtoUpGrade = false; 31 | super.run(); 32 | Looper.prepare(); 33 | while (failedtimes < 3){ 34 | try { 35 | URL url = new URL("http://www.h13studio.com/DownLoad/Ver/com.h13studio.fpv"); 36 | HttpURLConnection connnection = (HttpURLConnection) url.openConnection(); 37 | 38 | //默认值我GET 39 | connnection.setRequestMethod("GET"); 40 | 41 | int responseCode = connnection.getResponseCode(); 42 | if(responseCode == 200){ 43 | System.out.println("Response Code : " + responseCode); 44 | 45 | BufferedReader in = new BufferedReader( 46 | new InputStreamReader(connnection.getInputStream())); 47 | String inputLine; 48 | StringBuffer response = new StringBuffer(); 49 | 50 | while ((inputLine = in.readLine()) != null) { 51 | response.append(inputLine + "\r\n"); 52 | } 53 | in.close(); 54 | 55 | JSONObject jsonObj = JSON.parseObject(response.toString()); 56 | 57 | 58 | if(jsonObj.get("Latest version Code") != null){ 59 | if(VersionCode < (int)jsonObj.get("Latest version Code")){ 60 | NeedtoUpGrade = true; 61 | } else { 62 | NeedtoUpGrade = false; 63 | } 64 | } 65 | 66 | Log.i("Update", String.valueOf(jsonObj.get("Latest version Code"))); 67 | break; 68 | }else { 69 | failedtimes ++; 70 | } 71 | }catch (IOException e){ 72 | e.printStackTrace(); 73 | } 74 | } 75 | 76 | failedtimes = 0; 77 | while ((NeedtoUpGrade) && (failedtimes < 3)){ 78 | try { 79 | URL url = new URL("http://www.h13studio.com/DownLoad/Ver/com.h13studio.fpv.updatelog"); 80 | HttpURLConnection connnection = (HttpURLConnection) url.openConnection(); 81 | 82 | //默认值我GET 83 | connnection.setRequestMethod("GET"); 84 | 85 | int responseCode = connnection.getResponseCode(); 86 | if (responseCode == 200) { 87 | System.out.println("Response Code : " + responseCode); 88 | 89 | BufferedReader in = new BufferedReader( 90 | new InputStreamReader(connnection.getInputStream())); 91 | String inputLine; 92 | StringBuffer response = new StringBuffer(); 93 | 94 | while ((inputLine = in.readLine()) != null) { 95 | response.append(inputLine + "\r\n"); 96 | } 97 | in.close(); 98 | mainCallBack.onMainCallBack(true,response.toString()); 99 | Log.i("Update",response.toString()); 100 | break; 101 | } else { 102 | failedtimes++; 103 | Log.i("Update","Faild"); 104 | } 105 | }catch (IOException e){ 106 | 107 | } 108 | } 109 | } 110 | }.start(); 111 | } 112 | 113 | /**主线程回调接口*/ 114 | public interface OnMainCallBack{ 115 | void onMainCallBack(boolean NewVersion, String UpdateLog); 116 | } 117 | 118 | private class VersionJson{ 119 | @JSONField(name = "Latest version Code") 120 | public int VersionCode; 121 | 122 | @JSONField(name = "Latest version") 123 | public String VersionName; 124 | 125 | @JSONField(name = "Update Log") 126 | public String UpdateLog; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/ControlAddressTextWatcher.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.text.Editable; 4 | import android.text.TextWatcher; 5 | 6 | public class ControlAddressTextWatcher implements TextWatcher { 7 | private AdvancedSettingsAdapter.ModeViewHolder holder; 8 | private Settings settings; 9 | 10 | public ControlAddressTextWatcher(final AdvancedSettingsAdapter.ModeViewHolder holder,Settings settings){ 11 | this.holder = holder; 12 | this.settings = settings; 13 | } 14 | 15 | @Override 16 | public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { 17 | 18 | } 19 | 20 | @Override 21 | public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { 22 | 23 | } 24 | 25 | @Override 26 | public void afterTextChanged(Editable editable) { 27 | switch (holder.ControlMode.getSelectedItemPosition()){ 28 | case 0:{ 29 | settings.setTCPAddress(editable.toString()); 30 | break; 31 | } 32 | 33 | case 1:{ 34 | settings.setBluetoothAddress(editable.toString()); 35 | break; 36 | } 37 | 38 | default:{ 39 | break; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/ControlModeItemSelected.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.text.SpannableString; 4 | import android.view.View; 5 | import android.widget.AdapterView; 6 | 7 | public class ControlModeItemSelected implements AdapterView.OnItemSelectedListener{ 8 | private Settings settings; 9 | private AdvancedSettingsAdapter.ModeViewHolder holder; 10 | 11 | public ControlModeItemSelected(final AdvancedSettingsAdapter.ModeViewHolder holder,Settings settings){ 12 | this.holder = holder; 13 | this.settings = settings; 14 | } 15 | 16 | @Override 17 | public void onItemSelected(AdapterView adapterView, View view, int i, long l) { 18 | settings.setConrtolMode(i); 19 | if(i == 1){ 20 | holder.ControlAddress.setHint(new SpannableString("暂时不支持自动填充蓝牙MAC地址,请从主页面复制过来")); 21 | } else { 22 | holder.ControlAddress.setHint(new SpannableString("")); 23 | } 24 | } 25 | 26 | @Override 27 | public void onNothingSelected(AdapterView adapterView) { 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/ControllerOnCheckedChanged.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.util.Log; 4 | import android.view.View; 5 | import android.widget.CompoundButton; 6 | 7 | public class ControllerOnCheckedChanged implements CompoundButton.OnCheckedChangeListener{ 8 | private AdvancedSettingsAdapter.ControlViewHolder holder; 9 | private Settings settings; 10 | 11 | public ControllerOnCheckedChanged(final AdvancedSettingsAdapter.ControlViewHolder holder,Settings settings){ 12 | this.holder = holder; 13 | this.settings = settings; 14 | } 15 | 16 | @Override 17 | public void onCheckedChanged(CompoundButton compoundButton, boolean b) { 18 | Log.i("CheckedChanged","OnChanged"); 19 | switch (compoundButton.getId()){ 20 | case R.id.Switchl:{ 21 | if(b) { 22 | holder.rockerviewl.setVisibility(View.INVISIBLE); 23 | holder.seekbarl.setVisibility(View.VISIBLE); 24 | holder.model.setText("滑杆"); 25 | }else { 26 | holder.rockerviewl.setVisibility(View.VISIBLE); 27 | holder.seekbarl.setVisibility(View.INVISIBLE); 28 | holder.model.setText("摇杆"); 29 | } 30 | 31 | settings.setModeLeft(b); 32 | break; 33 | } 34 | case R.id.Switchr:{ 35 | if(b){ 36 | holder.rockerviewr.setVisibility(View.INVISIBLE); 37 | holder.seekbarr.setVisibility(View.VISIBLE); 38 | holder.moder.setText("滑杆"); 39 | }else{ 40 | holder.rockerviewr.setVisibility(View.VISIBLE); 41 | holder.seekbarr.setVisibility(View.INVISIBLE); 42 | holder.moder.setText("摇杆"); 43 | } 44 | 45 | settings.setModeRight(b); 46 | break; 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/FPVAddressTextWatcher.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.text.Editable; 4 | import android.text.TextWatcher; 5 | import android.util.Log; 6 | 7 | public class FPVAddressTextWatcher implements TextWatcher { 8 | private Settings settings; 9 | private AdvancedSettingsAdapter.ModeViewHolder holder; 10 | 11 | public FPVAddressTextWatcher(final AdvancedSettingsAdapter.ModeViewHolder holder,Settings settings){ 12 | this.holder = holder; 13 | this.settings = settings; 14 | } 15 | @Override 16 | public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { 17 | 18 | } 19 | 20 | @Override 21 | public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { 22 | 23 | } 24 | 25 | @Override 26 | public void afterTextChanged(Editable editable) { 27 | switch (holder.FPVMode.getSelectedItemPosition()){ 28 | case 0:{ 29 | settings.sethttpAddress(editable.toString()); 30 | break; 31 | } 32 | 33 | case 1:{ 34 | settings.setUDPAddress(editable.toString()); 35 | break; 36 | } 37 | 38 | case 2:{ 39 | settings.setPhotoAddress(editable.toString()); 40 | break; 41 | } 42 | 43 | default:{ 44 | break; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/FPVModeItemSelected.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.bluetooth.BluetoothAdapter; 4 | import android.bluetooth.BluetoothManager; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.net.Uri; 9 | import android.os.Bundle; 10 | import android.text.SpannableString; 11 | import android.text.TextUtils; 12 | import android.util.Log; 13 | import android.view.View; 14 | import android.widget.AdapterView; 15 | import android.widget.Toast; 16 | 17 | import static androidx.core.app.ActivityCompat.startActivityForResult; 18 | 19 | public class FPVModeItemSelected implements AdapterView.OnItemSelectedListener{ 20 | private Settings settings; 21 | private AdvancedSettingsAdapter.ModeViewHolder holder; 22 | 23 | public FPVModeItemSelected(final AdvancedSettingsAdapter.ModeViewHolder holder,Settings settings){ 24 | this.holder = holder; 25 | this.settings = settings; 26 | } 27 | 28 | @Override 29 | public void onItemSelected(AdapterView adapterView, View view, int i, long l) { 30 | settings.setFPVMode(i); 31 | if(i == 2){ 32 | holder.FPVAddress.setHint(new SpannableString("暂时不支持自动填充图片URL,请从主页面复制过来")); 33 | } else { 34 | holder.FPVAddress.setHint(new SpannableString("")); 35 | } 36 | } 37 | 38 | @Override 39 | public void onNothingSelected(AdapterView adapterView) { 40 | 41 | } 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/FileUtil.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.ContentUris; 5 | import android.content.Context; 6 | import android.database.Cursor; 7 | import android.net.Uri; 8 | import android.os.Build; 9 | import android.os.Environment; 10 | import android.provider.DocumentsContract; 11 | import android.provider.MediaStore; 12 | 13 | public class FileUtil { 14 | /** 15 | * 根据URI获取文件真实路径(兼容多张机型) 16 | * @param context 17 | * @param uri 18 | * @return 19 | */ 20 | public static String getFilePathByUri(Context context, Uri uri) { 21 | if ("content".equalsIgnoreCase(uri.getScheme())) { 22 | 23 | int sdkVersion = Build.VERSION.SDK_INT; 24 | if (sdkVersion >= 19) { // api >= 19 25 | return getRealPathFromUriAboveApi19(context, uri); 26 | } else { // api < 19 27 | return getRealPathFromUriBelowAPI19(context, uri); 28 | } 29 | } else if ("file".equalsIgnoreCase(uri.getScheme())) { 30 | return uri.getPath(); 31 | } 32 | return null; 33 | } 34 | 35 | /** 36 | * 适配api19及以上,根据uri获取图片的绝对路径 37 | * 38 | * @param context 上下文对象 39 | * @param uri 图片的Uri 40 | * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null 41 | */ 42 | @SuppressLint("NewApi") 43 | private static String getRealPathFromUriAboveApi19(Context context, Uri uri) { 44 | String filePath = null; 45 | if (DocumentsContract.isDocumentUri(context, uri)) { 46 | // 如果是document类型的 uri, 则通过document id来进行处理 47 | String documentId = DocumentsContract.getDocumentId(uri); 48 | if (isMediaDocument(uri)) { // MediaProvider 49 | // 使用':'分割 50 | String type = documentId.split(":")[0]; 51 | String id = documentId.split(":")[1]; 52 | 53 | String selection = MediaStore.Images.Media._ID + "=?"; 54 | String[] selectionArgs = {id}; 55 | 56 | // 57 | Uri contentUri = null; 58 | if ("image".equals(type)) { 59 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 60 | } else if ("video".equals(type)) { 61 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 62 | } else if ("audio".equals(type)) { 63 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 64 | } 65 | 66 | filePath = getDataColumn(context, contentUri, selection, selectionArgs); 67 | } else if (isDownloadsDocument(uri)) { // DownloadsProvider 68 | Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(documentId)); 69 | filePath = getDataColumn(context, contentUri, null, null); 70 | }else if (isExternalStorageDocument(uri)) { 71 | // ExternalStorageProvider 72 | final String docId = DocumentsContract.getDocumentId(uri); 73 | final String[] split = docId.split(":"); 74 | final String type = split[0]; 75 | if ("primary".equalsIgnoreCase(type)) { 76 | filePath = Environment.getExternalStorageDirectory() + "/" + split[1]; 77 | } 78 | }else { 79 | //Log.e("路径错误"); 80 | } 81 | } else if ("content".equalsIgnoreCase(uri.getScheme())) { 82 | // 如果是 content 类型的 Uri 83 | filePath = getDataColumn(context, uri, null, null); 84 | } else if ("file".equals(uri.getScheme())) { 85 | // 如果是 file 类型的 Uri,直接获取图片对应的路径 86 | filePath = uri.getPath(); 87 | } 88 | return filePath; 89 | } 90 | 91 | /** 92 | * 适配api19以下(不包括api19),根据uri获取图片的绝对路径 93 | * 94 | * @param context 上下文对象 95 | * @param uri 图片的Uri 96 | * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null 97 | */ 98 | private static String getRealPathFromUriBelowAPI19(Context context, Uri uri) { 99 | return getDataColumn(context, uri, null, null); 100 | } 101 | 102 | /** 103 | * 获取数据库表中的 _data 列,即返回Uri对应的文件路径 104 | * 105 | * @return 106 | */ 107 | private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { 108 | String path = null; 109 | 110 | String[] projection = new String[]{MediaStore.Images.Media.DATA}; 111 | Cursor cursor = null; 112 | try { 113 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); 114 | if (cursor != null && cursor.moveToFirst()) { 115 | int columnIndex = cursor.getColumnIndexOrThrow(projection[0]); 116 | path = cursor.getString(columnIndex); 117 | } 118 | } catch (Exception e) { 119 | if (cursor != null) { 120 | cursor.close(); 121 | } 122 | } 123 | return path; 124 | } 125 | 126 | /** 127 | * @param uri the Uri to check 128 | * @return Whether the Uri authority is MediaProvider 129 | */ 130 | private static boolean isMediaDocument(Uri uri) { 131 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 132 | } 133 | 134 | private static boolean isExternalStorageDocument(Uri uri) { 135 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 136 | } 137 | 138 | /** 139 | * @param uri the Uri to check 140 | * @return Whether the Uri authority is DownloadsProvider 141 | */ 142 | private static boolean isDownloadsDocument(Uri uri) { 143 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 144 | } 145 | } -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.app.Activity; 4 | import android.bluetooth.BluetoothAdapter; 5 | import android.bluetooth.BluetoothManager; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.content.Intent; 9 | import android.content.pm.PackageInfo; 10 | import android.content.pm.PackageManager; 11 | import android.net.Uri; 12 | import android.os.Bundle; 13 | import android.os.Looper; 14 | import android.provider.MediaStore; 15 | import android.text.SpannableString; 16 | import android.text.TextUtils; 17 | import android.text.method.ScrollingMovementMethod; 18 | import android.util.Log; 19 | import android.view.MenuItem; 20 | import android.view.View; 21 | import android.widget.AdapterView; 22 | import android.widget.Button; 23 | import android.widget.Spinner; 24 | import android.widget.TextView; 25 | import android.widget.Toast; 26 | 27 | import androidx.annotation.NonNull; 28 | import androidx.appcompat.app.AlertDialog; 29 | import androidx.appcompat.app.AppCompatActivity; 30 | import androidx.appcompat.widget.Toolbar; 31 | import androidx.core.app.ActivityCompat; 32 | import androidx.core.view.GravityCompat; 33 | import androidx.drawerlayout.widget.DrawerLayout; 34 | 35 | import com.google.android.material.navigation.NavigationView; 36 | 37 | 38 | public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { 39 | private Toolbar toolbar; 40 | private Button mBtn_Linear; 41 | private DrawerLayout drawer; 42 | private TextView fpvAddress,controladdress,EventLog; 43 | private Spinner fpvModeSpinner,ControlModeSpinner; 44 | private int fpvMode = 0; 45 | private int controlMode = 0; 46 | 47 | //用于标记应用是否刚刚打开,这样可以避免刚打开APP就有烦人的Toast 48 | private boolean firstrun = true; 49 | 50 | //找到侧边抽屉的navigationmenu控件 51 | private NavigationView navigationview; 52 | 53 | //标记是否成功选择目标蓝牙设备 54 | private boolean BluetoothTargetSelected = false; 55 | 56 | //控制对象 57 | private MsgObject msgobject; 58 | 59 | private Bundle bundletofpvactivity; 60 | 61 | //标记是否通过条件检查 62 | private boolean checkpass = true; 63 | 64 | //App设置 65 | private Settings settings; 66 | 67 | @Override 68 | protected void onCreate(Bundle savedInstanceState) { 69 | super.onCreate(savedInstanceState); 70 | setContentView(R.layout.activity_main); 71 | 72 | //找到侧边抽屉控件 73 | drawer = findViewById(R.id.drawer_layout); 74 | //找到fpvAddress,controladdress,EventLog控件 75 | fpvAddress = findViewById(R.id.fpvaddress); 76 | controladdress = findViewById(R.id.controladdress); 77 | EventLog = findViewById(R.id.MainEventLog); 78 | //找到ToolBar控件 79 | toolbar = (Toolbar)findViewById(R.id.ToolBarMain0); 80 | //找到Start按钮 81 | mBtn_Linear = findViewById(R.id.startfpv); 82 | //找到fpvModeSpinner,控件 83 | fpvModeSpinner = findViewById(R.id.fpvModeSpinner); 84 | ControlModeSpinner = findViewById(R.id.ControlModeSpinner); 85 | //找到navigation控件 86 | navigationview = findViewById(R.id.nav_view); 87 | 88 | //初始化App设置 89 | settings = new Settings(getSharedPreferences("Settings",MODE_PRIVATE)); 90 | 91 | //初始化ToolBar 92 | setSupportActionBar(toolbar); 93 | toolbar.setNavigationIcon(R.drawable.ic_baseline_dehaze_24); 94 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 95 | @Override 96 | public void onClick(View view) { 97 | drawer.openDrawer(GravityCompat.START, true); 98 | 99 | } 100 | }); 101 | 102 | //加载默认设置 103 | fpvModeSpinner.setSelection(settings.getFPVMode()); 104 | switch (settings.getFPVMode()){ 105 | case 0:{ 106 | fpvAddress.setText(settings.gethttpAddress()); 107 | break; 108 | } 109 | 110 | case 1:{ 111 | fpvAddress.setText(settings.getUDPAddress()); 112 | break; 113 | } 114 | 115 | case 2:{ 116 | fpvAddress.setText(settings.getPhotoAddress()); 117 | break; 118 | } 119 | 120 | default:{ 121 | break; 122 | } 123 | } 124 | 125 | ControlModeSpinner.setSelection(settings.getControlMode()); 126 | switch (settings.getControlMode()){ 127 | case 0:{ 128 | controladdress.setText(settings.getTCPAddress()); 129 | break; 130 | } 131 | 132 | case 1:{ 133 | controladdress.setText(settings.getBluetoothAddress()); 134 | break; 135 | } 136 | 137 | default:{ 138 | break; 139 | } 140 | } 141 | 142 | //检查更新 143 | if(settings.getCheckUpdate()) { 144 | //获取VersionCode 145 | PackageManager packageManager = getPackageManager(); 146 | PackageInfo packInfo = null; 147 | try { 148 | packInfo = packageManager.getPackageInfo(getPackageName(), 0); 149 | } catch (PackageManager.NameNotFoundException e) { 150 | e.printStackTrace(); 151 | } 152 | CheckUpdate checkUpdate = new CheckUpdate(packInfo.versionCode, new CheckUpdate.OnMainCallBack() { 153 | @Override 154 | public void onMainCallBack(boolean NewVersion, final String UpdateLog) { 155 | if (NewVersion) { 156 | runOnUiThread(new Runnable() { 157 | @Override 158 | public void run() { 159 | showUpdateDialog(UpdateLog); 160 | 161 | } 162 | }); 163 | } 164 | } 165 | }); 166 | } 167 | 168 | 169 | //设置fpv模式修改监听事件 170 | fpvModeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 171 | @Override 172 | public void onItemSelected(AdapterView adapterView, View view, int i, long l) { 173 | switch (i){ 174 | case 0:{ 175 | fpvMode = 0; 176 | fpvAddress.setFocusable(true); 177 | fpvAddress.setFocusableInTouchMode(true); 178 | fpvAddress.setHint(new SpannableString("eg: http://192.168.192.101:80")); 179 | if(!firstrun) { 180 | Toast.makeText(getApplicationContext(), "不输入则默认白色背景", Toast.LENGTH_SHORT).show(); 181 | } 182 | EventLog.append("fpv Service is running on http mode...\r\n"); 183 | break; 184 | } 185 | case 1:{ 186 | fpvMode = 1; 187 | fpvAddress.setHint(new SpannableString("暂不支持,敬请期待")); 188 | fpvAddress.setFocusable(false); 189 | fpvAddress.setFocusableInTouchMode(false); 190 | Toast.makeText(getApplicationContext(), "虽然理论上是更好的解决方案,但是暂时不支持,所以请关注更新哦~", Toast.LENGTH_SHORT).show(); 191 | break; 192 | } 193 | case 2:{ 194 | fpvMode = 2; 195 | fpvAddress.setFocusable(false); 196 | fpvAddress.setFocusableInTouchMode(false); 197 | 198 | //让用户选择图片 199 | choosePhoto(); 200 | 201 | //验证储存权限 202 | verifyStoragePermissions(MainActivity.this); 203 | 204 | EventLog.append("fpv Service is disabled...\r\n"); 205 | break; 206 | } 207 | default:{ 208 | fpvMode = 0; 209 | fpvAddress.setHint(new SpannableString("暂不支持,敬请期待")); 210 | fpvAddress.setFocusable(false); 211 | fpvAddress.setFocusableInTouchMode(false); 212 | Toast.makeText(getApplicationContext(), "暂不支持", Toast.LENGTH_SHORT).show(); 213 | EventLog.append("It's a feature, not a bug.\r\n"); 214 | break; 215 | } 216 | } 217 | } 218 | 219 | @Override 220 | public void onNothingSelected(AdapterView adapterView) { 221 | 222 | } 223 | }); 224 | 225 | ControlModeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 226 | @Override 227 | public void onItemSelected(AdapterView adapterView, View view, int i, long l) { 228 | switch(i){ 229 | case 0:{ 230 | controlMode = 0; 231 | controladdress.setFocusable(true); 232 | controladdress.setFocusableInTouchMode(true); 233 | controladdress.setHint(new SpannableString("eg: 192.168.192.101:8080")); 234 | EventLog.append("Control Sevrice is running on TCP Mode...\r\n"); 235 | break; 236 | } 237 | 238 | case 1:{ 239 | controlMode = 1; 240 | 241 | EventLog.append("Control Sevrice is running on Bluetooth Mode...\r\n"); 242 | //这里该让用户选择目标蓝牙设备了 243 | SelectBluetoothTarget(); 244 | break; 245 | } 246 | 247 | default:{ 248 | controlMode = -1; 249 | controladdress.setFocusable(false); 250 | controladdress.setFocusableInTouchMode(false); 251 | Toast.makeText(getApplicationContext(), "暂不支持", Toast.LENGTH_SHORT).show(); 252 | EventLog.append("It's a feature, not a bug.\r\n"); 253 | break; 254 | } 255 | } 256 | } 257 | 258 | @Override 259 | public void onNothingSelected(AdapterView adapterView) { 260 | 261 | } 262 | }); 263 | 264 | //为侧边栏item设置监听事件 265 | navigationview.setNavigationItemSelectedListener(this); 266 | 267 | //轮流使用EditText焦点 268 | fpvAddress.setOnClickListener(new View.OnClickListener() { 269 | @Override 270 | public void onClick(View view) { 271 | fpvAddress.requestFocus(); 272 | controladdress.clearFocus(); 273 | } 274 | }); 275 | 276 | controladdress.setOnClickListener(new View.OnClickListener() { 277 | @Override 278 | public void onClick(View view) { 279 | fpvAddress.clearFocus(); 280 | controladdress.requestFocus(); 281 | } 282 | }); 283 | 284 | //初始化Start按钮 285 | mBtn_Linear.setOnClickListener(new View.OnClickListener() { 286 | @Override 287 | public void onClick(View view) { 288 | EventLog.setMovementMethod(new ScrollingMovementMethod()); 289 | EventLog.setText(""); 290 | checkpass = true; 291 | switch (ControlModeSpinner.getSelectedItem().toString()) { 292 | case "TCP": { 293 | EventLog.append("Ping TCP Sever...\r\n"); 294 | 295 | //提取IP和端口 296 | String[] temp; 297 | String host = new String(); 298 | int port = 0; 299 | temp = controladdress.getText().toString().split(":"); 300 | if (temp.length == 2) { 301 | host = temp[0]; 302 | port = Integer.valueOf(temp[1]).intValue(); 303 | } else { 304 | Toast.makeText(getApplicationContext(), "请检查TCP地址设置", Toast.LENGTH_SHORT).show(); 305 | EventLog.append("TCP Address error!\r\n"); 306 | checkpass = false; 307 | } 308 | 309 | //测试连接性 310 | final int finalPort = port; 311 | final String finalHost = host; 312 | new CheckHost(EventLog, host, new CheckHost.OnMainCallBack() { 313 | @Override 314 | public void onMainCallBack(Boolean checkpass) { 315 | if((checkpass) || (!settings.getCheckConfig())) { 316 | fpvActivity fpv = new fpvActivity(); 317 | 318 | Intent intent = new Intent(MainActivity.this, fpv.getClass()); 319 | 320 | intent.putExtra("fpvMode", getfpvmode()); 321 | intent.putExtra("Address", fpvAddress.getText().toString()); 322 | 323 | intent.putExtra("ControlMode", "TCP"); 324 | intent.putExtra("Host", finalHost); 325 | intent.putExtra("Port", String.valueOf(finalPort)); 326 | startActivity(intent); 327 | } else { 328 | EventLog.append("Config Check error!"); 329 | } 330 | } 331 | }).start(); 332 | break; 333 | } 334 | 335 | case "蓝牙串口": { 336 | 337 | if(BluetoothTargetSelected) { 338 | //跳转到控制界面 339 | if (BluetoothTargetSelected && (controladdress.getText() != "")) { 340 | fpvActivity fpv = new fpvActivity(); 341 | 342 | Intent intent = new Intent(MainActivity.this, fpv.getClass()); 343 | 344 | intent.putExtra("fpvMode", getfpvmode()); 345 | intent.putExtra("Address", fpvAddress.getText().toString()); 346 | 347 | intent.putExtra("ControlMode", "Bluetooth"); 348 | intent.putExtra("Mac", controladdress.getText().toString()); 349 | startActivity(intent); 350 | } 351 | } 352 | break; 353 | } 354 | default: { 355 | Toast.makeText(getApplicationContext(), "暂不支持。", Toast.LENGTH_SHORT).show(); 356 | break; 357 | } 358 | } 359 | } 360 | }); 361 | 362 | navigationview.setNavigationItemSelectedListener(this); 363 | firstrun = false; 364 | } 365 | 366 | private String getfpvmode(){ 367 | switch (fpvMode){ 368 | case 0:{ 369 | return "http"; 370 | } 371 | 372 | case 1:{ 373 | //暂且先不做UDP 374 | //return "UDP"; 375 | return "http"; 376 | } 377 | 378 | case 2:{ 379 | return "Photo"; 380 | } 381 | 382 | default:{ 383 | return "Unknow"; 384 | } 385 | } 386 | } 387 | 388 | @Override 389 | public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { 390 | switch (menuItem.getItemId()){ 391 | case R.id.QuickStart:{ 392 | Intent intent = new Intent(); 393 | //Intent intent = new Intent(Intent.ACTION_VIEW,uri); 394 | intent.setAction("android.intent.action.VIEW"); 395 | Uri content_url = Uri.parse("http://www.h13studio.com/fpv%E5%9B%BE%E4%BC%A0%E9%81%A5%E6%8E%A7%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/"); 396 | intent.setData(content_url); 397 | startActivity(intent); 398 | 399 | break; 400 | } 401 | 402 | case R.id.Score:{ 403 | Uri uri = Uri.parse("market://details?id="+getPackageName()); 404 | Intent intent = new Intent(Intent.ACTION_VIEW,uri); 405 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 406 | startActivity(intent); 407 | break; 408 | } 409 | 410 | case R.id.Aboutme:{ 411 | Intent intent = new Intent(); 412 | intent.setAction("android.intent.action.VIEW"); 413 | Uri content_url = Uri.parse("http://www.h13studio.com/"); 414 | intent.setData(content_url); 415 | startActivity(intent); 416 | break; 417 | } 418 | 419 | case R.id.Update:{ 420 | Intent intent = new Intent(); 421 | intent.setAction("android.intent.action.VIEW"); 422 | Uri content_url = Uri.parse("https://www.coolapk.com/apk/com.h13studio.fpv"); 423 | intent.setData(content_url); 424 | startActivity(intent); 425 | break; 426 | } 427 | 428 | case R.id.Settings:{ 429 | AdvancedSettings settings = new AdvancedSettings(); 430 | Intent intent = new Intent(MainActivity.this, settings.getClass()); 431 | startActivity(intent); 432 | break; 433 | } 434 | 435 | case R.id.SourceCode:{ 436 | Intent intent = new Intent(); 437 | intent.setAction("android.intent.action.VIEW"); 438 | Uri content_url = Uri.parse("https://github.com/h13-0/fpv-Remote-Control"); 439 | intent.setData(content_url); 440 | startActivity(intent); 441 | break; 442 | } 443 | 444 | default:{ 445 | Toast.makeText(getApplicationContext(), "暂不支持。", Toast.LENGTH_SHORT).show(); 446 | break; 447 | } 448 | } 449 | DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); 450 | drawer.closeDrawer(GravityCompat.START); 451 | return false; 452 | } 453 | 454 | //这写的是什么鬼垃圾 待会儿再回来清理 455 | //测试TCP连接性线程 456 | public static class CheckHost extends Thread { 457 | private String[] temp; 458 | private TextView tv; 459 | private String host; 460 | private OnMainCallBack mOnMainCallBack; 461 | private boolean checkpass = true; 462 | 463 | public CheckHost(TextView textview,String Host,OnMainCallBack mainCallBack){ 464 | tv = textview; 465 | host = Host; 466 | this.mOnMainCallBack=mainCallBack; 467 | } 468 | 469 | @Override 470 | public void run() { 471 | super.run(); 472 | Looper.prepare(); 473 | PingTask ping = new PingTask(host, 3); 474 | String Source; 475 | String ttl, time; 476 | for (int t = 0; t < 5; t++) { 477 | Source = ping.Ping(); 478 | if (Source.contains("time=")) { 479 | temp = Source.split("ttl="); 480 | temp = temp[1].split(" "); 481 | ttl = temp[0]; 482 | time = temp[1]; 483 | time = time.replace("time=",""); 484 | tv.append("ping: " + host + ", ttl = " + ttl + ", time = " + time + " ms.\r\n"); 485 | checkpass = true; 486 | } else { 487 | tv.append(Source + "\r\n"); 488 | checkpass = false; 489 | } 490 | } 491 | 492 | mOnMainCallBack.onMainCallBack((Boolean)(checkpass)); 493 | } 494 | 495 | /**主线程回调接口*/ 496 | 497 | public interface OnMainCallBack{ 498 | void onMainCallBack(Boolean Checkpass); 499 | } 500 | 501 | } 502 | 503 | //选择蓝牙目标 504 | private boolean SelectBluetoothTarget(){ 505 | BluetoothAdapter bluetoothAdapter; 506 | int REQUEST_ENABLE_BT = 1; 507 | 508 | // 判断手机硬件支持蓝牙 509 | if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { 510 | Toast.makeText(getApplicationContext(), "这台手机不支持蓝牙串口,砸了吧", Toast.LENGTH_SHORT).show(); 511 | checkpass = false; 512 | } 513 | 514 | //获取手机本地的蓝牙适配器 515 | final BluetoothManager bluetoothManager = 516 | (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); 517 | 518 | bluetoothAdapter = bluetoothManager.getAdapter(); 519 | if (checkpass) { 520 | // 打开蓝牙权限 521 | if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { 522 | //弹出对话框,请求打开蓝牙 523 | startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), REQUEST_ENABLE_BT); 524 | } 525 | } 526 | 527 | if (checkpass) { 528 | int timeused = 0; 529 | while (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { 530 | try { 531 | Thread.sleep(1000); 532 | } catch (InterruptedException e) { 533 | e.printStackTrace(); 534 | } 535 | timeused++; 536 | if (timeused > 5) { 537 | Toast.makeText(getApplicationContext(), "获取蓝牙权限超时,请重新获取", Toast.LENGTH_SHORT).show(); 538 | checkpass = false; 539 | break; 540 | } 541 | } 542 | } 543 | 544 | //跳转到选择配对设备界面 545 | if (checkpass) { 546 | BluetoothActivity bluetoothactivity = new BluetoothActivity(); 547 | Intent i = new Intent(MainActivity.this, bluetoothactivity.getClass()); 548 | startActivityForResult(i, 0); 549 | } 550 | 551 | return checkpass; 552 | } 553 | 554 | 555 | 556 | //选择蓝牙目标之后的回调程序 557 | //0->bluetooth 558 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 559 | super.onActivityResult(requestCode, resultCode, data); 560 | switch (requestCode) { 561 | //蓝牙配对界面返回 562 | case 0: { 563 | if(resultCode==RESULT_OK){ 564 | Bundle bundle = data.getExtras(); 565 | String text =null; 566 | if(bundle!=null) 567 | text=bundle.getString("Mac"); 568 | Log.d("result",text); 569 | controladdress.setText(text); 570 | 571 | EventLog.append("Select Bluetooth target at" + text); 572 | BluetoothTargetSelected = true; 573 | } 574 | 575 | break; 576 | } 577 | 578 | case 1: { 579 | if(data != null) { 580 | if(data.getData() != null) { 581 | Uri uri = data.getData(); 582 | String filePath = FileUtil.getFilePathByUri(this, uri); 583 | if (!TextUtils.isEmpty(filePath)) { 584 | fpvAddress.setText("file://" + filePath); 585 | } 586 | break; 587 | } 588 | } 589 | } 590 | } 591 | 592 | } 593 | 594 | //从相册选取图片 595 | private void choosePhoto() { 596 | Intent intentToPickPic = new Intent(Intent.ACTION_PICK, null); 597 | intentToPickPic.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*"); 598 | startActivityForResult(intentToPickPic, 1); 599 | } 600 | 601 | //动态申请读写储存权限 602 | //先定义 603 | private static final int REQUEST_EXTERNAL_STORAGE = 1; 604 | 605 | private static String[] PERMISSIONS_STORAGE = { 606 | "android.permission.READ_EXTERNAL_STORAGE", 607 | "android.permission.WRITE_EXTERNAL_STORAGE" }; 608 | 609 | //然后通过一个函数来申请 610 | public static void verifyStoragePermissions(Activity activity) { 611 | try { 612 | //检测是否有写的权限 613 | int permission = ActivityCompat.checkSelfPermission(activity, 614 | "android.permission.WRITE_EXTERNAL_STORAGE"); 615 | if (permission != PackageManager.PERMISSION_GRANTED) { 616 | // 没有写的权限,去申请写的权限,会弹出对话框 617 | ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE); 618 | } 619 | } catch (Exception e) { 620 | e.printStackTrace(); 621 | } 622 | } 623 | 624 | private void showUpdateDialog(String message){ 625 | final AlertDialog.Builder normalDialog = 626 | new AlertDialog.Builder(MainActivity.this); 627 | normalDialog.setIcon(R.mipmap.ic_launcher_round_gray); 628 | normalDialog.setTitle("检测到新版本"); 629 | normalDialog.setMessage(message); 630 | normalDialog.setPositiveButton("现在更新", 631 | new DialogInterface.OnClickListener() { 632 | @Override 633 | public void onClick(DialogInterface dialog, int which) { 634 | Intent intent = new Intent(); 635 | intent.setAction("android.intent.action.VIEW"); 636 | Uri content_url = Uri.parse("https://www.coolapk.com/apk/com.h13studio.fpv"); 637 | intent.setData(content_url); 638 | startActivity(intent); 639 | } 640 | }); 641 | normalDialog.setNegativeButton("以后再说", 642 | new DialogInterface.OnClickListener() { 643 | @Override 644 | public void onClick(DialogInterface dialog, int which) { 645 | 646 | } 647 | }); 648 | // 显示 649 | normalDialog.show(); 650 | } 651 | } 652 | 653 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/MsgObject.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.bluetooth.BluetoothAdapter; 4 | import android.bluetooth.BluetoothDevice; 5 | import android.content.Intent; 6 | import android.util.Log; 7 | import android.widget.TextView; 8 | import android.widget.Toast; 9 | 10 | import java.io.IOException; 11 | import java.io.Serializable; 12 | import java.net.Socket; 13 | import java.security.Policy; 14 | import java.util.Set; 15 | 16 | public class MsgObject implements Serializable { 17 | //private static final long serialVersionUID = 1L; //一会就说这个是做什么的 18 | //0--TCP 1--Bluetooth 19 | private int ControlMode; 20 | private TextView msgview; 21 | 22 | //TCP Parameters 23 | private transient TCPClient tcpclient; 24 | private String host; 25 | private int port; 26 | 27 | //Bluetooth Parameters 28 | private BluetoothClient bluetoothclient; 29 | private String mac; 30 | 31 | public MsgObject(String Host, int Port, TextView MsgView){ 32 | host = Host; 33 | port = Port; 34 | ControlMode = 0; 35 | msgview = MsgView; 36 | tcpclient = new TCPClient(host, port, new TCPClient.OnMainCallBack() { 37 | @Override 38 | public void onMainCallBack(String data) { 39 | msgview.append(data); 40 | } 41 | }); 42 | } 43 | 44 | public MsgObject(String Mac,TextView MsgView) { 45 | mac = Mac; 46 | ControlMode = 1; 47 | msgview = MsgView; 48 | bluetoothclient = new BluetoothClient(mac, new BluetoothClient.OnMainCallBack() { 49 | @Override 50 | public void onMainCallBack(String data) { 51 | msgview.append(data); 52 | } 53 | }); 54 | } 55 | 56 | public void SendMsg(String string){ 57 | if(ControlMode == 0){ 58 | tcpclient.SendMSG(string); 59 | }else { 60 | bluetoothclient.SenMsg(string); 61 | } 62 | } 63 | 64 | public void SendMsg(byte[] data){ 65 | if(ControlMode == 0){ 66 | tcpclient.Sendbyte(data); 67 | }else { 68 | bluetoothclient.SenMsg(data); 69 | } 70 | } 71 | 72 | public void stop(){ 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/PingTask.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.os.Looper; 4 | import android.util.Log; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.InputStreamReader; 10 | import java.io.Serializable; 11 | 12 | // 创建ping任务 13 | public class PingTask extends Thread { 14 | String host; 15 | int timelimit; 16 | private OnMainCallBack mOnMainCallBack; 17 | 18 | public PingTask(String Host,int TimeLimit){ 19 | host = Host; 20 | timelimit = TimeLimit; 21 | } 22 | 23 | public PingTask(String Host, int TimeLimit, OnMainCallBack mainCallBack){ 24 | host = Host; 25 | timelimit = TimeLimit; 26 | this.mOnMainCallBack = mainCallBack; 27 | } 28 | 29 | public String Ping(){ 30 | StringBuffer buffer = new StringBuffer(); 31 | 32 | try { 33 | Process p = null; 34 | p = Runtime.getRuntime().exec("ping -c 1 -w " + timelimit + " " + host); 35 | InputStream input = p.getInputStream(); 36 | BufferedReader in = new BufferedReader(new InputStreamReader(input)); 37 | buffer = new StringBuffer(); 38 | String line = ""; 39 | while ((line = in.readLine()) != null) { 40 | buffer.append(line); 41 | } 42 | Log.i("Ping", buffer.toString()); 43 | 44 | 45 | } catch (IOException e) { 46 | e.printStackTrace(); 47 | } 48 | return buffer.toString(); 49 | } 50 | 51 | public void StartPingTask(final int delay){ 52 | new Thread() { 53 | @Override 54 | public void run() { 55 | super.run(); 56 | Looper.prepare(); 57 | Process p = null; 58 | String Source = ""; 59 | 60 | while (true) { 61 | try { 62 | p = Runtime.getRuntime().exec("ping -c 1 -w " + timelimit + " " + host); 63 | InputStream input = p.getInputStream(); 64 | BufferedReader in = new BufferedReader(new InputStreamReader(input)); 65 | String line = ""; 66 | if(in.readLine() != null) 67 | { 68 | Source = in.readLine(); 69 | Log.i("Ping", Source); 70 | } 71 | } catch (IOException e) { 72 | e.printStackTrace(); 73 | Log.d("error",e.toString()); 74 | } 75 | 76 | String[] temp; 77 | String time; 78 | 79 | if (Source.contains("time=")) { 80 | temp = Source.split("ttl="); 81 | temp = temp[1].split(" "); 82 | time = temp[1]; 83 | time = time.replace("time=",""); 84 | Source = ("Ping = " + time + "ms"); 85 | } else { 86 | Source = "TCP ping error!"; 87 | } 88 | 89 | mOnMainCallBack.onMainCallBack(Source); 90 | 91 | try { 92 | Thread.sleep(delay); 93 | } catch (InterruptedException e) { 94 | e.printStackTrace(); 95 | } 96 | } 97 | } 98 | }.start(); 99 | } 100 | 101 | /**主线程回调接口*/ 102 | public interface OnMainCallBack{ 103 | void onMainCallBack(String data); 104 | } 105 | 106 | public void StopPingTask() 107 | { 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/Settings.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.app.Activity; 4 | import android.content.SharedPreferences; 5 | import android.util.Log; 6 | 7 | import static android.content.Context.MODE_PRIVATE; 8 | 9 | public class Settings { 10 | private SharedPreferences datapreference; 11 | private SharedPreferences.Editor editor; 12 | 13 | public final int http = 0; 14 | public final int UDP = 1; 15 | public final int Photo = 2; 16 | 17 | public Settings(SharedPreferences Datapreference) { 18 | datapreference = Datapreference; 19 | editor = datapreference.edit(); 20 | } 21 | 22 | 23 | /** 24 | * @brief: 图传界面左侧控件类型 25 | * @return: True -> 滑杆 False -> 摇杆 26 | */ 27 | public Boolean getModeLeft() { 28 | return datapreference.getBoolean("ModeLeft", new Boolean(false)); 29 | } 30 | 31 | public void setModeLeft(boolean data) { 32 | editor.putBoolean("ModeLeft", data).commit(); 33 | } 34 | 35 | 36 | /** 37 | * @brief: 图传界面右侧控件类型 38 | * @return: True -> 滑杆 False -> 摇杆 39 | */ 40 | public Boolean getModeRight() { 41 | return datapreference.getBoolean("ModeRight", new Boolean(true)); 42 | } 43 | 44 | public void setModeRight(boolean data) { 45 | editor.putBoolean("ModeRight", data).commit(); 46 | } 47 | 48 | 49 | /** 50 | * @brief: 在正式遥控前是否强制检测配置项 51 | * @return: True or False 52 | */ 53 | public Boolean getCheckConfig(){ 54 | return datapreference.getBoolean("CheckConfig", new Boolean(true)); 55 | } 56 | 57 | public void setCheckConfig(boolean data){ 58 | editor.putBoolean("CheckConfig",data).commit(); 59 | } 60 | 61 | /** 62 | * @brief: 有更新时提示 63 | * @return: True or False 64 | */ 65 | public Boolean getCheckUpdate(){ 66 | return datapreference.getBoolean("CheckUpdate", new Boolean(true)); 67 | } 68 | 69 | public void setCheckUpdate(boolean data){ 70 | editor.putBoolean("CheckUpdate",data).commit(); 71 | } 72 | 73 | /** 74 | * @brief: 遥控模式 75 | * @return: 76 | * 0 -> TCP 77 | * 1 -> Bluetooth 78 | */ 79 | public int getControlMode(){ 80 | return datapreference.getInt("ControlMode", 0); 81 | } 82 | 83 | public void setConrtolMode(int Mode){ 84 | editor.putInt("ControlMode", Mode).commit(); 85 | } 86 | 87 | 88 | /** 89 | * @brief: 图传模式 90 | * @return: 91 | * 0 -> http 92 | * 1 -> UDP 93 | * 2 -> Photo 94 | */ 95 | public int getFPVMode(){ 96 | return datapreference.getInt("FPVMode", 0); 97 | } 98 | 99 | public void setFPVMode(int Mode){ 100 | editor.putInt("FPVMode",Mode).commit(); 101 | } 102 | 103 | 104 | /** 105 | * @brief: http Address 106 | * @return: URL 107 | */ 108 | public String gethttpAddress(){ 109 | return datapreference.getString("httpAddress",""); 110 | } 111 | 112 | public void sethttpAddress(String address){ 113 | editor.putString("httpAddress",address).commit(); 114 | } 115 | 116 | 117 | /** 118 | * @brief: UDP Address 119 | * @return: URL 120 | */ 121 | public String getUDPAddress(){ 122 | return datapreference.getString("UDPAddress",""); 123 | } 124 | 125 | public void setUDPAddress(String address){ 126 | editor.putString("UDPAddress",address).commit(); 127 | } 128 | 129 | 130 | /** 131 | * @brief: Photo Address 132 | * @return: Path 133 | */ 134 | public String getPhotoAddress(){ 135 | return datapreference.getString("PhotoAddress",""); 136 | } 137 | 138 | public void setPhotoAddress(String address){ 139 | editor.putString("PhotoAddress",address).commit(); 140 | } 141 | 142 | 143 | /** 144 | * @brief: TCP Address 145 | * @return: Address 146 | */ 147 | public String getTCPAddress(){ 148 | return datapreference.getString("TCPAddress",""); 149 | } 150 | 151 | public void setTCPAddress(String address){ 152 | editor.putString("TCPAddress",address).commit(); 153 | } 154 | 155 | 156 | /** 157 | * @brief: Bluetooth Address 158 | * @return: MAC Address 159 | */ 160 | public String getBluetoothAddress(){ 161 | return datapreference.getString("BluetoothAddress",""); 162 | } 163 | 164 | public void setBluetoothAddress(String address){ 165 | editor.putString("BluetoothAddress",address).commit(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/SwitchOnCheckedChanged.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.widget.CompoundButton; 4 | 5 | public class SwitchOnCheckedChanged implements CompoundButton.OnCheckedChangeListener{ 6 | private AdvancedSettingsAdapter.SwitchHolder holder; 7 | private Settings settings; 8 | 9 | public SwitchOnCheckedChanged(final AdvancedSettingsAdapter.SwitchHolder holder,Settings settings){ 10 | this.holder = holder; 11 | this.settings = settings; 12 | } 13 | 14 | @Override 15 | public void onCheckedChanged(CompoundButton compoundButton, boolean b) { 16 | switch (compoundButton.getId()){ 17 | case R.id.CheckConfig:{ 18 | settings.setCheckConfig(b); 19 | break; 20 | } 21 | 22 | case R.id.CheckUpdate:{ 23 | settings.setCheckUpdate(b); 24 | break; 25 | } 26 | 27 | default:{ 28 | break; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/TCPClient.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | import android.os.Looper; 3 | import android.util.Log; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStreamReader; 8 | import java.io.Serializable; 9 | import java.lang.reflect.Array; 10 | import java.net.Socket; 11 | public class TCPClient extends Thread { 12 | private String host; 13 | private int port; 14 | private boolean MSGOccuping; 15 | private Socket socket; 16 | private String MSG = new String(); 17 | private byte[] data; 18 | private boolean dataOccuping; 19 | private OnMainCallBack mOnMainCallBack; 20 | 21 | public TCPClient(String Host,int Port,OnMainCallBack mainCallBack) { 22 | host = Host; 23 | port = Port; 24 | this.mOnMainCallBack = mainCallBack; 25 | 26 | new Thread() { 27 | @Override 28 | public void run() { 29 | while (true) { 30 | try { 31 | SendtoSever(); 32 | Log.i("TCP", "OK"); 33 | } catch (IOException e) { 34 | try { 35 | if (socket != null) 36 | socket.close(); 37 | } catch (IOException ex) { 38 | ex.printStackTrace(); 39 | } 40 | Log.i("TCP", e.toString()); 41 | } 42 | try { 43 | Thread.sleep(300); 44 | } catch (InterruptedException e) { 45 | e.printStackTrace(); 46 | } 47 | } 48 | } 49 | }.start(); 50 | 51 | new Thread() { 52 | @Override 53 | public void run() { 54 | super.run(); 55 | Looper.prepare(); 56 | while (true) { 57 | if(socket != null) { 58 | try { 59 | BufferedReader msg = new BufferedReader(new InputStreamReader(socket.getInputStream())); 60 | if(msg != null){ 61 | String data = ""; 62 | try{ 63 | data = msg.readLine(); 64 | }catch (IOException e){ 65 | 66 | } 67 | if ((data != null) && (data.length() != 0)){ 68 | mOnMainCallBack.onMainCallBack(msg.readLine() + "\r\n"); 69 | } 70 | } 71 | } catch (IOException e) { 72 | 73 | } 74 | }else { 75 | try { 76 | Thread.sleep(300); 77 | } catch (InterruptedException e) { 78 | e.printStackTrace(); 79 | } 80 | } 81 | } 82 | } 83 | }.start(); 84 | 85 | } 86 | 87 | 88 | 89 | private void SendtoSever() throws IOException { 90 | socket = new Socket(host, port); 91 | while (true) { 92 | if (MSG != "") { 93 | MSGOccuping = true; 94 | socket.getOutputStream().write(MSG.getBytes()); 95 | socket.getOutputStream().flush(); 96 | MSG = ""; 97 | MSGOccuping = false; 98 | } 99 | 100 | if(data != null){ 101 | dataOccuping = true; 102 | socket.getOutputStream().write(data); 103 | socket.getOutputStream().flush(); 104 | data = null; 105 | dataOccuping = false; 106 | } 107 | } 108 | } 109 | 110 | public void SendMSG(String string){ 111 | if(!MSGOccuping) 112 | MSG = string; 113 | } 114 | 115 | public void Sendbyte(byte[] Data){ 116 | if(!dataOccuping) 117 | data = Data; 118 | } 119 | 120 | /**主线程回调接口*/ 121 | public interface OnMainCallBack{ 122 | void onMainCallBack(String data); 123 | } 124 | } -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/UnpairedBluetoothRecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.LinearLayout; 8 | import android.widget.TextView; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.recyclerview.widget.RecyclerView; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | public class UnpairedBluetoothRecyclerAdapter extends RecyclerView.Adapter { 17 | private List mac; 18 | private List name; 19 | private UnpairedBluetoothRecyclerAdapter.OnClick onclick; 20 | 21 | @NonNull 22 | @Override 23 | public UnpairedBluetoothRecyclerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, final int viewType) { 24 | @SuppressLint("ResourceType") View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.unpaired_bluetooth_item_layout,parent,false); 25 | return new ViewHolder(view); 26 | } 27 | 28 | @Override 29 | public void onBindViewHolder(@NonNull UnpairedBluetoothRecyclerAdapter.ViewHolder holder, final int position) { 30 | if(this.getItemCount() != 0) { 31 | holder.bluetoothmac.setText(mac.get(position)); 32 | holder.bluetoothname.setText(name.get(position)); 33 | holder.linearlayout.setOnClickListener(onclick); 34 | holder.linearlayout.setTag(mac.get(position)); 35 | } 36 | } 37 | 38 | @Override 39 | public int getItemCount() { 40 | return name.size(); 41 | } 42 | 43 | class ViewHolder extends RecyclerView.ViewHolder { 44 | LinearLayout linearlayout; 45 | TextView bluetoothname,bluetoothmac; 46 | 47 | @SuppressLint("ResourceType") 48 | public ViewHolder(@NonNull View itemView) { 49 | super(itemView); 50 | 51 | linearlayout = (LinearLayout) itemView.findViewById(R.id.unpairedbluetoothitem); 52 | bluetoothname = (TextView) itemView.findViewById(R.id.unpairedbluetoothname); 53 | bluetoothmac = (TextView) itemView.findViewById(R.id.unpairedbluetoothmac); 54 | } 55 | } 56 | 57 | public void AddItem(String Name,String Mac){ 58 | name.add(Name); 59 | mac.add(Mac); 60 | } 61 | 62 | public UnpairedBluetoothRecyclerAdapter(){ 63 | mac = new ArrayList(); 64 | name = new ArrayList(); 65 | } 66 | 67 | public static class OnClick implements View.OnClickListener{ 68 | @Override 69 | public void onClick(View view) { 70 | //Log.i("ClickID",""); 71 | } 72 | } 73 | 74 | public void setOnclick(UnpairedBluetoothRecyclerAdapter.OnClick onClick){ 75 | onclick = onClick; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/h13studio/fpv/fpvActivity.java: -------------------------------------------------------------------------------- 1 | package com.h13studio.fpv; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | import androidx.core.app.ActivityCompat; 5 | 6 | import android.annotation.SuppressLint; 7 | import android.app.Activity; 8 | import android.content.Intent; 9 | import android.content.pm.PackageManager; 10 | import android.graphics.Color; 11 | import android.os.Build; 12 | import android.os.Bundle; 13 | import android.util.Log; 14 | import android.view.KeyEvent; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.view.Window; 19 | import android.view.WindowManager; 20 | import android.webkit.CookieManager; 21 | import android.webkit.CookieSyncManager; 22 | import android.webkit.WebSettings; 23 | import android.webkit.WebView; 24 | import android.webkit.WebViewClient; 25 | import android.widget.Button; 26 | import android.widget.ImageView; 27 | import android.widget.SeekBar; 28 | import android.widget.TextView; 29 | import android.widget.Toast; 30 | 31 | import com.kongqw.rockerlibrary.view.RockerView; 32 | 33 | import java.util.HashMap; 34 | import java.util.Map; 35 | 36 | /** 37 | * An example full-screen activity that shows and hides the system UI (i.e. 38 | * status bar and navigation/system bar) with user interaction. 39 | */ 40 | public class fpvActivity extends AppCompatActivity { 41 | /** 42 | * Whether or not the system UI should be auto-hidden after 43 | * {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds. 44 | */ 45 | private static final boolean AUTO_HIDE = true; 46 | 47 | /** 48 | * If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after 49 | * user interaction before hiding the system UI. 50 | */ 51 | private static final int AUTO_HIDE_DELAY_MILLIS = 3000; 52 | 53 | /** 54 | * Some older devices needs a small delay between UI widget updates 55 | * and a change of the status and navigation bar. 56 | */ 57 | private static final int UI_ANIMATION_DELAY = 300; 58 | private WebView mWebView0; 59 | private String fpvMode; 60 | private String Address; 61 | private String host; 62 | private int port; 63 | private String ControlMode; 64 | private String mac; 65 | private com.gcssloop.widget.RockerView rockerViewl,rockerViewr; 66 | private SeekBar seekbarl,seekbarr; 67 | private MsgObject msgobject; 68 | private Button ControlBtn1,ControlBtn2,ControlBtn3,ControlBtn4,ControlBtn5,ControlBtn6,ControlBtn7,ControlBtn8; 69 | private Button LockButton,RefreshButton; 70 | private TextView StatusView,MsgView; 71 | private TextView valuel,valuer; 72 | 73 | private Settings settings; 74 | 75 | //默认允许触摸和缩放WebView 76 | private boolean EnableWebviewTouchEvent = true; 77 | 78 | private enum ControlerTypy 79 | { 80 | RockerView, 81 | Seekbar, 82 | Button; 83 | } 84 | 85 | /** 86 | * Touch listener to use for in-layout UI controls to delay hiding the 87 | * system UI. This is to prevent the jarring behavior of controls going away 88 | * while interacting with activity UI. 89 | */ 90 | 91 | @SuppressLint("ResourceAsColor") 92 | @Override 93 | protected void onCreate(final Bundle savedInstanceState) { 94 | super.onCreate(savedInstanceState); 95 | 96 | //加载UI 97 | setContentView(R.layout.fpv_web); 98 | 99 | //隐藏状态栏 100 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 101 | 102 | //隐藏导航栏 103 | getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 104 | 105 | Window window = getWindow(); 106 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); 107 | window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 108 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); 109 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 110 | window.setNavigationBarColor(Color.TRANSPARENT); 111 | 112 | //找到PingView,MsgView; 113 | StatusView = findViewById(R.id.StatusView); 114 | MsgView = findViewById(R.id.MsgView); 115 | 116 | //获取fpvAddress参数 117 | Intent intent = getIntent(); 118 | fpvMode = intent.getStringExtra("fpvMode"); 119 | Address = intent.getStringExtra("Address"); 120 | ControlMode = intent.getStringExtra("ControlMode"); 121 | 122 | //加载手动刷新按钮 123 | RefreshButton = findViewById(R.id.refreshbutton); 124 | 125 | //加载LockWebView按钮 126 | LockButton = findViewById(R.id.lockbutton); 127 | 128 | switch (fpvMode){ 129 | case "http":{ 130 | //注册监听事件 131 | RefreshButton.setOnClickListener(new View.OnClickListener() { 132 | @Override 133 | public void onClick(View view) { 134 | mWebView0.loadUrl(Address); 135 | Toast.makeText(getApplicationContext(), "正在刷新http图传...", Toast.LENGTH_SHORT).show(); 136 | } 137 | }); 138 | 139 | LockButton.setOnClickListener(new View.OnClickListener() { 140 | @Override 141 | public void onClick(View view) { 142 | EnableWebviewTouchEvent = !EnableWebviewTouchEvent; 143 | if(EnableWebviewTouchEvent){ 144 | LockButton.setBackgroundResource(R.drawable.ic_baseline_lock_open_24); 145 | }else { 146 | LockButton.setBackgroundResource(R.drawable.ic_baseline_lock_24); 147 | } 148 | } 149 | }); 150 | break; 151 | } 152 | 153 | case "Photo":{ 154 | //验证储存权限 155 | verifyStoragePermissions(this); 156 | 157 | LockButton.setOnClickListener(new View.OnClickListener() { 158 | @Override 159 | public void onClick(View view) { 160 | EnableWebviewTouchEvent = !EnableWebviewTouchEvent; 161 | if(EnableWebviewTouchEvent){ 162 | LockButton.setBackgroundResource(R.drawable.ic_baseline_lock_open_24); 163 | }else { 164 | LockButton.setBackgroundResource(R.drawable.ic_baseline_lock_24); 165 | } 166 | } 167 | }); 168 | break; 169 | } 170 | 171 | default:{ 172 | //隐藏LockButton,RefreshButton 173 | RefreshButton.setVisibility(View.INVISIBLE); 174 | LockButton.setVisibility(View.INVISIBLE); 175 | break; 176 | } 177 | } 178 | 179 | switch(ControlMode) { 180 | case "TCP": { 181 | host = intent.getStringExtra("Host"); 182 | port = Integer.valueOf(intent.getStringExtra("Port")).intValue(); 183 | msgobject = new MsgObject(host,port,MsgView); 184 | break; 185 | } 186 | case "Bluetooth": { 187 | mac = intent.getStringExtra("Mac"); 188 | Log.i("Mac",mac); 189 | msgobject = new MsgObject(mac,MsgView); 190 | break; 191 | } 192 | 193 | default:{ 194 | Toast.makeText(getApplicationContext(), "什么鬼", Toast.LENGTH_SHORT).show(); 195 | 196 | //隐藏LockButton,RefreshButton 197 | RefreshButton.setVisibility(View.INVISIBLE); 198 | LockButton.setVisibility(View.INVISIBLE); 199 | 200 | finish(); 201 | } 202 | } 203 | 204 | //找到WebView 205 | mWebView0 = findViewById(R.id.fullweb0); 206 | 207 | //初始化WebView 208 | webviewinit(mWebView0); 209 | 210 | //禁用或启用WebView触摸事件 211 | mWebView0.setOnTouchListener(new View.OnTouchListener() { 212 | @Override 213 | public boolean onTouch(View view, MotionEvent motionEvent) { 214 | return !EnableWebviewTouchEvent; 215 | } 216 | }); 217 | 218 | //加载http页面 219 | mWebView0.loadUrl(Address); 220 | 221 | mWebView0.setWebViewClient(new WebViewClient(){ 222 | @Override 223 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 224 | view.loadUrl(url); 225 | view.loadUrl(url); 226 | return true; 227 | } 228 | }); 229 | 230 | //初始化设置对象 231 | settings = new Settings(getSharedPreferences("Settings",MODE_PRIVATE)); 232 | 233 | //找到一堆按钮 234 | ControlBtn1 = findViewById(R.id.control_btn_1); 235 | ControlBtn2 = findViewById(R.id.control_btn_2); 236 | ControlBtn3 = findViewById(R.id.control_btn_3); 237 | ControlBtn4 = findViewById(R.id.control_btn_4); 238 | ControlBtn5 = findViewById(R.id.control_btn_5); 239 | ControlBtn6 = findViewById(R.id.control_btn_6); 240 | ControlBtn7 = findViewById(R.id.control_btn_7); 241 | ControlBtn8 = findViewById(R.id.control_btn_8); 242 | 243 | //注册监听事件 244 | ControlBtn1.setOnClickListener(new OnClick()); 245 | ControlBtn2.setOnClickListener(new OnClick()); 246 | ControlBtn3.setOnClickListener(new OnClick()); 247 | ControlBtn4.setOnClickListener(new OnClick()); 248 | ControlBtn5.setOnClickListener(new OnClick()); 249 | ControlBtn6.setOnClickListener(new OnClick()); 250 | ControlBtn7.setOnClickListener(new OnClick()); 251 | ControlBtn8.setOnClickListener(new OnClick()); 252 | 253 | //启动Ping任务 254 | switch (ControlMode){ 255 | case "TCP":{ 256 | new PingTask(host, 3000, new PingTask.OnMainCallBack() { 257 | @Override 258 | public void onMainCallBack(final String data) { 259 | runOnUiThread(new Runnable(){ 260 | @Override 261 | public void run() { 262 | StatusView.setText(data); 263 | } 264 | }); 265 | } 266 | }).StartPingTask(3000); 267 | break; 268 | } 269 | 270 | default:{ 271 | StatusView.setText(""); 272 | break; 273 | } 274 | } 275 | 276 | //找到左右两个控件的value反馈 277 | valuel = findViewById(R.id.valuel); 278 | valuer = findViewById(R.id.valuer); 279 | 280 | //找到RockerView 281 | rockerViewl = findViewById(R.id.rockerViewl); 282 | rockerViewr = findViewById(R.id.rockerViewr); 283 | 284 | //找到Seekbar 285 | seekbarl = findViewById(R.id.SeekBarl); 286 | seekbarr = findViewById(R.id.SeekBarr); 287 | 288 | if(settings.getModeLeft()){ 289 | //则左边为滑杆 290 | rockerViewl.setVisibility(View.INVISIBLE); 291 | seekbarl.setVisibility(View.VISIBLE); 292 | 293 | //注册滑杆事件 294 | seekbarl.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 295 | @Override 296 | public void onProgressChanged(SeekBar seekBar, int i, boolean b) { 297 | if(b) { 298 | valuel.setText("value: " + String.valueOf(i)); 299 | msgobject.SendMsg(ToBinaryData(ControlerTypy.Seekbar, 0, i)); 300 | } 301 | } 302 | 303 | @Override 304 | public void onStartTrackingTouch(SeekBar seekBar) { 305 | valuel.setVisibility(View.VISIBLE); 306 | } 307 | 308 | @Override 309 | public void onStopTrackingTouch(SeekBar seekBar) { 310 | valuel.setVisibility(View.INVISIBLE); 311 | } 312 | }); 313 | }else { 314 | //则左边为摇杆 315 | 316 | rockerViewl.setVisibility(View.VISIBLE); 317 | seekbarl.setVisibility(View.INVISIBLE); 318 | valuel.setVisibility(View.VISIBLE); 319 | 320 | rockerViewl.setListener(new com.gcssloop.widget.RockerView.RockerListener() { 321 | @Override 322 | public void callback(int i, int i1, float v) { 323 | if(i == com.gcssloop.widget.RockerView.EVENT_ACTION) { 324 | //rockerViewr.setVisibility(View.INVISIBLE); 325 | if (i1 == -1) { 326 | valuel.setText("-1 0"); 327 | msgobject.SendMsg(ToBinaryData(ControlerTypy.RockerView, 0, 0)); 328 | return; 329 | } 330 | 331 | int angel = 0; 332 | if (i1 >= 90) { 333 | angel = i1 - 90; 334 | } else { 335 | angel = 270 + i1; 336 | } 337 | 338 | if (v <= 190) { 339 | valuel.setText(String.valueOf(angel) + " " + String.valueOf((int) (v))); 340 | msgobject.SendMsg(ToBinaryData(ControlerTypy.RockerView, 0, (int) (angel + 65536 * v))); 341 | } else { 342 | valuel.setText(String.valueOf(angel) + " " + "190"); 343 | msgobject.SendMsg(ToBinaryData(ControlerTypy.RockerView, 0, (int) (angel + 65536 * 190))); 344 | } 345 | } 346 | } 347 | }); 348 | } 349 | 350 | if(settings.getModeRight()) { 351 | //则右边为滑杆 352 | 353 | //这还有个BUG,不能直接把rockerViewr设置为INVISABLE,否则左边的摇杆绘制不出来。 354 | rockerViewr.setActivated(false); 355 | rockerViewr.setRockerRadius(0); 356 | rockerViewr.setAreaRadius(0); 357 | seekbarr.setVisibility(View.VISIBLE); 358 | 359 | //注册滑杆事件 360 | seekbarr.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 361 | @Override 362 | public void onProgressChanged(SeekBar seekBar, int i, boolean b) { 363 | if(b) { 364 | valuer.setText("value: " + String.valueOf(i)); 365 | msgobject.SendMsg(ToBinaryData(ControlerTypy.Seekbar, 1, i)); 366 | } 367 | } 368 | 369 | @Override 370 | public void onStartTrackingTouch(SeekBar seekBar) { 371 | valuer.setVisibility(View.VISIBLE); 372 | } 373 | 374 | @Override 375 | public void onStopTrackingTouch(SeekBar seekBar) { 376 | valuer.setVisibility(View.INVISIBLE); 377 | } 378 | }); 379 | }else { 380 | //则右边为摇杆 381 | rockerViewr.setVisibility(View.VISIBLE); 382 | seekbarr.setVisibility(View.INVISIBLE); 383 | valuer.setVisibility(View.VISIBLE); 384 | 385 | rockerViewr.setListener(new com.gcssloop.widget.RockerView.RockerListener() { 386 | @Override 387 | public void callback(int i, int i1, float v) { 388 | if(i == com.gcssloop.widget.RockerView.EVENT_ACTION) { 389 | if (i1 == -1) { 390 | valuer.setText("-1 0"); 391 | msgobject.SendMsg(ToBinaryData(ControlerTypy.RockerView, 1, 0)); 392 | return; 393 | } 394 | 395 | int angel = 0; 396 | if (i1 >= 90) { 397 | angel = i1 - 90; 398 | } else { 399 | angel = 270 + i1; 400 | } 401 | 402 | if (v <= 190) { 403 | valuer.setText(angel + " " + (int) (v)); 404 | msgobject.SendMsg(ToBinaryData(ControlerTypy.RockerView, 1, (int) (angel + 65536 * v))); 405 | } else { 406 | valuer.setText(angel + " " + "190"); 407 | msgobject.SendMsg(ToBinaryData(ControlerTypy.RockerView, 1, (int) (angel + 65536 * 190))); 408 | } 409 | } 410 | } 411 | }); 412 | } 413 | //这句也不能删,否则也会影响左边控件绘制... 414 | rockerViewr.setVisibility(View.VISIBLE); 415 | } 416 | 417 | //统一处理按钮的OnClick事件 418 | private class OnClick implements View.OnClickListener{ 419 | @Override 420 | public void onClick(View view) { 421 | switch (view.getId()){ 422 | case R.id.control_btn_1:{ 423 | msgobject.SendMsg(ToBinaryData(ControlerTypy.Button,0,1)); 424 | break; 425 | } 426 | 427 | case R.id.control_btn_2:{ 428 | msgobject.SendMsg(ToBinaryData(ControlerTypy.Button,1,1)); 429 | break; 430 | } 431 | 432 | case R.id.control_btn_3:{ 433 | msgobject.SendMsg(ToBinaryData(ControlerTypy.Button,2,1)); 434 | break; 435 | } 436 | 437 | case R.id.control_btn_4:{ 438 | msgobject.SendMsg(ToBinaryData(ControlerTypy.Button,3,1)); 439 | break; 440 | } 441 | 442 | case R.id.control_btn_5:{ 443 | msgobject.SendMsg(ToBinaryData(ControlerTypy.Button,4,1)); 444 | break; 445 | } 446 | 447 | case R.id.control_btn_6:{ 448 | msgobject.SendMsg(ToBinaryData(ControlerTypy.Button,5,1)); 449 | break; 450 | } 451 | 452 | case R.id.control_btn_7:{ 453 | msgobject.SendMsg(ToBinaryData(ControlerTypy.Button,6,1)); 454 | break; 455 | } 456 | 457 | case R.id.control_btn_8:{ 458 | msgobject.SendMsg(ToBinaryData(ControlerTypy.Button,7,1)); 459 | break; 460 | } 461 | 462 | default:{ 463 | break; 464 | } 465 | } 466 | } 467 | } 468 | 469 | @Override 470 | protected void onPostCreate(Bundle savedInstanceState) { 471 | super.onPostCreate(savedInstanceState); 472 | } 473 | 474 | /***************************************************************/ 475 | //将控件value打包为二进制包 476 | // 因为单片机的蓝牙串口速率极低,故放弃Json打包方式 477 | // 478 | //函数参数: 479 | // ControlerTypy -> 标记是哪种控件 480 | // ID -> 同类控件中的ID 481 | // value -> 控件值 482 | // 483 | //数据包中数据: 484 | //'f' 'p' 'v': 485 | // 均用于标识数据包 486 | // 487 | //'ID': 488 | // 0x0x -> RockerView 489 | // 0x1x -> Slider 490 | // 0x2x -> Button 491 | /***************************************************************/ 492 | private byte[] ToBinaryData(ControlerTypy type,int ID,int value){ 493 | switch (type){ 494 | case RockerView:{ 495 | //'f','ID:0x0x','distance','value','value','p','v' -> distance特性暂不支持 496 | byte[] data = {0x66, 0x00, 0x00, 0x00, 0x00, 0x70, 0x76}; 497 | data[1] = (byte) ID; 498 | //MD,位运算好像有bug 499 | data[2] = (byte) ((value/65535)%256); 500 | data[3] = (byte) ((value/256)%256); 501 | data[4] = (byte) (0x0000ff&value); 502 | return data; 503 | } 504 | 505 | case Seekbar:{ 506 | //'f','ID:0x1x','value','p','v' 507 | byte[] data = {0x66, 0x00, 0x00, 0x70, 0x76}; 508 | data[1] = (byte) (ID + 0x10); 509 | data[2] = (byte) value; 510 | return data; 511 | } 512 | 513 | case Button:{ 514 | //'f','ID:0x2x','value','p','v' 515 | byte[] data = {0x66, 0x00, 0x00, 0x70, 0x76}; 516 | data[1] = (byte) (ID + 0x20); 517 | data[2] = (byte) value; 518 | return data; 519 | } 520 | 521 | default:{ 522 | return null; 523 | } 524 | } 525 | } 526 | 527 | //监听返回键,双击返回才能退出 528 | long exitTime = 0; 529 | @Override 530 | public boolean onKeyDown(int keyCode, KeyEvent event) { 531 | if(keyCode== KeyEvent.KEYCODE_BACK){ 532 | exit(); 533 | return false; 534 | } 535 | return super.onKeyDown(keyCode,event); 536 | } 537 | 538 | public void exit() { 539 | if ((System.currentTimeMillis() - exitTime) > 2000) { 540 | Toast.makeText(getApplicationContext(), "再按一次退出程序", 541 | Toast.LENGTH_SHORT).show(); 542 | exitTime = System.currentTimeMillis(); 543 | } else { 544 | System.exit(0); 545 | finish(); 546 | } 547 | } 548 | 549 | private void webviewinit(WebView webview){ 550 | //设置WebView 551 | WebSettings webSettings = webview.getSettings(); 552 | webSettings.setJavaScriptEnabled(true);//启用JavaScript 553 | webSettings.setSaveFormData(false); //不保存表单 554 | webSettings.setUseWideViewPort(true); 555 | webSettings.setLoadWithOverviewMode(true); 556 | webSettings.setSupportZoom(true); 557 | webSettings.setBuiltInZoomControls(true); 558 | webSettings.setDisplayZoomControls(false); 559 | webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); 560 | webSettings.setAllowFileAccess(true); 561 | webSettings.setJavaScriptCanOpenWindowsAutomatically(true); 562 | webSettings.setLoadsImagesAutomatically(true); 563 | webSettings.setDefaultTextEncodingName("utf-8"); 564 | webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); 565 | //设置浏览器UA为横屏, 缩短Headers, 减少对单片机的要求 566 | webSettings.setUserAgentString("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0"); 567 | } 568 | 569 | //动态申请读写储存权限 570 | //先定义 571 | private static final int REQUEST_EXTERNAL_STORAGE = 1; 572 | 573 | private static String[] PERMISSIONS_STORAGE = { 574 | "android.permission.READ_EXTERNAL_STORAGE", 575 | "android.permission.WRITE_EXTERNAL_STORAGE" }; 576 | 577 | //然后通过一个函数来申请 578 | public static void verifyStoragePermissions(Activity activity) { 579 | try { 580 | //检测是否有写的权限 581 | int permission = ActivityCompat.checkSelfPermission(activity, 582 | "android.permission.WRITE_EXTERNAL_STORAGE"); 583 | if (permission != PackageManager.PERMISSION_GRANTED) { 584 | // 没有写的权限,去申请写的权限,会弹出对话框 585 | ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE); 586 | } 587 | } catch (Exception e) { 588 | e.printStackTrace(); 589 | } 590 | } 591 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/advanced_setting_shape1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/fullsizeicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/drawable/fullsizeicon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/function_button_ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_autorenew_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_book_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_code_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_dehaze_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_favorite_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_lock_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_lock_open_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_person_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_rotate_right_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_settings_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_gray_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/drawable/icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/rocker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/drawable/rocker.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/rockeronfocused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/drawable/rockeronfocused.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/rockerunfocused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/drawable/rockerunfocused.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/start_button_ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/fpv_web.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 19 | 20 | 33 | 34 | 45 | 46 | 55 | 56 | 69 | 70 | 80 | 81 | 90 | 91 | 98 | 99 | 108 | 109 | 114 | 115 | 119 |