├── .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 |
6 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
127 |
128 |
136 |
137 |
144 |
145 |
146 |
147 |
151 |
159 |
167 |
168 |
169 |
170 |
171 |
176 |
177 |
178 |
179 |
180 |
187 |
188 |
192 |
193 |
200 |
201 |
209 |
210 |
218 |
219 |
220 |
221 |
225 |
226 |
234 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_advanced_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
367 |
368 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_bluetooth.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
24 |
25 |
31 |
32 |
38 |
39 |
47 |
48 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
25 |
26 |
37 |
38 |
50 |
51 |
62 |
63 |
74 |
75 |
85 |
86 |
93 |
94 |
95 |
96 |
106 |
107 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/advanced_settings_controller.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
25 |
26 |
32 |
33 |
44 |
45 |
53 |
54 |
55 |
61 |
62 |
74 |
75 |
82 |
83 |
84 |
85 |
86 |
87 |
94 |
95 |
102 |
108 |
114 |
115 |
120 |
121 |
128 |
129 |
130 |
131 |
132 |
139 |
145 |
151 |
152 |
157 |
158 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/advanced_settings_edittextview.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/advanced_settings_modeselect.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
19 |
28 |
29 |
38 |
39 |
46 |
47 |
48 |
49 |
58 |
59 |
68 |
69 |
78 |
79 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/advanced_settings_switch.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
22 |
23 |
31 |
32 |
38 |
39 |
46 |
47 |
52 |
53 |
54 |
55 |
56 |
62 |
63 |
64 |
65 |
73 |
74 |
80 |
81 |
88 |
89 |
94 |
95 |
96 |
97 |
98 |
104 |
105 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/bluetooth_item_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
24 |
25 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_gallery.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/nav_header_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
23 |
24 |
30 |
31 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/unpaired_bluetooth_item_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
24 |
25 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/activity_main_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round_gray.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/array.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - http图传
5 | - UDP图传(暂不支持)
6 | - 固定图片背景(不图传)
7 |
8 |
9 |
10 | - TCP
11 | - 蓝牙串口
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #ff1986ff
4 | #ff1965ff
5 | #03DAC5
6 | #66000000
7 | #00ffffff
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 8dp
6 | 176dp
7 | 16dp
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | fpv
3 | Dummy Button
4 | DUMMY\nCONTENT
5 | 192.168.1.1
6 | 请选择图传方式
7 | 请选择控制方式
8 | activity_main
9 | Open navigation drawer
10 | Close navigation drawer
11 | FPV图传
12 | com.h13studio.fpv
13 | Navigation header
14 | Settings
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
19 |
20 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath "com.android.tools.build:gradle:4.0.1"
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | google()
18 | jcenter()
19 | maven { url 'https://jitpack.io' }
20 | }
21 | }
22 |
23 | task clean(type: Delete) {
24 | delete rootProject.buildDir
25 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h13-0/fpv-Remote-Control/2b1082d89e14f1626a0c0baaa9d3472ffb19b653/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jul 25 01:25:52 CST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "fpv"
--------------------------------------------------------------------------------