mirror of
https://github.com/Cateners/tiny_computer.git
synced 2026-05-21 00:45:49 +08:00
Compare commits
92 Commits
v1.0.14+10
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
553e5862ca | ||
|
|
125791e44c | ||
|
|
587e93ca31 | ||
|
|
b2d45c95ac | ||
|
|
b6b9ac61c7 | ||
|
|
45da44d078 | ||
|
|
77fec49a75 | ||
|
|
8cde9b878a | ||
|
|
b15fe80e83 | ||
|
|
ae88dea9c5 | ||
|
|
6425e0443e | ||
|
|
010cf544ea | ||
|
|
a4a2898214 | ||
|
|
b2b642e7c0 | ||
|
|
23968eb1fc | ||
|
|
2aac0e57d7 | ||
|
|
ed1c4aa9b1 | ||
|
|
f2eb3e0491 | ||
|
|
d17e515981 | ||
|
|
10f481f976 | ||
|
|
c10d2b733f | ||
|
|
417cf7feef | ||
|
|
7b219facfe | ||
|
|
ed3ec63212 | ||
|
|
8f26ed77e7 | ||
|
|
db4431d4c7 | ||
|
|
0793f589f2 | ||
|
|
1f8b83ddb1 | ||
|
|
ceca9a1892 | ||
|
|
4cb8bfb01e | ||
|
|
645e60cf83 | ||
|
|
350f4e93ef | ||
|
|
ee22cbc1d9 | ||
|
|
51c67ac546 | ||
|
|
ba43ec7ad0 | ||
|
|
1203dcf737 | ||
|
|
d9c4c24adc | ||
|
|
3b84c7da2c | ||
|
|
3a6d22956f | ||
|
|
8b5013a479 | ||
|
|
6d924bded9 | ||
|
|
6ff16e3559 | ||
|
|
cd8fea5f98 | ||
|
|
83a544acda | ||
|
|
c80be46909 | ||
|
|
90be3dc9ee | ||
|
|
45d60d6519 | ||
|
|
d8b4390c03 | ||
|
|
0e186e93f7 | ||
|
|
b788cea689 | ||
|
|
affc3173ef | ||
|
|
6b67ddaf9a | ||
|
|
009cd4ebe2 | ||
|
|
7e685fae64 | ||
|
|
91688ec4ae | ||
|
|
ba62910793 | ||
|
|
e1f24796b2 | ||
|
|
23b7889fcc | ||
|
|
db0689a9bd | ||
|
|
9f294af413 | ||
|
|
304df8ca96 | ||
|
|
c0795474cf | ||
|
|
1f596424f2 | ||
|
|
e86fb4e3a9 | ||
|
|
ee230f91dd | ||
|
|
b5cbda42cb | ||
|
|
fd535f0e20 | ||
|
|
a5a381604d | ||
|
|
a911efdc54 | ||
|
|
49c2377e46 | ||
|
|
a117050fde | ||
|
|
958d7839ff | ||
|
|
106b5fc325 | ||
|
|
699f1eef37 | ||
|
|
905ff609b1 | ||
|
|
70c2018ddf | ||
|
|
b1895af653 | ||
|
|
62995e0a5d | ||
|
|
5ebbaf7073 | ||
|
|
cb1f4b23ee | ||
|
|
0d2f5f4e91 | ||
|
|
d54d1ef6f3 | ||
|
|
0377aa7b8f | ||
|
|
232afe9929 | ||
|
|
fcb472594d | ||
|
|
aa6d0feed7 | ||
|
|
1ee935105e | ||
|
|
7242c45e38 | ||
|
|
d0a539d6dc | ||
|
|
3e443ceedc | ||
|
|
b50622787d | ||
|
|
231d1167e0 |
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
name: Bug 报告
|
||||||
|
description: 报告软件异常行为(提交前请阅读所有说明文字)
|
||||||
|
title: "[Bug] "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**必读须知:**
|
||||||
|
- 请先搜索[所有issue](https://github.com/Cateners/tiny_computer/issues?q=is%3Aissue),**相同问题重复提交会被直接关闭**
|
||||||
|
- 请先自查[常见使用问题](https://gitee.com/caten/tc-hints/blob/master/pool/faq.md),是否能解决你的问题
|
||||||
|
- 本模板中的每项要求都有其特定原因,请完整填写
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device
|
||||||
|
attributes:
|
||||||
|
label: "设备型号"
|
||||||
|
description: |
|
||||||
|
**为什么需要这个信息?**
|
||||||
|
许多Bug与设备强相关,例如:
|
||||||
|
- 三星OneUI 7无法使用v1.0.23及以下版本(issue #303)
|
||||||
|
- 鸿蒙4缺少无线调试选项
|
||||||
|
placeholder: "品牌+具体型号"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: os-version
|
||||||
|
attributes:
|
||||||
|
label: "操作系统版本"
|
||||||
|
description: |
|
||||||
|
**为什么需要这个信息?**
|
||||||
|
Android各版本存在兼容性差异
|
||||||
|
placeholder: "完整系统名称+版本号"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: app-version
|
||||||
|
attributes:
|
||||||
|
label: "小小电脑版本"
|
||||||
|
description: |
|
||||||
|
**为什么需要这个信息?**
|
||||||
|
1. 旧版问题可能已在新版修复(请始终使用最新版)
|
||||||
|
2. 不同桌面环境(XFCE/LXQT/GXDE)行为可能不同
|
||||||
|
**如果不是最新版,请做好问题被忽略的准备**
|
||||||
|
placeholder: "版本号+桌面环境(如:v1.0.23 GXDE)"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: is-latest
|
||||||
|
attributes:
|
||||||
|
label: "是否最新版?"
|
||||||
|
options:
|
||||||
|
- "是"
|
||||||
|
- "否"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: why-not-latest
|
||||||
|
attributes:
|
||||||
|
label: "若非最新版,必须说明不使用最新版的原因"
|
||||||
|
placeholder: "详细解释原因..."
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: "重现问题的操作过程"
|
||||||
|
description: |
|
||||||
|
**请注意:**
|
||||||
|
请提供从启动软件开始到问题发生的**完整屏幕录制视频**,而不要使用文字描述或截图,因为根据以往的issue来看,用户的描述会遗漏可能的细节。
|
||||||
|
由于Linux环境的复杂性,任何操作细节(如权限、环境配置等)都可能影响问题的重现。确保你的问题在新安装的小小电脑上可以复现。
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**问题优先级说明:**
|
||||||
|
本软件的初衷是安装PC级软件如WPS、CAJ Viewer、亿图图示。小小电脑本身的问题,以及和这些软件相关的问题会得到重视。如果是其他问题,尤其是第三方软件或Linux本身的使用方法等,请在[discussion](https://github.com/Cateners/tiny_computer/discussions)区发布讨论
|
||||||
81
.github/ISSUE_TEMPLATE/bug_report_eng.yml
vendored
Normal file
81
.github/ISSUE_TEMPLATE/bug_report_eng.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report abnormal software behavior (please read all instructions before submitting)
|
||||||
|
title: "[Bug] "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**Important Notes:**
|
||||||
|
- First search [all issues](https://github.com/Cateners/tiny_computer/issues?q=is%3Aissue). **Duplicate reports will be closed immediately**
|
||||||
|
- Every requirement in this template has its specific purpose. Please complete all fields
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device
|
||||||
|
attributes:
|
||||||
|
label: "Device Model"
|
||||||
|
description: |
|
||||||
|
**Why is this needed?**
|
||||||
|
Many bugs are device-specific, such as:
|
||||||
|
- Samsung OneUI 7 incompatible with v1.0.23 and below (issue #303)
|
||||||
|
- HarmonyOS 4 missing wireless debugging option
|
||||||
|
placeholder: "Brand + specific model"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: os-version
|
||||||
|
attributes:
|
||||||
|
label: "Operating System Version"
|
||||||
|
description: |
|
||||||
|
**Why is this needed?**
|
||||||
|
Compatibility varies across Android versions
|
||||||
|
placeholder: "Full OS name + version number"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: app-version
|
||||||
|
attributes:
|
||||||
|
label: "Tiny Computer Version"
|
||||||
|
description: |
|
||||||
|
**Why is this needed?**
|
||||||
|
1. Old version issues may have been fixed in newer releases (always use the latest version)
|
||||||
|
2. Different desktop environments (XFCE/LXQT/GXDE) may behave differently
|
||||||
|
**If not using the latest version, be prepared for your issue to be ignored**
|
||||||
|
placeholder: "Version number + desktop environment (e.g., v1.0.23 GXDE)"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: is-latest
|
||||||
|
attributes:
|
||||||
|
label: "Is this the latest version?"
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- "No"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: why-not-latest
|
||||||
|
attributes:
|
||||||
|
label: "If not using the latest version, you must explain the reason for not using the latest version"
|
||||||
|
placeholder: "Explain in detail..."
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: "Steps to reproduce the issue"
|
||||||
|
description: |
|
||||||
|
**Please note:**
|
||||||
|
Provide a **complete screen recording video** starting from launching the software until the issue occurs, rather than using text descriptions or screenshots. Based on past issues, user descriptions often omit potential details.
|
||||||
|
Due to the complexity of the Linux environment, any operational details (such as permissions, environment configurations, etc.) may affect issue reproduction. Ensure your issue can be reproduced on a freshly installed Tiny Computer.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**Issue Priority Explanation:**
|
||||||
|
The original purpose of this software is to install PC-level applications such as WPS, CAJ Viewer, and Edraw Max. Issues related to the Tiny Computer itself and problems associated with these software will be prioritized. For other issues, especially those involving third-party software or general Linux usage, please post in the [discussion](https://github.com/Cateners/tiny_computer/discussions) section.
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 获取帮助或提问
|
||||||
|
url: https://github.com/Cateners/tiny_computer/discussions
|
||||||
|
about: 请在这里提问和讨论,不要将其作为 Issue。
|
||||||
21
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
21
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: 功能请求
|
||||||
|
description: "请先确认:该功能是否能显著提升小小电脑的使用体验?"
|
||||||
|
title: "[Feature] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**必读须知:**
|
||||||
|
- 请先搜索[所有issue](https://github.com/Cateners/tiny_computer/issues?q=is%3Aissue),**相同问题重复提交会被直接关闭**
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: rationale
|
||||||
|
attributes:
|
||||||
|
label: "必须说明使用场景和价值"
|
||||||
|
description: |
|
||||||
|
**为什么需要这个?实现这个会对其他用户带来什么好处?开发者会花费许多时间在上面,这对开发者有什么好处?**
|
||||||
|
(简单来说,就是不要许愿,用充分的理由来说服我或其他潜在(?)的开发者吧!如果你想要一个功能,要么有时间自己学自己开发,要么用钱雇人做,要么等待其他和你想法一致且有时间或有钱的人来做)
|
||||||
|
因为精力有限,我会更高可能采纳有价值且易实现的功能。没能处理的issue会保留以让更多人看到,也许网友会有更好的解决办法!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
21
.github/ISSUE_TEMPLATE/feature_request_eng.yml
vendored
Normal file
21
.github/ISSUE_TEMPLATE/feature_request_eng.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: "Please confirm first: Will this feature significantly improve the user experience of Tiny Computer?"
|
||||||
|
title: "[Feature] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**Important Notes:**
|
||||||
|
- Please search [all issues](https://github.com/Cateners/tiny_computer/issues?q=is%3Aissue) first. **Duplicate submissions will be closed directly**
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: rationale
|
||||||
|
attributes:
|
||||||
|
label: "Must specify usage scenarios and value"
|
||||||
|
description: |
|
||||||
|
**Why is this needed? What benefits will implementing this bring to other users? Developers will spend a lot of time on this—what’s in it for them?**
|
||||||
|
(In short, don’t just make a wish—convince me or other potential (?) developers with solid reasoning! If you want a feature, either take the time to learn and develop it yourself, pay someone to do it, or wait for someone who shares your idea and has the time or money to implement it.)
|
||||||
|
Due to limited resources, I'm more likely to adopt valuable and easily implementable features. Unaddressed issues will remain visible for community solutions!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -39,6 +39,7 @@ app.*.symbols
|
|||||||
app.*.map.json
|
app.*.map.json
|
||||||
|
|
||||||
# Android Studio will place build artifacts here
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/build
|
||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
@@ -47,3 +48,18 @@ app.*.map.json
|
|||||||
/backup
|
/backup
|
||||||
|
|
||||||
assets/xa*
|
assets/xa*
|
||||||
|
assets/patch.tar.gz
|
||||||
|
|
||||||
|
android/app/src/main/jniLibs/*
|
||||||
|
|
||||||
|
devtools_options.yaml
|
||||||
|
|
||||||
|
lib/l10n/app_localizations*
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
|
||||||
|
# Configuration files
|
||||||
|
android/keystore.properties
|
||||||
|
android/local.properties
|
||||||
25
.metadata
25
.metadata
@@ -4,7 +4,7 @@
|
|||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: "efbf63d9c66b9f6ec30e9ad4611189aa80003d31"
|
revision: "edada7c56edf4a183c1735310e123c7f923584f1"
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
@@ -13,26 +13,11 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||||
- platform: android
|
- platform: android
|
||||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||||
- platform: ios
|
|
||||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
|
||||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
|
||||||
- platform: linux
|
|
||||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
|
||||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
|
||||||
- platform: macos
|
|
||||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
|
||||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
|
||||||
- platform: web
|
|
||||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
|
||||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
|
||||||
- platform: windows
|
|
||||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
|
||||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
|||||||
106
README.md
106
README.md
@@ -1,81 +1,113 @@
|
|||||||
|
[](https://github.com/Cateners/tiny_computer/blob/master/readme/cover0.png)
|
||||||
|
|
||||||
# 小小电脑
|
# 小小电脑
|
||||||
|
|
||||||
<img decoding="async" src="readme/cover0.png" width="50%">
|
给所有安卓 9 以上 arm64 设备的“PC 应用引擎”平替。你可以在小小电脑上安装 PC 级 WPS、CAJ Viewer、亿图图示等软件。
|
||||||
|
|
||||||
给所有安卓arm64设备的“PC应用引擎”平替
|
|
||||||
|
|
||||||
Click-to-run debian bookworm xfce on android for Chinese users, with fcitx pinyin input method preinstalled. No termux required.
|
Run Debian Trixie with XFCE, LXQt, or other desktop environments on Android—just with one click. Originally developed for Chinese users to run applications like WPS Office, it comes preinstalled with tools such as the Fcitx Pinyin input method. Please note that this app does not require Termux.
|
||||||
|
|
||||||
|
To change the language inside the container, simply run the `tmoe` command, select “Manager” and navigate to the locale settings. The root filesystem was built using [tmoe](https://github.com/2moe/tmoe), so locale configuration is handled through it. You will also need to update the `LANG=zh_CN.UTF-8` environment variable in the startup command (go to Control → Advanced Settings → Startup Command) when switching to another language.
|
||||||
|
|
||||||
|
Note: English UI is supported since version 1.0.23, though some hint texts may still appear in Chinese.
|
||||||
|
As of version 1.0.100, the container will automatically switch to English if it detects that your device is not using Chinese.
|
||||||
|
|
||||||
## 特点
|
## 特点
|
||||||
|
|
||||||
- 一键安装,即开即用
|
- 一键安装,即开即用
|
||||||
- 来自kali-undercover的win10主题(仅xfce版本),友好的界面
|
- 来自 kali-undercover 的 win10 主题(仅 xfce 版本),友好的界面
|
||||||
|
|
||||||
<img decoding="async" src="readme/img1.png" width="50%">
|

|
||||||
|
|
||||||
- 提供常用软件的一键安装指令
|
- 提供常用软件的一键安装指令(点击图片可查看更多说明)
|
||||||
|
|
||||||
<img decoding="async" src="readme/img2.png" width="50%">
|
[](https://gitee.com/caten/tc-hints/blob/master/pool/solution.md)
|
||||||
|
|
||||||
- 可方便地改变屏幕缩放,不用担心屏幕过大或过小
|
- 可方便地改变屏幕缩放,不用担心屏幕过大或过小 (点击图片可查看更多说明)
|
||||||
|
|
||||||
<img decoding="async" src="readme/img3.gif" width="50%">
|
[](https://gitee.com/caten/tc-hints/blob/master/pool/scale.md)
|
||||||
|
|
||||||
- 便捷访问设备文件,或通过设备SAF访问软件文件
|
- 便捷访问设备文件,或通过设备 SAF 访问软件文件(点击图片可查看更多说明)
|
||||||
|
|
||||||
<img decoding="async" src="readme/img4.png" width="50%">
|
[](https://gitee.com/caten/tc-hints/blob/master/pool/fileaccess.md)
|
||||||
|
|
||||||
- 提供终端和众多可调节参数供高级用户使用
|
- 提供终端和众多可调节参数供高级用户使用
|
||||||
|
|
||||||
<img decoding="async" src="readme/img5.png" width="50%">
|

|
||||||
|
|
||||||
|
## 下载
|
||||||
|
|
||||||
|
小小电脑提供多个版本。要将小小电脑作为 PC 应用引擎使用,请在 [Releases](https://github.com/Cateners/tiny_computer/releases) 页面下载并安装 [XFCE](https://xfce.org/) 版本(tiny-computer-xfce.apk)。
|
||||||
|
|
||||||
|
如果遇到黑屏问题,请卸载后尝试 [LXQt](https://lxqt-project.org/) 版本([Releases](https://github.com/Cateners/tiny_computer/releases) 页寻找 tiny-computer-lxqt.apk)。
|
||||||
|
|
||||||
|
这些版本的区别在于桌面环境不同。你可以简单地理解为界面不一样,但功能基本一致。
|
||||||
|
|
||||||
|
LXQt 的界面示例:
|
||||||
|
|
||||||
|
[](https://camo.githubusercontent.com/016ff8803c228f26db750c8424777d8e04a3aebec4ff11d8436a0b22a2e6f58a/68747470733a2f2f6c7871742d70726f6a6563742e6f72672f696d616765732f73637265656e73686f74732f616d6269616e63652e706e67)
|
||||||
|
|
||||||
|
如果你下载小小电脑是为了体验更多桌面环境,享受折腾 Linux 的乐趣,这里也有一些其他版本供下载!
|
||||||
|
|
||||||
|
和 [GXDE](https://www.gxde.org/) 团队合作的版本 [#129](https://github.com/Cateners/tiny_computer/issues/129)。可在[此处](https://mirrors.sdu.edu.cn/spark-store-repository/GXDE-OS/APK/)下载。GXDE 的界面示例:
|
||||||
|
|
||||||
|
[](https://www.gxde.top/1.jpg)
|
||||||
|
|
||||||
|
由[灵墨桌面](https://www.lingmo.org/)开发者提供的版本[#218](https://github.com/Cateners/tiny_computer/issues/218)。灵墨桌面的界面[示例](https://www.bilibili.com/video/BV1Ci421R7AR)。
|
||||||
|
|
||||||
## 原理
|
## 原理
|
||||||
|
|
||||||
使用proot运行debian环境
|
使用 proot 运行 debian 环境。
|
||||||
|
|
||||||
内置[noVNC](https://github.com/novnc/noVNC)显示图形界面
|
内置 [noVNC](https://github.com/novnc/noVNC)/[AVNC](https://github.com/gujjwal00/avnc)/[Termux:X11](https://github.com/termux/termux-x11) 显示图形界面。
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
assets的文件来源信息可以在[这里](extra/readme.md)找到。
|
assets 和 android/app/src/main/jniLibs 的文件源信息可以在[这里](https://github.com/Cateners/tiny_computer/blob/master/extra/readme.md)找到。
|
||||||
|
|
||||||
完整的容器制作过程可以在[这里](extra/build-tiny-rootfs.md)看到。
|
完整的容器制作过程可以在[这里](https://github.com/Cateners/tiny_computer/blob/master/extra/build-tiny-rootfs.md)看到。
|
||||||
|
|
||||||
数据包不再在assets中更新,而是随releases提供,主要是为了避免git越来越大
|
数据包、patch.tar.gz 以及 jniLibs 的文件不在代码仓更新,而是随 releases 提供,主要是为了避免 git 越来越大。
|
||||||
|
|
||||||
lib目录:
|
lib 目录:
|
||||||
|
|
||||||
- main.dart文件,页面布局,有点乱
|
- main.dart 文件,页面布局,有点乱
|
||||||
- workflow.dart文件,逻辑部分,目前也还可以理解
|
- workflow.dart 文件,逻辑部分,目前也还可以理解
|
||||||
- Util 工具类
|
- Util 工具类
|
||||||
- TermPty 一个终端
|
- TermPty 一个终端
|
||||||
- G 全局变量类
|
- G 全局变量类
|
||||||
- Workflow 从软件点开到容器启动的所有步骤
|
- Workflow 从软件点开到容器启动的所有步骤
|
||||||
|
- l10n 文件夹,包含多语言文件
|
||||||
|
|
||||||
## 编译
|
## 编译
|
||||||
|
|
||||||
你需要配置好flutter和安卓sdk,然后克隆此项目。
|
你需要配置好 flutter 和安卓 sdk。
|
||||||
|
|
||||||
在编译之前,需要在release中下载系统rootfs(或者[自行制作](extra/build-tiny-rootfs.md)),之后使用split命令分割,拷贝到assets。一般我将其分为98MB。
|
在编译之前,需要在 release 中下载 jniLibs.zip ,将里面的库文件解压后放到 android/app/src/main/jniLibs/arm64-v8a;下载 patch.tar.gz 拷贝到 assets。以及下载系统 rootfs(或者[自行制作](https://github.com/Cateners/tiny_computer/blob/master/extra/build-tiny-rootfs.md)),之后使用 split 命令分割,拷贝到 assets。一般我将其分为 98MB。
|
||||||
|
|
||||||
`split -b 98M debian.tar.xz`
|
```bash
|
||||||
|
split -b 98M debian.tar.xz
|
||||||
|
```
|
||||||
|
|
||||||
接下来就可以编译了。我使用的命令如下:
|
接下来就可以编译了。如果要编译release版本,需要设置发布密钥,可以参考android/keystore.properties.example文件。
|
||||||
|
|
||||||
`flutter build apk --target-platform android-arm64 --split-per-abi --obfuscate --split-debug-info=tiny_computer/sdi`
|
我使用的编译命令如下:
|
||||||
|
|
||||||
## 目前已知bug
|
```bash
|
||||||
|
flutter build apk --target-platform android-arm64 --split-per-abi --obfuscate --split-debug-info=tiny_computer/sdi
|
||||||
|
```
|
||||||
|
|
||||||
多用户/分身情形无法sudo, 其它见issue
|
## 目前已知 bug
|
||||||
|
|
||||||
|
多用户/分身情形无法 sudo , 其它见 issue。
|
||||||
|
|
||||||
## 一些链接
|
## 一些链接
|
||||||
|
|
||||||
这是我的第一个flutter软件,感谢这些项目为我指路
|
这是我的第一个 flutter 软件,感谢这些项目为我指路
|
||||||
|
|
||||||
- 要一点基础的 [《Flutter实战·第二版》](https://book.flutterchina.club)
|
- 要一点基础的[《Flutter实战·第二版》](https://book.flutterchina.club/)
|
||||||
- 也许是零基础的Flutter视频课程 [freeCodeCamp Flutter Course](https://www.youtube.com/watch?v=wFn-m-OgKPU&list=PL6yRaaP0WPkVtoeNIGqILtRAgd3h2CNpT)
|
- 也许是零基础的Flutter视频课程 [freeCodeCamp Flutter Course](https://www.youtube.com/watch?v=wFn-m-OgKPU&list=PL6yRaaP0WPkVtoeNIGqILtRAgd3h2CNpT)
|
||||||
|
- 安卓上的 VS Code [Code FA](https://github.com/nightmare-space/vscode_for_android)
|
||||||
- 安卓上的VS Code [Code FA](https://github.com/nightmare-space/vscode_for_android)
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -86,6 +118,4 @@ A few resources to get you started if this is your first Flutter project:
|
|||||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
For help getting started with Flutter development, view the
|
For help getting started with Flutter development, view the [online documentation](https://docs.flutter.dev/), which offers tutorials, samples, guidance on mobile development, and a full API reference.
|
||||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
|
||||||
samples, guidance on mobile development, and a full API reference.
|
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
|
|
||||||
cmake_minimum_required(VERSION 3.10)
|
|
||||||
|
|
||||||
project(NativeVNC C CXX ASM)
|
|
||||||
|
|
||||||
set(AVNC_EXTERN_DIR ${PROJECT_SOURCE_DIR}/../extern)
|
|
||||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared Libs" FORCE)
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Utilities
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
# Make sure given submodule is checked out
|
|
||||||
macro(avnc_check_submodule name)
|
|
||||||
if (NOT EXISTS "${AVNC_EXTERN_DIR}/${name}/CMakeLists.txt")
|
|
||||||
message(FATAL_ERROR "git submodule for ${name} is not initialized.
|
|
||||||
Please run 'git submodule update --init'.")
|
|
||||||
endif ()
|
|
||||||
endmacro()
|
|
||||||
|
|
||||||
|
|
||||||
# Required to enable SIMD support on ARM
|
|
||||||
if (CMAKE_ANDROID_ARCH STREQUAL "arm64")
|
|
||||||
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} --target=aarch64-linux-android${ANDROID_NATIVE_API_LEVEL}")
|
|
||||||
elseif (CMAKE_ANDROID_ARCH STREQUAL "arm")
|
|
||||||
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} --target=arm-linux-androideabi${ANDROID_NATIVE_API_LEVEL}")
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# JPEG
|
|
||||||
###############################################################################
|
|
||||||
avnc_check_submodule(libjpeg-turbo)
|
|
||||||
|
|
||||||
set(AVNC_LIBJPEG_SRC_DIR ${AVNC_EXTERN_DIR}/libjpeg-turbo)
|
|
||||||
set(AVNC_LIBJPEG_BUILD_DIR ${CMAKE_BINARY_DIR}/libjpeg-turbo)
|
|
||||||
|
|
||||||
add_subdirectory(${AVNC_LIBJPEG_SRC_DIR} ${AVNC_LIBJPEG_BUILD_DIR})
|
|
||||||
|
|
||||||
# Set these variables so FindJPEG can find the library
|
|
||||||
set(JPEG_LIBRARY ${AVNC_LIBJPEG_BUILD_DIR}/libturbojpeg.a)
|
|
||||||
set(JPEG_INCLUDE_DIR ${AVNC_LIBJPEG_SRC_DIR})
|
|
||||||
|
|
||||||
include_directories(${AVNC_LIBJPEG_SRC_DIR} ${AVNC_LIBJPEG_BUILD_DIR})
|
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# SSL
|
|
||||||
###############################################################################
|
|
||||||
avnc_check_submodule(wolfssl)
|
|
||||||
|
|
||||||
set(AVNC_LIBSSL_SRC_DIR ${AVNC_EXTERN_DIR}/wolfssl)
|
|
||||||
set(AVNC_LIBSSL_BUILD_DIR ${CMAKE_BINARY_DIR}/wolfssl)
|
|
||||||
|
|
||||||
# CMake support in wolfSSl is still under development, so we have to
|
|
||||||
# manually set some flags to enable OpenSSL compatibility layer
|
|
||||||
set(WOLFSSL_CRL yes)
|
|
||||||
set(WOLFSSL_DES3 yes)
|
|
||||||
set(WOLFSSL_CRYPT_TESTS no)
|
|
||||||
add_definitions(-DOPENSSL_ALL -DOPENSSL_EXTRA -DHAVE_CRL -DHAVE_EX_DATA
|
|
||||||
-DHAVE_ANON -DWOLFSSL_AES_DIRECT -DHAVE_AES_ECB -DWOLFSSL_DES_ECB
|
|
||||||
-DWC_NO_HARDEN)
|
|
||||||
|
|
||||||
add_subdirectory(${AVNC_LIBSSL_SRC_DIR} ${AVNC_LIBSSL_BUILD_DIR})
|
|
||||||
|
|
||||||
# Set these variables so FindOpenSSL can find the library
|
|
||||||
set(OPENSSL_SSL_LIBRARY ${AVNC_LIBSSL_BUILD_DIR}/libwolfssl.a)
|
|
||||||
set(OPENSSL_CRYPTO_LIBRARY ${AVNC_LIBSSL_BUILD_DIR}/libwolfssl.a)
|
|
||||||
set(OPENSSL_INCLUDE_DIR ${AVNC_LIBSSL_SRC_DIR}/wolfssl)
|
|
||||||
|
|
||||||
include_directories(${AVNC_LIBSSL_SRC_DIR})
|
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# LibVNC
|
|
||||||
###############################################################################
|
|
||||||
avnc_check_submodule(libvncserver)
|
|
||||||
|
|
||||||
set(AVNC_LIBVNC_SRC_DIR ${AVNC_EXTERN_DIR}/libvncserver)
|
|
||||||
set(AVNC_LIBVNC_BUILD_DIR ${CMAKE_BINARY_DIR}/libvncserver)
|
|
||||||
set(WITH_LIBSSH2 OFF CACHE BOOL "Find LibSSH" FORCE)
|
|
||||||
|
|
||||||
add_subdirectory(${AVNC_LIBVNC_SRC_DIR} ${AVNC_LIBVNC_BUILD_DIR}) # (source dir, build dir)
|
|
||||||
|
|
||||||
include_directories(${AVNC_LIBVNC_SRC_DIR}/include ${AVNC_LIBVNC_BUILD_DIR}/include)
|
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Native VNC
|
|
||||||
#
|
|
||||||
# It contains implementation of JNI native methods, some NDK scaffolding and
|
|
||||||
# some helpers for OpenGL ES rendring. This is the library loaded from Java.
|
|
||||||
###############################################################################
|
|
||||||
set(AVNC_NATIVE_SOURCE src/main/cpp/native-vnc.cpp)
|
|
||||||
|
|
||||||
add_library(native-vnc SHARED ${AVNC_NATIVE_SOURCE})
|
|
||||||
|
|
||||||
target_link_libraries(native-vnc vncclient)
|
|
||||||
|
|
||||||
|
|
||||||
# Link NDK libraries
|
|
||||||
find_library(LIB_LOG log)
|
|
||||||
target_link_libraries(native-vnc ${LIB_LOG})
|
|
||||||
|
|
||||||
find_library(LIB_GLES GLESv2)
|
|
||||||
target_link_libraries(native-vnc ${LIB_GLES})
|
|
||||||
@@ -2,10 +2,6 @@ plugins {
|
|||||||
id "com.android.application"
|
id "com.android.application"
|
||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
|
||||||
id "org.jetbrains.kotlin.kapt"
|
|
||||||
id "org.jetbrains.kotlin.plugin.serialization" version "1.9.10"
|
|
||||||
id "org.jetbrains.kotlin.plugin.parcelize"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def localProperties = new Properties()
|
def localProperties = new Properties()
|
||||||
@@ -26,18 +22,24 @@ if (flutterVersionName == null) {
|
|||||||
flutterVersionName = '1.0'
|
flutterVersionName = '1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def keystoreProperties = new Properties()
|
||||||
|
def keystorePropertiesFile = rootProject.file('keystore.properties')
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "com.example.tiny_computer"
|
namespace "com.example.tiny_computer"
|
||||||
compileSdkVersion flutter.compileSdkVersion
|
compileSdkVersion flutter.compileSdkVersion
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion "27.0.12077973" // flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -50,43 +52,67 @@ android {
|
|||||||
applicationId "com.fct.tiny"
|
applicationId "com.fct.tiny"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
minSdkVersion 24 //ffmpeg_kit; flutter.minSdkVersion
|
minSdk 28 // glob() version //proot version //ffmpeg_kit; flutter.minSdkVersion
|
||||||
targetSdkVersion 28 //https://github.com/termux/termux-app/issues/1072; native; linker; flutter.targetSdkVersion
|
targetSdk 28 //https://github.com/termux/termux-app/issues/1072; native; linker; flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|
||||||
javaCompileOptions {
|
buildConfigField "String", "COMMIT", "\"" + ("git rev-parse HEAD\n".execute().getText().trim() ?: (System.getenv('CURRENT_COMMIT') ?: "NO_COMMIT")) + "\""
|
||||||
annotationProcessorOptions {
|
|
||||||
arguments += ["room.schemaLocation": "$projectDir/roomSchema/".toString()]
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
cppFlags "-std=c++11"
|
||||||
|
arguments "-DANDROID_STL=c++_shared"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig signingConfigs.release
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
debug {
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
|
aidl true
|
||||||
dataBinding true
|
dataBinding true
|
||||||
|
viewBinding true
|
||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
version '3.22.1'
|
|
||||||
path file('CMakeLists.txt')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
lint {
|
||||||
|
disable "NullSafeMutableLiveData"
|
||||||
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
//checkReleaseBuilds false
|
//checkReleaseBuilds false
|
||||||
abortOnError false
|
abortOnError false
|
||||||
}
|
}
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path "src/main/cpp/CMakeLists.txt"
|
||||||
|
version "3.22.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||||
@@ -101,26 +127,8 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.core:core-ktx:1.12.0"
|
implementation "androidx.core:core-ktx:1.15.0"
|
||||||
implementation "androidx.activity:activity-ktx:1.8.2"
|
implementation "androidx.appcompat:appcompat:1.7.0"
|
||||||
implementation "androidx.fragment:fragment-ktx:1.6.2"
|
|
||||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
|
||||||
implementation "androidx.preference:preference-ktx:1.2.1"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
|
|
||||||
implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"
|
|
||||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
|
||||||
|
|
||||||
|
|
||||||
def roomVersion = "2.6.1"
|
|
||||||
implementation "androidx.room:room-runtime:$roomVersion"
|
|
||||||
implementation "androidx.room:room-ktx:$roomVersion"
|
|
||||||
kapt "androidx.room:room-compiler:$roomVersion"
|
|
||||||
|
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.7.0"
|
implementation "com.google.android.material:material:1.7.0"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0"
|
implementation 'com.github.tiann:FreeReflection:3.2.2'
|
||||||
implementation "org.connectbot:sshlib:2.2.23"
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
android/app/proguard-rules.pro
vendored
Normal file
17
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Please add these rules to your existing keep rules in order to suppress warnings.
|
||||||
|
# This is generated automatically by the Android Gradle plugin.
|
||||||
|
-dontwarn android.app.ActivityThread
|
||||||
|
-dontwarn android.app.ContextImpl
|
||||||
|
-dontwarn android.app.IActivityManager
|
||||||
|
-dontwarn android.content.IIntentReceiver$Stub
|
||||||
|
-dontwarn android.content.IIntentReceiver
|
||||||
|
-dontwarn android.content.IIntentSender
|
||||||
|
-dontwarn android.content.pm.IPackageManager
|
||||||
|
-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
|
||||||
|
-dontwarn com.google.errorprone.annotations.Immutable
|
||||||
|
# 保持 Termux X11 所有内容
|
||||||
|
-keep class com.termux.x11.** { *; }
|
||||||
|
-keepclassmembers class com.termux.x11.** { *; }
|
||||||
|
|
||||||
|
|
||||||
|
-dontwarn javax.annotation.Nullable
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 1,
|
|
||||||
"identityHash": "ccb0ad6d8acbefcb44a49c07f353adc8",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "profiles",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `securityType` INTEGER NOT NULL, `channelType` INTEGER NOT NULL, `colorLevel` INTEGER NOT NULL, `imageQuality` INTEGER NOT NULL, `viewOnly` INTEGER NOT NULL, `useLocalCursor` INTEGER NOT NULL, `keyCompatMode` INTEGER NOT NULL, `useRepeater` INTEGER NOT NULL, `idOnRepeater` INTEGER NOT NULL, `sshHost` TEXT NOT NULL, `sshPort` INTEGER NOT NULL, `sshUsername` TEXT NOT NULL, `sshAuthType` INTEGER NOT NULL, `sshPassword` TEXT NOT NULL, `sshPrivateKey` TEXT NOT NULL, `sshPrivateKeyPassword` TEXT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "ID",
|
|
||||||
"columnName": "ID",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "host",
|
|
||||||
"columnName": "host",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "port",
|
|
||||||
"columnName": "port",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "username",
|
|
||||||
"columnName": "username",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "password",
|
|
||||||
"columnName": "password",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "securityType",
|
|
||||||
"columnName": "securityType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "channelType",
|
|
||||||
"columnName": "channelType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "colorLevel",
|
|
||||||
"columnName": "colorLevel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "imageQuality",
|
|
||||||
"columnName": "imageQuality",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "viewOnly",
|
|
||||||
"columnName": "viewOnly",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useLocalCursor",
|
|
||||||
"columnName": "useLocalCursor",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "keyCompatMode",
|
|
||||||
"columnName": "keyCompatMode",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useRepeater",
|
|
||||||
"columnName": "useRepeater",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "idOnRepeater",
|
|
||||||
"columnName": "idOnRepeater",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshHost",
|
|
||||||
"columnName": "sshHost",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPort",
|
|
||||||
"columnName": "sshPort",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshUsername",
|
|
||||||
"columnName": "sshUsername",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshAuthType",
|
|
||||||
"columnName": "sshAuthType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPassword",
|
|
||||||
"columnName": "sshPassword",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPrivateKey",
|
|
||||||
"columnName": "sshPrivateKey",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPrivateKeyPassword",
|
|
||||||
"columnName": "sshPrivateKeyPassword",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"ID"
|
|
||||||
],
|
|
||||||
"autoGenerate": true
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ccb0ad6d8acbefcb44a49c07f353adc8')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 2,
|
|
||||||
"identityHash": "d54a9ffbaa53dfe8f4ce8f5708a719ae",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "profiles",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `securityType` INTEGER NOT NULL, `channelType` INTEGER NOT NULL, `colorLevel` INTEGER NOT NULL, `imageQuality` INTEGER NOT NULL, `useRawEncoding` INTEGER NOT NULL DEFAULT 0, `zoom1` REAL NOT NULL DEFAULT 1.0, `zoom2` REAL NOT NULL DEFAULT 1.0, `viewOnly` INTEGER NOT NULL, `useLocalCursor` INTEGER NOT NULL, `keyCompatMode` INTEGER NOT NULL, `gestureStyle` TEXT NOT NULL DEFAULT 'auto', `useRepeater` INTEGER NOT NULL, `idOnRepeater` INTEGER NOT NULL, `sshHost` TEXT NOT NULL, `sshPort` INTEGER NOT NULL, `sshUsername` TEXT NOT NULL, `sshAuthType` INTEGER NOT NULL, `sshPassword` TEXT NOT NULL, `sshPrivateKey` TEXT NOT NULL, `sshPrivateKeyPassword` TEXT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "ID",
|
|
||||||
"columnName": "ID",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "host",
|
|
||||||
"columnName": "host",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "port",
|
|
||||||
"columnName": "port",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "username",
|
|
||||||
"columnName": "username",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "password",
|
|
||||||
"columnName": "password",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "securityType",
|
|
||||||
"columnName": "securityType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "channelType",
|
|
||||||
"columnName": "channelType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "colorLevel",
|
|
||||||
"columnName": "colorLevel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "imageQuality",
|
|
||||||
"columnName": "imageQuality",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useRawEncoding",
|
|
||||||
"columnName": "useRawEncoding",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "zoom1",
|
|
||||||
"columnName": "zoom1",
|
|
||||||
"affinity": "REAL",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "zoom2",
|
|
||||||
"columnName": "zoom2",
|
|
||||||
"affinity": "REAL",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "viewOnly",
|
|
||||||
"columnName": "viewOnly",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useLocalCursor",
|
|
||||||
"columnName": "useLocalCursor",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "keyCompatMode",
|
|
||||||
"columnName": "keyCompatMode",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "gestureStyle",
|
|
||||||
"columnName": "gestureStyle",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "'auto'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useRepeater",
|
|
||||||
"columnName": "useRepeater",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "idOnRepeater",
|
|
||||||
"columnName": "idOnRepeater",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshHost",
|
|
||||||
"columnName": "sshHost",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPort",
|
|
||||||
"columnName": "sshPort",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshUsername",
|
|
||||||
"columnName": "sshUsername",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshAuthType",
|
|
||||||
"columnName": "sshAuthType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPassword",
|
|
||||||
"columnName": "sshPassword",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPrivateKey",
|
|
||||||
"columnName": "sshPrivateKey",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPrivateKeyPassword",
|
|
||||||
"columnName": "sshPrivateKeyPassword",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"ID"
|
|
||||||
],
|
|
||||||
"autoGenerate": true
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd54a9ffbaa53dfe8f4ce8f5708a719ae')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 3,
|
|
||||||
"identityHash": "eb32c7692bb75a6297413b807713616c",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "profiles",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `securityType` INTEGER NOT NULL, `channelType` INTEGER NOT NULL, `colorLevel` INTEGER NOT NULL, `imageQuality` INTEGER NOT NULL, `useRawEncoding` INTEGER NOT NULL DEFAULT 0, `zoom1` REAL NOT NULL DEFAULT 1.0, `zoom2` REAL NOT NULL DEFAULT 1.0, `viewOnly` INTEGER NOT NULL, `useLocalCursor` INTEGER NOT NULL, `serverTypeHint` TEXT NOT NULL DEFAULT '', `compatFlags` INTEGER NOT NULL, `gestureStyle` TEXT NOT NULL DEFAULT 'auto', `screenOrientation` TEXT NOT NULL DEFAULT 'auto', `shortcutRank` INTEGER NOT NULL DEFAULT 0, `useRepeater` INTEGER NOT NULL, `idOnRepeater` INTEGER NOT NULL, `sshHost` TEXT NOT NULL, `sshPort` INTEGER NOT NULL, `sshUsername` TEXT NOT NULL, `sshAuthType` INTEGER NOT NULL, `sshPassword` TEXT NOT NULL, `sshPrivateKey` TEXT NOT NULL, `sshPrivateKeyPassword` TEXT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "ID",
|
|
||||||
"columnName": "ID",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "host",
|
|
||||||
"columnName": "host",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "port",
|
|
||||||
"columnName": "port",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "username",
|
|
||||||
"columnName": "username",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "password",
|
|
||||||
"columnName": "password",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "securityType",
|
|
||||||
"columnName": "securityType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "channelType",
|
|
||||||
"columnName": "channelType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "colorLevel",
|
|
||||||
"columnName": "colorLevel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "imageQuality",
|
|
||||||
"columnName": "imageQuality",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useRawEncoding",
|
|
||||||
"columnName": "useRawEncoding",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "zoom1",
|
|
||||||
"columnName": "zoom1",
|
|
||||||
"affinity": "REAL",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "zoom2",
|
|
||||||
"columnName": "zoom2",
|
|
||||||
"affinity": "REAL",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "viewOnly",
|
|
||||||
"columnName": "viewOnly",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useLocalCursor",
|
|
||||||
"columnName": "useLocalCursor",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serverTypeHint",
|
|
||||||
"columnName": "serverTypeHint",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "compatFlags",
|
|
||||||
"columnName": "compatFlags",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "gestureStyle",
|
|
||||||
"columnName": "gestureStyle",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "'auto'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "screenOrientation",
|
|
||||||
"columnName": "screenOrientation",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "'auto'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shortcutRank",
|
|
||||||
"columnName": "shortcutRank",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useRepeater",
|
|
||||||
"columnName": "useRepeater",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "idOnRepeater",
|
|
||||||
"columnName": "idOnRepeater",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshHost",
|
|
||||||
"columnName": "sshHost",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPort",
|
|
||||||
"columnName": "sshPort",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshUsername",
|
|
||||||
"columnName": "sshUsername",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshAuthType",
|
|
||||||
"columnName": "sshAuthType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPassword",
|
|
||||||
"columnName": "sshPassword",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPrivateKey",
|
|
||||||
"columnName": "sshPrivateKey",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPrivateKeyPassword",
|
|
||||||
"columnName": "sshPrivateKeyPassword",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"ID"
|
|
||||||
],
|
|
||||||
"autoGenerate": true
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb32c7692bb75a6297413b807713616c')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 4,
|
|
||||||
"identityHash": "5ff9de8e52fb13b10ee86f9c75714cd5",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "profiles",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `securityType` INTEGER NOT NULL, `channelType` INTEGER NOT NULL, `colorLevel` INTEGER NOT NULL, `imageQuality` INTEGER NOT NULL, `useRawEncoding` INTEGER NOT NULL DEFAULT 0, `zoom1` REAL NOT NULL DEFAULT 1.0, `zoom2` REAL NOT NULL DEFAULT 1.0, `viewOnly` INTEGER NOT NULL, `useLocalCursor` INTEGER NOT NULL, `serverTypeHint` TEXT NOT NULL DEFAULT '', `compatFlags` INTEGER NOT NULL, `gestureStyle` TEXT NOT NULL DEFAULT 'auto', `screenOrientation` TEXT NOT NULL DEFAULT 'auto', `shortcutRank` INTEGER NOT NULL DEFAULT 0, `useRepeater` INTEGER NOT NULL, `idOnRepeater` INTEGER NOT NULL, `resizeRemoteDesktop` INTEGER NOT NULL DEFAULT 0, `sshHost` TEXT NOT NULL, `sshPort` INTEGER NOT NULL, `sshUsername` TEXT NOT NULL, `sshAuthType` INTEGER NOT NULL, `sshPassword` TEXT NOT NULL, `sshPrivateKey` TEXT NOT NULL, `sshPrivateKeyPassword` TEXT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "ID",
|
|
||||||
"columnName": "ID",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "host",
|
|
||||||
"columnName": "host",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "port",
|
|
||||||
"columnName": "port",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "username",
|
|
||||||
"columnName": "username",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "password",
|
|
||||||
"columnName": "password",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "securityType",
|
|
||||||
"columnName": "securityType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "channelType",
|
|
||||||
"columnName": "channelType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "colorLevel",
|
|
||||||
"columnName": "colorLevel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "imageQuality",
|
|
||||||
"columnName": "imageQuality",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useRawEncoding",
|
|
||||||
"columnName": "useRawEncoding",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "zoom1",
|
|
||||||
"columnName": "zoom1",
|
|
||||||
"affinity": "REAL",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "zoom2",
|
|
||||||
"columnName": "zoom2",
|
|
||||||
"affinity": "REAL",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "viewOnly",
|
|
||||||
"columnName": "viewOnly",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useLocalCursor",
|
|
||||||
"columnName": "useLocalCursor",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serverTypeHint",
|
|
||||||
"columnName": "serverTypeHint",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "compatFlags",
|
|
||||||
"columnName": "compatFlags",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "gestureStyle",
|
|
||||||
"columnName": "gestureStyle",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "'auto'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "screenOrientation",
|
|
||||||
"columnName": "screenOrientation",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "'auto'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shortcutRank",
|
|
||||||
"columnName": "shortcutRank",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useRepeater",
|
|
||||||
"columnName": "useRepeater",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "idOnRepeater",
|
|
||||||
"columnName": "idOnRepeater",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "resizeRemoteDesktop",
|
|
||||||
"columnName": "resizeRemoteDesktop",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshHost",
|
|
||||||
"columnName": "sshHost",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPort",
|
|
||||||
"columnName": "sshPort",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshUsername",
|
|
||||||
"columnName": "sshUsername",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshAuthType",
|
|
||||||
"columnName": "sshAuthType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPassword",
|
|
||||||
"columnName": "sshPassword",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPrivateKey",
|
|
||||||
"columnName": "sshPrivateKey",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPrivateKeyPassword",
|
|
||||||
"columnName": "sshPrivateKeyPassword",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"ID"
|
|
||||||
],
|
|
||||||
"autoGenerate": true
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5ff9de8e52fb13b10ee86f9c75714cd5')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 5,
|
|
||||||
"identityHash": "8448d51fc9838e5760b91d4b5318ade2",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "profiles",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `securityType` INTEGER NOT NULL, `channelType` INTEGER NOT NULL, `colorLevel` INTEGER NOT NULL, `imageQuality` INTEGER NOT NULL, `useRawEncoding` INTEGER NOT NULL DEFAULT 0, `zoom1` REAL NOT NULL DEFAULT 1.0, `zoom2` REAL NOT NULL DEFAULT 1.0, `viewOnly` INTEGER NOT NULL, `useLocalCursor` INTEGER NOT NULL, `serverTypeHint` TEXT NOT NULL DEFAULT '', `flags` INTEGER NOT NULL, `gestureStyle` TEXT NOT NULL DEFAULT 'auto', `screenOrientation` TEXT NOT NULL DEFAULT 'auto', `useCount` INTEGER NOT NULL, `useRepeater` INTEGER NOT NULL, `idOnRepeater` INTEGER NOT NULL, `resizeRemoteDesktop` INTEGER NOT NULL DEFAULT 0, `sshHost` TEXT NOT NULL, `sshPort` INTEGER NOT NULL, `sshUsername` TEXT NOT NULL, `sshAuthType` INTEGER NOT NULL, `sshPassword` TEXT NOT NULL, `sshPrivateKey` TEXT NOT NULL, `sshPrivateKeyPassword` TEXT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "ID",
|
|
||||||
"columnName": "ID",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "host",
|
|
||||||
"columnName": "host",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "port",
|
|
||||||
"columnName": "port",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "username",
|
|
||||||
"columnName": "username",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "password",
|
|
||||||
"columnName": "password",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "securityType",
|
|
||||||
"columnName": "securityType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "channelType",
|
|
||||||
"columnName": "channelType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "colorLevel",
|
|
||||||
"columnName": "colorLevel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "imageQuality",
|
|
||||||
"columnName": "imageQuality",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useRawEncoding",
|
|
||||||
"columnName": "useRawEncoding",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "zoom1",
|
|
||||||
"columnName": "zoom1",
|
|
||||||
"affinity": "REAL",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "zoom2",
|
|
||||||
"columnName": "zoom2",
|
|
||||||
"affinity": "REAL",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "viewOnly",
|
|
||||||
"columnName": "viewOnly",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useLocalCursor",
|
|
||||||
"columnName": "useLocalCursor",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serverTypeHint",
|
|
||||||
"columnName": "serverTypeHint",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "flags",
|
|
||||||
"columnName": "flags",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "gestureStyle",
|
|
||||||
"columnName": "gestureStyle",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "'auto'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "screenOrientation",
|
|
||||||
"columnName": "screenOrientation",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "'auto'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useCount",
|
|
||||||
"columnName": "useCount",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "useRepeater",
|
|
||||||
"columnName": "useRepeater",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "idOnRepeater",
|
|
||||||
"columnName": "idOnRepeater",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "resizeRemoteDesktop",
|
|
||||||
"columnName": "resizeRemoteDesktop",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshHost",
|
|
||||||
"columnName": "sshHost",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPort",
|
|
||||||
"columnName": "sshPort",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshUsername",
|
|
||||||
"columnName": "sshUsername",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshAuthType",
|
|
||||||
"columnName": "sshAuthType",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPassword",
|
|
||||||
"columnName": "sshPassword",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPrivateKey",
|
|
||||||
"columnName": "sshPrivateKey",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sshPrivateKeyPassword",
|
|
||||||
"columnName": "sshPrivateKeyPassword",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"ID"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8448d51fc9838e5760b91d4b5318ade2')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,14 +4,17 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="小小电脑"
|
android:label="@string/tc_app_name"
|
||||||
android:name="${applicationName}"
|
android:name=".MainApplication"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@style/App.Theme">
|
android:launchMode="singleInstance"
|
||||||
|
android:theme="@style/App.Theme"
|
||||||
|
android:extractNativeLibs="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -33,32 +36,10 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity-alias
|
|
||||||
android:name="com.gaurav.avnc.UriReceiverActivity"
|
|
||||||
android:targetActivity="com.gaurav.avnc.ui.vnc.IntentReceiverActivity">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="vnc" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity-alias>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.gaurav.avnc.ui.vnc.VncActivity"
|
android:name=".Signal9Activity"
|
||||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
android:exported="true"
|
||||||
android:resizeableActivity="true"
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||||
android:supportsPictureInPicture="true"
|
|
||||||
tools:ignore="UnusedAttribute" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="com.gaurav.avnc.ui.vnc.IntentReceiverActivity"
|
|
||||||
android:theme="@style/App.SplashTheme.Dark" />
|
|
||||||
|
|
||||||
<activity android:name="com.gaurav.avnc.ui.prefs.PrefsActivity" />
|
|
||||||
<activity android:name="com.gaurav.avnc.ui.about.AboutActivity" />
|
|
||||||
<provider
|
<provider
|
||||||
android:name="com.example.tiny_computer.filepicker.TinyDocumentsProvider"
|
android:name="com.example.tiny_computer.filepicker.TinyDocumentsProvider"
|
||||||
android:authorities="com.example.tiny_computer.documents"
|
android:authorities="com.example.tiny_computer.documents"
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
The Modified (3-clause) BSD License
|
|
||||||
===================================
|
|
||||||
|
|
||||||
Copyright (C)2009-2021 D. R. Commander. All Rights Reserved.<br>
|
|
||||||
Copyright (C)2015 Viktor Szathmáry. All Rights Reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
- Redistributions of source code must retain the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer.
|
|
||||||
- Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
- Neither the name of the libjpeg-turbo Project nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from this
|
|
||||||
software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS",
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
|
|
||||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
||||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
||||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
||||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
||||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
@@ -1,674 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
|
|
||||||
/***********************************************************
|
|
||||||
Copyright 1987, 1994, 1998 The Open Group
|
|
||||||
|
|
||||||
Permission to use, copy, modify, distribute, and sell this software and its
|
|
||||||
documentation for any purpose is hereby granted without fee, provided that
|
|
||||||
the above copyright notice appear in all copies and that both that
|
|
||||||
copyright notice and this permission notice appear in supporting
|
|
||||||
documentation.
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included
|
|
||||||
in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
IN NO EVENT SHALL THE OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
||||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
||||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
Except as contained in this notice, the name of The Open Group shall
|
|
||||||
not be used in advertising or otherwise to promote the sale, use or
|
|
||||||
other dealings in this Software without prior written authorization
|
|
||||||
from The Open Group.
|
|
||||||
|
|
||||||
|
|
||||||
Copyright 1987 by Digital Equipment Corporation, Maynard, Massachusetts
|
|
||||||
|
|
||||||
All Rights Reserved
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and distribute this software and its
|
|
||||||
documentation for any purpose and without fee is hereby granted,
|
|
||||||
provided that the above copyright notice appear in all copies and that
|
|
||||||
both that copyright notice and this permission notice appear in
|
|
||||||
supporting documentation, and that the name of Digital not be
|
|
||||||
used in advertising or publicity pertaining to distribution of the
|
|
||||||
software without specific, written prior permission.
|
|
||||||
|
|
||||||
DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
|
|
||||||
ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL
|
|
||||||
DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
|
|
||||||
ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
||||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
|
||||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
******************************************************************/
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
Copyright (c) 2007-2008 Trilead AG (http://www.trilead.com)
|
|
||||||
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions
|
|
||||||
are met:
|
|
||||||
|
|
||||||
a.) Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
b.) Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer in the
|
|
||||||
documentation and/or other materials provided with the distribution.
|
|
||||||
c.) Neither the name of Trilead nor the names of its contributors may
|
|
||||||
be used to endorse or promote products derived from this software
|
|
||||||
without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
||||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
||||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
||||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
||||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
||||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
Trilead SSH-2 for Java includes code that was written by Dr. Christian Plattner
|
|
||||||
during his PhD at ETH Zurich. The license states the following:
|
|
||||||
|
|
||||||
Copyright (c) 2005 - 2006 Swiss Federal Institute of Technology (ETH Zurich),
|
|
||||||
Department of Computer Science (http://www.inf.ethz.ch),
|
|
||||||
Christian Plattner. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions
|
|
||||||
are met:
|
|
||||||
|
|
||||||
a.) Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
b.) Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer in the
|
|
||||||
documentation and/or other materials provided with the distribution.
|
|
||||||
c.) Neither the name of ETH Zurich nor the names of its contributors may
|
|
||||||
be used to endorse or promote products derived from this software
|
|
||||||
without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
||||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
||||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
||||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
||||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
||||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
The Java implementations of the AES, Blowfish and 3DES ciphers have been
|
|
||||||
taken (and slightly modified) from the cryptography package released by
|
|
||||||
"The Legion Of The Bouncy Castle".
|
|
||||||
|
|
||||||
Their license states the following:
|
|
||||||
|
|
||||||
Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle
|
|
||||||
(http://www.bouncycastle.org)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
11
android/app/src/main/cpp/CMakeLists.txt
Normal file
11
android/app/src/main/cpp/CMakeLists.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.18.1)
|
||||||
|
project("native-socket")
|
||||||
|
|
||||||
|
# 添加库
|
||||||
|
add_library(native-socket SHARED native-socket.cpp)
|
||||||
|
|
||||||
|
# 链接日志库
|
||||||
|
find_library(log-lib log)
|
||||||
|
|
||||||
|
# 指定目标属性
|
||||||
|
target_link_libraries(native-socket ${log-lib})
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2022 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVNC_CLIENTEX_H
|
|
||||||
#define AVNC_CLIENTEX_H
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
#include "Cursor.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We attach some additional data to every rfbClient.
|
|
||||||
* ClientEx is used as wrapper for this data.
|
|
||||||
*/
|
|
||||||
struct ClientEx {
|
|
||||||
// Reference to managed `VncClient`
|
|
||||||
jobject managedClient;
|
|
||||||
|
|
||||||
// Although frame width & height are maintained in rfbClient, those values
|
|
||||||
// are modified before our MallocFrameBuffer callback is triggered, and
|
|
||||||
// we cannot protect them with a mutex. So we maintain the framebuffer
|
|
||||||
// size here, protected with fbMutex.
|
|
||||||
int fbRealWidth;
|
|
||||||
int fbRealHeight;
|
|
||||||
|
|
||||||
// Cursor data used for client-side cursor rendering
|
|
||||||
Cursor *cursor;
|
|
||||||
|
|
||||||
// Protects modification to framebuffer & cursor
|
|
||||||
MUTEX(mutex);
|
|
||||||
};
|
|
||||||
|
|
||||||
const int ClientExTag = 1;
|
|
||||||
|
|
||||||
ClientEx *getClientExtension(rfbClient *client) {
|
|
||||||
return (ClientEx *) rfbClientGetClientData(client, (void *) &ClientExTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setClientExtension(rfbClient *client, ClientEx *ex) {
|
|
||||||
rfbClientSetClientData(client, (void *) &ClientExTag, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns reference to managed `VncClient` associated with given rfbClient.
|
|
||||||
*/
|
|
||||||
jobject getManagedClient(rfbClient *client) {
|
|
||||||
return getClientExtension(client)->managedClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Associate given rfbClient & managed `VncClient`.
|
|
||||||
*/
|
|
||||||
void setManagedClient(rfbClient *client, jobject managedClient) {
|
|
||||||
getClientExtension(client)->managedClient = managedClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new ClientEx and assign it to given client.
|
|
||||||
*/
|
|
||||||
ClientEx *assignClientExtension(rfbClient *client) {
|
|
||||||
auto ex = (ClientEx *) malloc(sizeof(ClientEx));
|
|
||||||
if (ex) {
|
|
||||||
INIT_MUTEX(ex->mutex);
|
|
||||||
ex->cursor = nullptr;
|
|
||||||
setClientExtension(client, ex);
|
|
||||||
}
|
|
||||||
return ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Free all resources related to client extension.
|
|
||||||
*/
|
|
||||||
void freeClientExtension(rfbClient *client) {
|
|
||||||
auto ex = getClientExtension(client);
|
|
||||||
if (ex) {
|
|
||||||
TINI_MUTEX(ex->mutex);
|
|
||||||
freeCursor(ex->cursor);
|
|
||||||
free(ex);
|
|
||||||
setClientExtension(client, nullptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif //AVNC_CLIENTEX_H
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2022 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVNC_CURSOR_H
|
|
||||||
#define AVNC_CURSOR_H
|
|
||||||
|
|
||||||
|
|
||||||
/******************************************************************************
|
|
||||||
* Some servers (e.g TigerVNC) may not send the cursor immediately after
|
|
||||||
* connection. To provide consistent experience to users, we use a default
|
|
||||||
* cursor as fallback.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
const uint16_t DefaultCursorWidth = 10;
|
|
||||||
const uint16_t DefaultCursorHeight = 16;
|
|
||||||
const uint16_t DefaultCursorXHot = 1;
|
|
||||||
const uint16_t DefaultCursorYHot = 1;
|
|
||||||
|
|
||||||
const uint32_t DefaultCursorBuffer[DefaultCursorWidth * DefaultCursorHeight]
|
|
||||||
= {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x00FFFFFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF,
|
|
||||||
0x00FFFFFF, 0, 0, 0, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0, 0,
|
|
||||||
0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF,
|
|
||||||
0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF,
|
|
||||||
0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0x00FFFFFF,
|
|
||||||
0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0, 0,
|
|
||||||
0x00FFFFFF, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0, 0, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0, 0, 0, 0, 0, 0x00FFFFFF, 0x00FFFFFF, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0};
|
|
||||||
|
|
||||||
const uint8_t DefaultCursorMask[DefaultCursorWidth * DefaultCursorHeight]
|
|
||||||
= {1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0,
|
|
||||||
0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1,
|
|
||||||
1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
|
||||||
0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0,
|
|
||||||
0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0};
|
|
||||||
|
|
||||||
|
|
||||||
/******************************************************************************
|
|
||||||
* Cursor management
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for cursor information.
|
|
||||||
*
|
|
||||||
* rfbClient struct does not maintain all cursor related information inside it.
|
|
||||||
* Things like xHot, yHot are passed only via the cursor shape callback.
|
|
||||||
* This wrapper holds all information necessary to render the cursor.
|
|
||||||
*/
|
|
||||||
struct Cursor {
|
|
||||||
uint8_t *buffer;
|
|
||||||
uint8_t *mask;
|
|
||||||
uint8_t *scratchBuffer; //Used during rendering
|
|
||||||
uint16_t width;
|
|
||||||
uint16_t height;
|
|
||||||
uint16_t xHot;
|
|
||||||
uint16_t yHot;
|
|
||||||
};
|
|
||||||
|
|
||||||
//Only 4-byte pixels are currently supported
|
|
||||||
const uint8_t PixelBytes = 4;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new CursorData, initialized with default cursor info.
|
|
||||||
*/
|
|
||||||
Cursor *newCursor() {
|
|
||||||
auto cursor = (Cursor *) malloc(sizeof(Cursor));
|
|
||||||
if (cursor) {
|
|
||||||
cursor->buffer = (uint8_t *) DefaultCursorBuffer;
|
|
||||||
cursor->mask = (uint8_t *) DefaultCursorMask;
|
|
||||||
cursor->scratchBuffer = (uint8_t *) malloc(DefaultCursorWidth * DefaultCursorHeight * PixelBytes);
|
|
||||||
cursor->width = DefaultCursorWidth;
|
|
||||||
cursor->height = DefaultCursorHeight;
|
|
||||||
cursor->xHot = DefaultCursorXHot;
|
|
||||||
cursor->yHot = DefaultCursorYHot;
|
|
||||||
}
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
void freeCursorBuffers(Cursor *cursor) {
|
|
||||||
if (cursor) {
|
|
||||||
free(cursor->scratchBuffer);
|
|
||||||
if (cursor->buffer != (uint8_t *) DefaultCursorBuffer) free(cursor->buffer);
|
|
||||||
if (cursor->mask != (uint8_t *) DefaultCursorMask) free(cursor->mask);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void freeCursor(Cursor *cursor) {
|
|
||||||
freeCursorBuffers(cursor);
|
|
||||||
free(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateCursor(Cursor *cursor, uint8_t *buffer, uint8_t *mask, uint16_t width, uint16_t height,
|
|
||||||
uint16_t xHot, uint16_t yHot) {
|
|
||||||
|
|
||||||
freeCursorBuffers(cursor);
|
|
||||||
cursor->buffer = buffer;
|
|
||||||
cursor->mask = mask;
|
|
||||||
cursor->scratchBuffer = (uint8_t *) malloc(width * height * PixelBytes);
|
|
||||||
cursor->width = width;
|
|
||||||
cursor->height = height;
|
|
||||||
cursor->xHot = xHot;
|
|
||||||
cursor->yHot = yHot;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif //AVNC_CURSOR_H
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2022 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVNC_UTILITY_H
|
|
||||||
#define AVNC_UTILITY_H
|
|
||||||
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <netdb.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <android/log.h>
|
|
||||||
|
|
||||||
|
|
||||||
/******************************************************************************
|
|
||||||
* Utilities
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a native copy of the given jstring.
|
|
||||||
* Caller is responsible for releasing the memory.
|
|
||||||
*/
|
|
||||||
static char *getNativeStrCopy(JNIEnv *env, jstring jStr) {
|
|
||||||
const char *cStr = env->GetStringUTFChars(jStr, nullptr);
|
|
||||||
char *str = strdup(cStr);
|
|
||||||
env->ReleaseStringUTFChars(jStr, cStr);
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/******************************************************************************
|
|
||||||
* Logging
|
|
||||||
*****************************************************************************/
|
|
||||||
const char *LOG_TAG = "NativeVnc";
|
|
||||||
|
|
||||||
void log_info(const char *fmt, ...) {
|
|
||||||
va_list args;
|
|
||||||
va_start(args, fmt);
|
|
||||||
__android_log_vprint(ANDROID_LOG_INFO, LOG_TAG, fmt, args);
|
|
||||||
va_end(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
void log_error(const char *fmt, ...) {
|
|
||||||
va_list args;
|
|
||||||
va_start(args, fmt);
|
|
||||||
__android_log_vprint(ANDROID_LOG_ERROR, LOG_TAG, fmt, args);
|
|
||||||
va_end(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts given errno value to its description.
|
|
||||||
*/
|
|
||||||
static const char *errnoToStr(int e) {
|
|
||||||
|
|
||||||
// LibVNC is patched to report `getaddrinfo` errors as negative 'errno'.
|
|
||||||
// See ConnectClientToTcpAddr6WithTimeout() in sockets.c
|
|
||||||
if (e < -1000) {
|
|
||||||
return gai_strerror((-e) - 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (e) {
|
|
||||||
case ENETDOWN:
|
|
||||||
case ENETRESET:
|
|
||||||
case ENETUNREACH:
|
|
||||||
case ECONNABORTED:
|
|
||||||
case EHOSTDOWN:
|
|
||||||
case EHOSTUNREACH:
|
|
||||||
case ETIMEDOUT:
|
|
||||||
case ENOMEM:
|
|
||||||
case EPROTO:
|
|
||||||
case EIO:
|
|
||||||
return strerror(e);
|
|
||||||
|
|
||||||
case ECONNREFUSED:
|
|
||||||
return "Connection refused! Server may be down or running on different port";
|
|
||||||
|
|
||||||
case ECONNRESET:
|
|
||||||
return "Connection closed by server";
|
|
||||||
|
|
||||||
case EACCES:
|
|
||||||
return "Authentication failed";
|
|
||||||
|
|
||||||
default:
|
|
||||||
// In this case we don't want to display errno description to user
|
|
||||||
// because it is more likely to be misleading (e.g. EINTR, EAGAIN).
|
|
||||||
// BUT add it to logs in case LibVNC didn't.
|
|
||||||
log_error("errnoToStr: (%d %s)", errno, strerror(errno));
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif //AVNC_UTILITY_H
|
|
||||||
78
android/app/src/main/cpp/native-socket.cpp
Normal file
78
android/app/src/main/cpp/native-socket.cpp
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#include <jni.h>
|
||||||
|
#include <string>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <android/log.h>
|
||||||
|
#include <cerrno>
|
||||||
|
|
||||||
|
#define LOG_TAG "NativeAudio"
|
||||||
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
||||||
|
int server_fd = -1;
|
||||||
|
int client_fd = -1;
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jint JNICALL
|
||||||
|
Java_com_example_tiny_1computer_AudioStream_nativeInit(JNIEnv *env, jobject thiz, jstring path) {
|
||||||
|
const char *socket_path = env->GetStringUTFChars(path, 0);
|
||||||
|
|
||||||
|
server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
|
if (server_fd == -1) {
|
||||||
|
LOGE("Socket creation failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sockaddr_un addr;
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sun_family = AF_UNIX;
|
||||||
|
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
|
||||||
|
unlink(socket_path); // Remove existing file if any
|
||||||
|
|
||||||
|
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||||
|
LOGE("Bind failed: %s", strerror(errno));
|
||||||
|
close(server_fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen(server_fd, 1) == -1) {
|
||||||
|
LOGE("Listen failed");
|
||||||
|
close(server_fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
env->ReleaseStringUTFChars(path, socket_path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jint JNICALL
|
||||||
|
Java_com_example_tiny_1computer_AudioStream_nativeAccept(JNIEnv *env, jobject thiz) {
|
||||||
|
if (server_fd == -1) return -1;
|
||||||
|
// Blocks here until Linux connects
|
||||||
|
client_fd = accept(server_fd, NULL, NULL);
|
||||||
|
if (client_fd == -1) {
|
||||||
|
LOGE("Accept failed: %s", strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jint JNICALL
|
||||||
|
Java_com_example_tiny_1computer_AudioStream_nativeSend(JNIEnv *env, jobject thiz, jbyteArray data, jint size) {
|
||||||
|
if (client_fd == -1) return -1;
|
||||||
|
|
||||||
|
jbyte *buffer = env->GetByteArrayElements(data, NULL);
|
||||||
|
ssize_t sent = write(client_fd, buffer, size);
|
||||||
|
env->ReleaseByteArrayElements(data, buffer, JNI_ABORT);
|
||||||
|
|
||||||
|
if (sent == -1 && errno != EAGAIN) {
|
||||||
|
LOGE("Write failed (Broken Pipe?)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_com_example_tiny_1computer_AudioStream_nativeClose(JNIEnv *env, jobject thiz) {
|
||||||
|
if (client_fd != -1) { close(client_fd); client_fd = -1; }
|
||||||
|
if (server_fd != -1) { close(server_fd); server_fd = -1; }
|
||||||
|
}
|
||||||
@@ -1,561 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
#include <GLES2/gl2.h>
|
|
||||||
#include <rfb/rfbclient.h>
|
|
||||||
|
|
||||||
#include "ClientEx.h"
|
|
||||||
#include "Utility.h"
|
|
||||||
|
|
||||||
|
|
||||||
/******************************************************************************
|
|
||||||
* Library Initialization
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
struct JniContext {
|
|
||||||
JavaVM *vm; //JVM Instance
|
|
||||||
jclass managedCls; //Managed `VncClient` class
|
|
||||||
jmethodID cbFramebufferUpdated; //Cached reference to managed callback
|
|
||||||
|
|
||||||
JNIEnv *getEnv() const {
|
|
||||||
JNIEnv *env = nullptr;
|
|
||||||
|
|
||||||
if (vm != nullptr && vm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_OK)
|
|
||||||
return env;
|
|
||||||
|
|
||||||
return nullptr; //Should not happen
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static JniContext context{};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when our library is loaded.
|
|
||||||
*/
|
|
||||||
JNIEXPORT jint
|
|
||||||
JNI_OnLoad(JavaVM *vm, void *unused) {
|
|
||||||
context.vm = vm;
|
|
||||||
|
|
||||||
if (context.getEnv() == nullptr)
|
|
||||||
return JNI_ERR;
|
|
||||||
|
|
||||||
return JNI_VERSION_1_6;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void
|
|
||||||
JNI_OnUnload(JavaVM *vm, void *reserved) {
|
|
||||||
if (context.managedCls != nullptr)
|
|
||||||
context.getEnv()->DeleteGlobalRef(context.managedCls);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_initLibrary(JNIEnv *env, jclass clazz) {
|
|
||||||
context.managedCls = (jclass) env->NewGlobalRef(clazz);
|
|
||||||
context.cbFramebufferUpdated = env->GetMethodID(clazz, "cbFinishedFrameBufferUpdate", "()V");
|
|
||||||
//TODO: Cache more method IDs so we don't have to repeatedly search them
|
|
||||||
|
|
||||||
rfbClientLog = &log_info;
|
|
||||||
rfbClientErr = &log_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/******************************************************************************
|
|
||||||
* rfbClient Callbacks
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
static char *onGetPassword(rfbClient *client) {
|
|
||||||
auto obj = getManagedClient(client);
|
|
||||||
auto env = context.getEnv();
|
|
||||||
auto cls = context.managedCls;
|
|
||||||
|
|
||||||
auto mid = env->GetMethodID(cls, "cbGetPassword", "()Ljava/lang/String;");
|
|
||||||
auto jPassword = (jstring) env->CallObjectMethod(obj, mid);
|
|
||||||
|
|
||||||
return getNativeStrCopy(env, jPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
static rfbCredential *onGetCredential(rfbClient *client, int credentialType) {
|
|
||||||
if (credentialType != rfbCredentialTypeUser) {
|
|
||||||
//Only user credentials (i.e. username & password) are currently supported
|
|
||||||
rfbClientErr("Unsupported credential type requested");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto obj = getManagedClient(client);
|
|
||||||
auto env = context.getEnv();
|
|
||||||
auto cls = context.managedCls;
|
|
||||||
|
|
||||||
//Retrieve credentials
|
|
||||||
jmethodID mid = env->GetMethodID(cls, "cbGetCredential",
|
|
||||||
"()Lcom/gaurav/avnc/vnc/UserCredential;");
|
|
||||||
jobject jCredential = env->CallObjectMethod(obj, mid);
|
|
||||||
if (jCredential == nullptr) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Extract username & password
|
|
||||||
auto jCredentialCls = env->GetObjectClass(jCredential);
|
|
||||||
auto usernameField = env->GetFieldID(jCredentialCls, "username", "Ljava/lang/String;");
|
|
||||||
auto jUsername = env->GetObjectField(jCredential, usernameField);
|
|
||||||
|
|
||||||
auto passwordField = env->GetFieldID(jCredentialCls, "password", "Ljava/lang/String;");
|
|
||||||
auto jPassword = env->GetObjectField(jCredential, passwordField);
|
|
||||||
|
|
||||||
//Create native rfbCredential
|
|
||||||
auto credential = (rfbCredential *) malloc(sizeof(rfbCredential));
|
|
||||||
credential->userCredential.username = getNativeStrCopy(env, (jstring) jUsername);
|
|
||||||
credential->userCredential.password = getNativeStrCopy(env, (jstring) jPassword);
|
|
||||||
|
|
||||||
return credential;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onBell(rfbClient *client) {
|
|
||||||
auto obj = getManagedClient(client);
|
|
||||||
auto env = context.getEnv();
|
|
||||||
auto cls = context.managedCls;
|
|
||||||
|
|
||||||
jmethodID mid = env->GetMethodID(cls, "cbBell", "()V");
|
|
||||||
env->CallVoidMethod(obj, mid);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onGotXCutText(rfbClient *client, const char *text, int len, bool is_utf8) {
|
|
||||||
auto obj = getManagedClient(client);
|
|
||||||
auto env = context.getEnv();
|
|
||||||
auto cls = context.managedCls;
|
|
||||||
|
|
||||||
jmethodID mid = env->GetMethodID(cls, "cbGotXCutText", "([BZ)V");
|
|
||||||
jbyteArray bytes = env->NewByteArray(len);
|
|
||||||
env->SetByteArrayRegion(bytes, 0, len, reinterpret_cast<const jbyte *>(text));
|
|
||||||
env->CallVoidMethod(obj, mid, bytes, is_utf8);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onGotXCutTextLatin1(rfbClient *client, const char *text, int len) {
|
|
||||||
onGotXCutText(client, text, len, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onGotXCutTextUTF8(rfbClient *client, const char *text, int len) {
|
|
||||||
onGotXCutText(client, text, len, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static rfbBool onHandleCursorPos(rfbClient *client, int x, int y) {
|
|
||||||
auto obj = getManagedClient(client);
|
|
||||||
auto env = context.getEnv();
|
|
||||||
auto cls = context.managedCls;
|
|
||||||
|
|
||||||
jmethodID mid = env->GetMethodID(cls, "cbHandleCursorPos", "(II)V");
|
|
||||||
env->CallVoidMethod(obj, mid, x, y);
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onFinishedFrameBufferUpdate(rfbClient *client) {
|
|
||||||
auto obj = getManagedClient(client);
|
|
||||||
auto env = context.getEnv();
|
|
||||||
|
|
||||||
env->CallVoidMethod(obj, context.cbFramebufferUpdated);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We need to use our own allocator to know when frame size has changed.
|
|
||||||
* and to acquire framebuffer lock during modification.
|
|
||||||
*/
|
|
||||||
static rfbBool onMallocFrameBuffer(rfbClient *client) {
|
|
||||||
|
|
||||||
const auto width = client->width;
|
|
||||||
const auto height = client->height;
|
|
||||||
const auto requestedSize = (uint64_t) width * height * client->format.bitsPerPixel / 8;
|
|
||||||
|
|
||||||
if (requestedSize >= SIZE_MAX) {
|
|
||||||
rfbClientErr("CRITICAL: cannot allocate frameBuffer, requested size is too large\n");
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto allocSize = (size_t) requestedSize;
|
|
||||||
auto ex = getClientExtension(client);
|
|
||||||
|
|
||||||
LOCK(ex->mutex);
|
|
||||||
{
|
|
||||||
|
|
||||||
if (client->frameBuffer)
|
|
||||||
free(client->frameBuffer);
|
|
||||||
|
|
||||||
client->frameBuffer = static_cast<uint8_t *>(malloc(allocSize));
|
|
||||||
|
|
||||||
if (client->frameBuffer) {
|
|
||||||
ex->fbRealWidth = width;
|
|
||||||
ex->fbRealHeight = height;
|
|
||||||
memset(client->frameBuffer, 0, allocSize); //Clear any garbage
|
|
||||||
} else {
|
|
||||||
ex->fbRealWidth = 0;
|
|
||||||
ex->fbRealHeight = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UNLOCK(ex->mutex);
|
|
||||||
|
|
||||||
if (client->frameBuffer == nullptr) {
|
|
||||||
rfbClientErr("CRITICAL: frameBuffer allocation failed\n");
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto obj = getManagedClient(client);
|
|
||||||
auto env = context.getEnv();
|
|
||||||
auto cls = context.managedCls;
|
|
||||||
|
|
||||||
auto mid = env->GetMethodID(cls, "cbFramebufferSizeChanged", "(II)V");
|
|
||||||
env->CallVoidMethod(obj, mid, width, height);
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onGotCursorShape(rfbClient *client, int xHot, int yHot, int width, int height, int bytesPerPixel) {
|
|
||||||
auto ex = getClientExtension(client);
|
|
||||||
|
|
||||||
LOCK(ex->mutex);
|
|
||||||
|
|
||||||
//Steel buffers from rfbClient
|
|
||||||
updateCursor(ex->cursor, client->rcSource, client->rcMask, (uint16_t) width, (uint16_t) height,
|
|
||||||
(uint16_t) xHot, (uint16_t) yHot);
|
|
||||||
client->rcSource = NULL;
|
|
||||||
client->rcMask = NULL;
|
|
||||||
|
|
||||||
UNLOCK(ex->mutex);
|
|
||||||
|
|
||||||
//Fake framebuffer update to trigger rendering
|
|
||||||
onFinishedFrameBufferUpdate(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hooks callbacks to rfbClient.
|
|
||||||
*/
|
|
||||||
static void setCallbacks(rfbClient *client) {
|
|
||||||
client->GetPassword = onGetPassword;
|
|
||||||
client->GetCredential = onGetCredential;
|
|
||||||
client->Bell = onBell;
|
|
||||||
client->GotXCutText = onGotXCutTextLatin1;
|
|
||||||
client->GotXCutTextUTF8 = onGotXCutTextUTF8;
|
|
||||||
client->HandleCursorPos = onHandleCursorPos;
|
|
||||||
client->FinishedFrameBufferUpdate = onFinishedFrameBufferUpdate;
|
|
||||||
client->MallocFrameBuffer = onMallocFrameBuffer;
|
|
||||||
client->GotCursorShape = onGotCursorShape;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/******************************************************************************
|
|
||||||
* Native method Implementation
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jlong JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeClientCreate(JNIEnv *env, jobject thiz) {
|
|
||||||
rfbClient *client = rfbGetClient(8, 3, 4);
|
|
||||||
if (client == nullptr)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
if (!assignClientExtension(client))
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
setCallbacks(client);
|
|
||||||
client->canHandleNewFBSize = TRUE;
|
|
||||||
|
|
||||||
//Attach reference to managed object
|
|
||||||
auto obj = env->NewGlobalRef(thiz);
|
|
||||||
setManagedClient(client, obj);
|
|
||||||
|
|
||||||
return (jlong) client;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeConfigure(JNIEnv *env, jobject thiz, jlong client_ptr,
|
|
||||||
jint securityType, jboolean use_local_cursor, jint image_quality,
|
|
||||||
jboolean use_raw_encoding) {
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
|
|
||||||
// 0 means all auth types
|
|
||||||
if (securityType != 0) {
|
|
||||||
uint32_t auth[1] = {static_cast<uint32_t>(securityType)};
|
|
||||||
SetClientAuthSchemes(client, auth, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (use_local_cursor) {
|
|
||||||
client->appData.useRemoteCursor = TRUE;
|
|
||||||
getClientExtension(client)->cursor = newCursor();
|
|
||||||
}
|
|
||||||
|
|
||||||
client->appData.qualityLevel = image_quality;
|
|
||||||
if (use_raw_encoding)
|
|
||||||
client->appData.encodingsString = "raw";
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeSetDest(JNIEnv *env, jobject thiz, jlong client_ptr,
|
|
||||||
jstring host, jint port) {
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
client->destHost = getNativeStrCopy(env, host);
|
|
||||||
client->destPort = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeInit(JNIEnv *env, jobject thiz, jlong client_ptr,
|
|
||||||
jstring host, jint port) {
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
|
|
||||||
client->serverHost = getNativeStrCopy(env, host);
|
|
||||||
client->serverPort = port < 100 ? port + 5900 : port;
|
|
||||||
|
|
||||||
if (rfbInitClient(client, nullptr, nullptr)) {
|
|
||||||
return JNI_TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JNI_FALSE;
|
|
||||||
|
|
||||||
}
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeIsServerMacOS(JNIEnv *env, jobject thiz, jlong client_ptr) {
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
return client->serverMajor == 3 && client->serverMinor == 889;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeCleanup(JNIEnv *env, jobject thiz,
|
|
||||||
jlong client_ptr) {
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
|
|
||||||
if (client->frameBuffer) {
|
|
||||||
free(client->frameBuffer);
|
|
||||||
client->frameBuffer = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto managedClient = getManagedClient(client);
|
|
||||||
env->DeleteGlobalRef(managedClient);
|
|
||||||
|
|
||||||
freeClientExtension(client);
|
|
||||||
rfbClientCleanup(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeProcessServerMessage(JNIEnv *env, jobject thiz,
|
|
||||||
jlong client_ptr,
|
|
||||||
jint u_sec_timeout) {
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
|
|
||||||
auto waitResult = WaitForMessage(client, static_cast<unsigned int>(u_sec_timeout));
|
|
||||||
|
|
||||||
if (waitResult == 0) // Timeout
|
|
||||||
return JNI_TRUE;
|
|
||||||
|
|
||||||
if (waitResult > 0 && HandleRFBServerMessage(client))
|
|
||||||
return JNI_TRUE;
|
|
||||||
|
|
||||||
return JNI_FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jstring JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeGetLastErrorStr(JNIEnv *env, jobject thiz) {
|
|
||||||
auto str = errnoToStr(errno);
|
|
||||||
return env->NewStringUTF(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeSendKeyEvent(JNIEnv *env, jobject thiz, jlong client_ptr,
|
|
||||||
jint key_sym, jint xt_code, jboolean is_down) {
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
rfbBool down = is_down ? TRUE : FALSE;
|
|
||||||
|
|
||||||
if (xt_code > 0 && SendExtendedKeyEvent(client, key_sym, xt_code, down))
|
|
||||||
return JNI_TRUE;
|
|
||||||
else
|
|
||||||
return SendKeyEvent(client, key_sym, down);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeSendPointerEvent(JNIEnv *env, jobject thiz, jlong client_ptr, jint x, jint y,
|
|
||||||
jint mask) {
|
|
||||||
return (jboolean) SendPointerEvent((rfbClient *) client_ptr, x, y, mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeSendCutText(JNIEnv *env, jobject thiz, jlong client_ptr, jbyteArray bytes,
|
|
||||||
jboolean is_utf8) {
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
auto textBuffer = env->GetByteArrayElements(bytes, nullptr);
|
|
||||||
auto textLen = env->GetArrayLength(bytes);
|
|
||||||
auto textChars = reinterpret_cast<char *>(textBuffer);
|
|
||||||
|
|
||||||
rfbBool result = is_utf8
|
|
||||||
? SendClientCutTextUTF8(client, textChars, textLen)
|
|
||||||
: SendClientCutText(client, textChars, textLen);
|
|
||||||
|
|
||||||
env->ReleaseByteArrayElements(bytes, textBuffer, JNI_ABORT);
|
|
||||||
return (jboolean) result;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeIsUTF8CutTextSupported(JNIEnv *env, jobject thiz, jlong client_ptr) {
|
|
||||||
return (jboolean) (((rfbClient *) client_ptr)->extendedClipboardServerCapabilities != 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeSetDesktopSize(JNIEnv *env, jobject thiz, jlong client_ptr, jint width,
|
|
||||||
jint height) {
|
|
||||||
return (jboolean) SendExtDesktopSize((rfbClient *) client_ptr, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeRefreshFrameBuffer(JNIEnv *env, jobject thiz, jlong clientPtr) {
|
|
||||||
auto client = (rfbClient *) clientPtr;
|
|
||||||
return (jboolean) SendFramebufferUpdateRequest(client, 0, 0, client->width, client->height, TRUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeSetAutomaticFramebufferUpdates(JNIEnv *env, jobject thiz, jlong client_ptr,
|
|
||||||
jboolean enabled) {
|
|
||||||
auto client = ((rfbClient *) client_ptr);
|
|
||||||
client->automaticUpdateRequests = enabled ? TRUE : FALSE;
|
|
||||||
if (enabled) SendIncrementalFramebufferUpdateRequest(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jstring JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeGetDesktopName(JNIEnv *env, jobject thiz, jlong client_ptr) {
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
return env->NewStringUTF(client->desktopName ? client->desktopName : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jint JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeGetWidth(JNIEnv *env, jobject thiz, jlong client_ptr) {
|
|
||||||
return ((rfbClient *) client_ptr)->width;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jint JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeGetHeight(JNIEnv *env, jobject thiz, jlong client_ptr) {
|
|
||||||
return ((rfbClient *) client_ptr)->height;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeIsEncrypted(JNIEnv *env, jobject thiz, jlong client_ptr) {
|
|
||||||
return static_cast<jboolean>(((rfbClient *) client_ptr)->tlsSession ? JNI_TRUE : JNI_FALSE);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeUploadFrameTexture(JNIEnv *env, jobject thiz,
|
|
||||||
jlong client_ptr) {
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
auto ex = getClientExtension(client);
|
|
||||||
|
|
||||||
LOCK(ex->mutex);
|
|
||||||
|
|
||||||
if (client->frameBuffer) {
|
|
||||||
glTexImage2D(GL_TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
GL_RGBA,
|
|
||||||
ex->fbRealWidth,
|
|
||||||
ex->fbRealHeight,
|
|
||||||
0,
|
|
||||||
GL_RGBA,
|
|
||||||
GL_UNSIGNED_BYTE,
|
|
||||||
client->frameBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
UNLOCK(ex->mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_com_gaurav_avnc_vnc_VncClient_nativeUploadCursor(JNIEnv *env, jobject thiz, jlong client_ptr, jint px, jint py) {
|
|
||||||
|
|
||||||
auto client = (rfbClient *) client_ptr;
|
|
||||||
auto ex = getClientExtension(client);
|
|
||||||
auto cursor = ex->cursor;
|
|
||||||
|
|
||||||
if (!cursor)
|
|
||||||
return;
|
|
||||||
|
|
||||||
//Current algo for cursor rendering is slightly weird. Main issue is that
|
|
||||||
//glTexSubImage2D() does not perform any composition with target texture.
|
|
||||||
//So, we have to manually blend transparent/invalid pixels of the cursor
|
|
||||||
//with corresponding pixels from framebuffer. scratchBuffer is used for
|
|
||||||
//this composition.
|
|
||||||
|
|
||||||
LOCK(ex->mutex);
|
|
||||||
|
|
||||||
//Effective cursor position in framebuffer
|
|
||||||
int32_t fbCursorX = px - cursor->xHot;
|
|
||||||
int32_t fbCursorY = py - cursor->yHot;
|
|
||||||
|
|
||||||
//Rectangular portion of the framebuffer to be updated.
|
|
||||||
//Cursor can overflow outside the framebuffer if moved near the edges,
|
|
||||||
//but glTexSubImage2D() doesn't allow values outside target texture,
|
|
||||||
//so we need to only update the intersection of framebuffer & cursor.
|
|
||||||
int32_t left = -1, top = -1, right = -1, bottom = -1;
|
|
||||||
|
|
||||||
auto fb = (uint32_t *) client->frameBuffer;
|
|
||||||
auto buffer = (uint32_t *) cursor->buffer;
|
|
||||||
auto scratch = (uint32_t *) cursor->scratchBuffer;
|
|
||||||
auto mask = cursor->mask;
|
|
||||||
|
|
||||||
//Scratch buffer index
|
|
||||||
int32_t z = 0;
|
|
||||||
|
|
||||||
for (int32_t y = 0; y < cursor->height; ++y) {
|
|
||||||
for (int32_t x = 0; x < cursor->width; ++x) {
|
|
||||||
|
|
||||||
//Corresponding pixel in framebuffer
|
|
||||||
auto fbX = fbCursorX + x;
|
|
||||||
auto fbY = fbCursorY + y;
|
|
||||||
|
|
||||||
if (fbX >= 0 && fbX < ex->fbRealWidth && fbY >= 0 && fbY < ex->fbRealHeight) {
|
|
||||||
auto isValidPixel = mask[y * cursor->width + x];
|
|
||||||
if (isValidPixel)
|
|
||||||
scratch[z++] = buffer[y * cursor->width + x];
|
|
||||||
else
|
|
||||||
scratch[z++] = fb[fbY * ex->fbRealWidth + fbX];
|
|
||||||
|
|
||||||
if (left == -1 && top == -1) {
|
|
||||||
left = fbX;
|
|
||||||
top = fbY;
|
|
||||||
}
|
|
||||||
right = fbX;
|
|
||||||
bottom = fbY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (left >= 0 && top >= 0)
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
right - left + 1,
|
|
||||||
bottom - top + 1,
|
|
||||||
GL_RGBA,
|
|
||||||
GL_UNSIGNED_BYTE,
|
|
||||||
scratch);
|
|
||||||
|
|
||||||
UNLOCK(ex->mutex);
|
|
||||||
}
|
|
||||||
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -67,7 +67,7 @@ public class TinyDocumentsProvider extends DocumentsProvider {
|
|||||||
@Override
|
@Override
|
||||||
public Cursor queryRoots(String[] projection) {
|
public Cursor queryRoots(String[] projection) {
|
||||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
|
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
|
||||||
final String applicationName = "小小电脑";
|
final String applicationName = getContext().getString(R.string.tc_app_name);
|
||||||
final File BASE_DIR = new File(getContext().getFilesDir(), "containers");
|
final File BASE_DIR = new File(getContext().getFilesDir(), "containers");
|
||||||
final MatrixCursor.RowBuilder row = result.newRow();
|
final MatrixCursor.RowBuilder row = result.newRow();
|
||||||
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.example.tiny_computer
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.media.AudioFormat
|
||||||
|
import android.media.AudioRecord
|
||||||
|
import android.media.MediaRecorder
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
object AudioStream {
|
||||||
|
init {
|
||||||
|
System.loadLibrary("native-socket")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isStreaming = false
|
||||||
|
private var recordingThread: Thread? = null
|
||||||
|
|
||||||
|
// Native functions
|
||||||
|
private external fun nativeInit(path: String): Int
|
||||||
|
private external fun nativeAccept(): Int
|
||||||
|
private external fun nativeSend(data: ByteArray, size: Int): Int
|
||||||
|
private external fun nativeClose()
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission") // Ensure RECORD_AUDIO is granted in Manifest
|
||||||
|
fun startStreaming(path: String) {
|
||||||
|
if (isStreaming) return
|
||||||
|
isStreaming = true
|
||||||
|
|
||||||
|
recordingThread = Thread {
|
||||||
|
// 1. Initialize Socket Server
|
||||||
|
if (nativeInit(path) < 0) {
|
||||||
|
Log.e("AudioStream", "Failed to bind socket")
|
||||||
|
isStreaming = false
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Wait for Linux client to connect (Blocking)
|
||||||
|
Log.d("AudioStream", "Waiting for connection on $path...")
|
||||||
|
if (nativeAccept() < 0) {
|
||||||
|
Log.e("AudioStream", "Accept failed")
|
||||||
|
isStreaming = false
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
Log.d("AudioStream", "Client connected!")
|
||||||
|
|
||||||
|
// 3. Setup AudioRecord
|
||||||
|
val sampleRate = 44100
|
||||||
|
val bufferSize = AudioRecord.getMinBufferSize(
|
||||||
|
sampleRate,
|
||||||
|
AudioFormat.CHANNEL_IN_MONO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT
|
||||||
|
)
|
||||||
|
|
||||||
|
val recorder = AudioRecord(
|
||||||
|
MediaRecorder.AudioSource.MIC,
|
||||||
|
sampleRate,
|
||||||
|
AudioFormat.CHANNEL_IN_MONO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize
|
||||||
|
)
|
||||||
|
|
||||||
|
val data = ByteArray(bufferSize)
|
||||||
|
recorder.startRecording()
|
||||||
|
|
||||||
|
val discardMillis = 5000 // 丢弃前5秒
|
||||||
|
val discardBytes = (sampleRate * 2 * discardMillis / 1000).toInt() // 16bit = 2字节
|
||||||
|
var bytesRead = 0
|
||||||
|
|
||||||
|
// 先读取并丢弃初始数据
|
||||||
|
while (bytesRead < discardBytes && isStreaming) {
|
||||||
|
val readBytes = recorder.read(data, 0, minOf(bufferSize, discardBytes - bytesRead))
|
||||||
|
if (readBytes > 0) {
|
||||||
|
bytesRead += readBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Streaming Loop
|
||||||
|
while (isStreaming) {
|
||||||
|
val readBytes = recorder.read(data, 0, bufferSize)
|
||||||
|
if (readBytes > 0) {
|
||||||
|
val sent = nativeSend(data, readBytes)
|
||||||
|
if (sent < 0) break // Socket broken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
recorder.stop()
|
||||||
|
recorder.release()
|
||||||
|
nativeClose()
|
||||||
|
}
|
||||||
|
recordingThread?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopStreaming() {
|
||||||
|
isStreaming = false
|
||||||
|
nativeClose() // Unblocks the native Accept/Send if hung
|
||||||
|
recordingThread?.join()
|
||||||
|
recordingThread = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +1,37 @@
|
|||||||
package com.example.tiny_computer
|
package com.example.tiny_computer
|
||||||
|
|
||||||
|
import android.system.Os.setenv
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.gaurav.avnc.util.AppPreferences
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
class MainActivity: FlutterActivity() {
|
||||||
|
|
||||||
@Keep
|
|
||||||
lateinit var prefs: AppPreferences
|
|
||||||
|
|
||||||
private fun updateNightMode(theme: String) {
|
|
||||||
val nightMode = when (theme) {
|
|
||||||
"light" -> AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
||||||
}
|
|
||||||
AppCompatDelegate.setDefaultNightMode(nightMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
prefs = AppPreferences(this)
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "android").setMethodCallHandler {
|
||||||
prefs.ui.theme.observeForever { updateNightMode(it) }
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "avnc").setMethodCallHandler {
|
|
||||||
// 注册通道并设置方法调用处理器
|
// 注册通道并设置方法调用处理器
|
||||||
call, result ->
|
call, result ->
|
||||||
// 判断方法名
|
// 判断方法名
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"launchUsingUri" -> {
|
"launchSignal9Page" -> {
|
||||||
com.gaurav.avnc.ui.vnc.startVncActivity(this, com.gaurav.avnc.vnc.VncUri(call.argument("vncUri")!!))
|
startActivity(Intent(this, Signal9Activity::class.java))
|
||||||
result.success(0)
|
result.success(0)
|
||||||
}
|
}
|
||||||
"launchPrefsPage" -> {
|
"getNativeLibraryPath" -> {
|
||||||
startActivity(Intent(this, com.gaurav.avnc.ui.prefs.PrefsActivity::class.java))
|
result.success(getApplicationInfo().nativeLibraryDir)
|
||||||
result.success(0)
|
|
||||||
}
|
}
|
||||||
"launchAboutPage" -> {
|
"startStreaming" -> {
|
||||||
startActivity(Intent(this, com.gaurav.avnc.ui.about.AboutActivity::class.java))
|
AudioStream.startStreaming(call.argument("path")!!)
|
||||||
result.success(0)
|
}
|
||||||
|
"stopStreaming" -> {
|
||||||
|
AudioStream.stopStreaming()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// 不支持的方法名
|
// 不支持的方法名
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.example.tiny_computer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import io.flutter.app.FlutterApplication
|
||||||
|
import me.weishu.reflection.Reflection
|
||||||
|
|
||||||
|
class MainApplication : FlutterApplication() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
DynamicColors.applyToActivitiesIfAvailable(this@MainApplication)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
Reflection.unseal(base)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.example.tiny_computer
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.example.tiny_computer.databinding.ActivitySignal9Binding
|
||||||
|
|
||||||
|
class Signal9Activity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivitySignal9Binding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivitySignal9Binding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
// 设置状态栏和导航栏颜色匹配蓝屏背景
|
||||||
|
window.statusBarColor = ContextCompat.getColor(this, R.color.tc_s9a_blue_screen_blue)
|
||||||
|
window.navigationBarColor = ContextCompat.getColor(this, R.color.tc_s9a_blue_screen_blue)
|
||||||
|
|
||||||
|
setupContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupContent() {
|
||||||
|
// 设置错误信息
|
||||||
|
binding.errorDetails.text = getString(R.string.tc_s9a_error_message)
|
||||||
|
|
||||||
|
// 根据Android版本显示不同的解决方案
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
// Android 14以下版本
|
||||||
|
binding.preAndroid14Layout.isVisible = true
|
||||||
|
binding.solutionIntro.text = getString(R.string.tc_s9a_solution_intro)
|
||||||
|
binding.solutionAlternative.text = getString(R.string.tc_s9a_solution_alternative)
|
||||||
|
binding.toolButton.text = getString(R.string.tc_s9a_tool_button)
|
||||||
|
binding.tutorialButton.text = getString(R.string.tc_s9a_tutorial_button)
|
||||||
|
|
||||||
|
binding.toolButton.setOnClickListener {
|
||||||
|
openBrowserLink("https://www.vmos.cn/zhushou.htm")
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tutorialButton.setOnClickListener {
|
||||||
|
openBrowserLink("https://gitee.com/caten/tc-hints/blob/master/pool/signal9fix.md")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Android 14及以上版本
|
||||||
|
binding.solutionAndroid14.isVisible = true
|
||||||
|
binding.solutionAndroid14.text = getString(R.string.tc_s9a_solution_android14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openBrowserLink(url: String) {
|
||||||
|
if (url.isNotEmpty()) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
// 如果URL为空,则不执行任何操作(等待后续补充链接)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2022 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.model
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic wrapper for login information.
|
|
||||||
* This can be used to hold different [Type]s of credentials.
|
|
||||||
*/
|
|
||||||
data class LoginInfo(
|
|
||||||
var name: String = "", // Profile name
|
|
||||||
var host: String = "",
|
|
||||||
var username: String = "",
|
|
||||||
var password: String = "",
|
|
||||||
) {
|
|
||||||
enum class Type {
|
|
||||||
VNC_PASSWORD,
|
|
||||||
VNC_CREDENTIAL, // Username & Password
|
|
||||||
SSH_PASSWORD,
|
|
||||||
SSH_KEY_PASSWORD
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.room.ColumnInfo
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class holds connection configuration of a remote VNC server.
|
|
||||||
*
|
|
||||||
* Some fields remain unused until that feature is implemented.
|
|
||||||
*/
|
|
||||||
@Parcelize
|
|
||||||
@Serializable
|
|
||||||
@Entity(tableName = "profiles")
|
|
||||||
data class ServerProfile(
|
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
|
||||||
var ID: Long = 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Descriptive name of the server (e.g. 'Kitchen PC').
|
|
||||||
*/
|
|
||||||
var name: String = "",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internet address of the server (without port number).
|
|
||||||
* This can be hostname or IP address.
|
|
||||||
*/
|
|
||||||
var host: String = "",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Port number of the server.
|
|
||||||
*/
|
|
||||||
var port: Int = 5900,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Username used for authentication.
|
|
||||||
*/
|
|
||||||
var username: String = "",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Password used for authentication.
|
|
||||||
* Note: Username & password may not be used for all security types.
|
|
||||||
*/
|
|
||||||
var password: String = "",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Security type to use when connecting to this server (e.g. VncAuth).
|
|
||||||
* 0 enables all supported types.
|
|
||||||
*/
|
|
||||||
var securityType: Int = 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transport channel to be used for communicating with the server.
|
|
||||||
* e.g. TCP, SSH Tunnel
|
|
||||||
*/
|
|
||||||
var channelType: Int = CHANNEL_TCP,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies the color level of received frames.
|
|
||||||
* This value determines the pixel-format used for framebuffer.
|
|
||||||
*/
|
|
||||||
var colorLevel: Int = 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies the image quality of the frames.
|
|
||||||
* This mainly affects the compression level used by some encodings.
|
|
||||||
*/
|
|
||||||
var imageQuality: Int = 5,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use raw encoding for framebuffer.
|
|
||||||
* This can improve performance when server is running on localhost.
|
|
||||||
*/
|
|
||||||
@ColumnInfo(defaultValue = "0")
|
|
||||||
var useRawEncoding: Boolean = false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initial zoom for the viewer.
|
|
||||||
* This will be used in portrait orientation, or when per-orientation zooming is disabled.
|
|
||||||
*/
|
|
||||||
@ColumnInfo(defaultValue = "1.0")
|
|
||||||
var zoom1: Float = 1f,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will be used in landscape orientation if per-orientation zooming is enabled.
|
|
||||||
*/
|
|
||||||
@ColumnInfo(defaultValue = "1.0")
|
|
||||||
var zoom2: Float = 1f,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies whether 'View Only' mode should be used.
|
|
||||||
* In this mode client does not send any input messages to remote server.
|
|
||||||
*/
|
|
||||||
var viewOnly: Boolean = false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the cursor should be drawn by client instead of server.
|
|
||||||
* It's value is currently ignored, and hardcoded to true.
|
|
||||||
* See [com.gaurav.avnc.viewmodel.VncViewModel.preConnect]
|
|
||||||
*/
|
|
||||||
var useLocalCursor: Boolean = true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server type hint received from user, e.g. tigervnc, tightvnc, vino
|
|
||||||
* Can be used in future to handle known server quirks.
|
|
||||||
*/
|
|
||||||
@ColumnInfo(defaultValue = "")
|
|
||||||
var serverTypeHint: String = "",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composite field for various flags.
|
|
||||||
* This is accessed via individual members like [fLegacyKeySym].
|
|
||||||
*/
|
|
||||||
var flags: Long = FLAG_LEGACY_KEYSYM,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferred style to use for gesture handling.
|
|
||||||
* Possible values: auto, touchscreen, touchpad
|
|
||||||
*/
|
|
||||||
@ColumnInfo(defaultValue = "auto")
|
|
||||||
var gestureStyle: String = "auto",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferred screen orientation.
|
|
||||||
* Possible values: auto, portrait, landscape
|
|
||||||
*/
|
|
||||||
@ColumnInfo(defaultValue = "auto")
|
|
||||||
var screenOrientation: String = "auto",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Usage count tracks how many times user has connected to a server.
|
|
||||||
* Can be used to put frequent servers on top.
|
|
||||||
*/
|
|
||||||
var useCount: Int = 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether UltraVNC Repeater is used for connections.
|
|
||||||
* When repeater is used, [host] & [port] identifies the repeater.
|
|
||||||
*/
|
|
||||||
var useRepeater: Boolean = false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When using a repeater, this value identifies the VNC server.
|
|
||||||
* Valid IDs: [0, 999999999].
|
|
||||||
*/
|
|
||||||
var idOnRepeater: Int = 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resize remote desktop to match with local window size.
|
|
||||||
*/
|
|
||||||
@ColumnInfo(defaultValue = "0")
|
|
||||||
var resizeRemoteDesktop: Boolean = false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* These values are used for SSH Tunnel
|
|
||||||
*/
|
|
||||||
var sshHost: String = "",
|
|
||||||
var sshPort: Int = 22,
|
|
||||||
var sshUsername: String = "",
|
|
||||||
var sshAuthType: Int = SSH_AUTH_KEY,
|
|
||||||
var sshPassword: String = "",
|
|
||||||
var sshPrivateKey: String = "",
|
|
||||||
var sshPrivateKeyPassword: String = ""
|
|
||||||
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// Channel types (from RFC 7869)
|
|
||||||
const val CHANNEL_TCP = 1
|
|
||||||
const val CHANNEL_SSH_TUNNEL = 24
|
|
||||||
|
|
||||||
// SSH auth types
|
|
||||||
const val SSH_AUTH_KEY = 1
|
|
||||||
const val SSH_AUTH_PASSWORD = 2
|
|
||||||
|
|
||||||
// Flag masks
|
|
||||||
private const val FLAG_LEGACY_KEYSYM = 0x01L
|
|
||||||
private const val FLAG_BUTTON_UP_DELAY = 0x02L
|
|
||||||
private const val FLAG_ZOOM_LOCKED = 0x04L
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delegated property builder for [flags] field.
|
|
||||||
*/
|
|
||||||
private class Flag(val flag: Long) {
|
|
||||||
operator fun getValue(p: ServerProfile, kp: KProperty<*>) = (p.flags and flag) != 0L
|
|
||||||
operator fun setValue(p: ServerProfile, kp: KProperty<*>, value: Boolean) {
|
|
||||||
p.flags = if (value) p.flags or flag else p.flags and flag.inv()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag to emit legacy X KeySym events in certain cases.
|
|
||||||
*/
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var fLegacyKeySym by Flag(FLAG_LEGACY_KEYSYM)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag to insert artificial delay before UP event of left-click.
|
|
||||||
*/
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var fButtonUpDelay by Flag(FLAG_BUTTON_UP_DELAY)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If zoom is locked, user requests to change [zoom1] & [zoom2]
|
|
||||||
* should be ignored.
|
|
||||||
*/
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var fZoomLocked by Flag(FLAG_ZOOM_LOCKED)
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.model.db
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.room.AutoMigration
|
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.RenameColumn
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import androidx.room.migration.AutoMigrationSpec
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
|
|
||||||
@Database(entities = [ServerProfile::class], version = MainDb.VERSION, exportSchema = true, autoMigrations = [
|
|
||||||
AutoMigration(from = 1, to = 2, spec = MainDb.MigrationSpec1to2::class), // in v2.0.0
|
|
||||||
AutoMigration(from = 2, to = 3, spec = MainDb.MigrationSpec2to3::class), // in v2.1.0
|
|
||||||
AutoMigration(from = 3, to = 4), // in v2.2.2
|
|
||||||
AutoMigration(from = 4, to = 5, spec = MainDb.MigrationSpec4to5::class), // in v2.3.0
|
|
||||||
])
|
|
||||||
abstract class MainDb : RoomDatabase() {
|
|
||||||
abstract val serverProfileDao: ServerProfileDao
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Current database version
|
|
||||||
*/
|
|
||||||
const val VERSION = 5
|
|
||||||
|
|
||||||
private var instance: MainDb? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns database singleton.
|
|
||||||
* If database is not yet created then it will be created on first call.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun getInstance(context: Context): MainDb {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = Room.databaseBuilder(context, MainDb::class.java, "main").build()
|
|
||||||
}
|
|
||||||
return instance!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/******************************** Migrations ***********************************/
|
|
||||||
// Added in v2.0.0
|
|
||||||
class MigrationSpec1to2 : AutoMigrationSpec {
|
|
||||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
|
||||||
db.execSQL("UPDATE profiles SET imageQuality = 5")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Added in v2.1.0
|
|
||||||
@RenameColumn(tableName = "profiles", fromColumnName = "keyCompatMode", toColumnName = "compatFlags")
|
|
||||||
class MigrationSpec2to3 : AutoMigrationSpec
|
|
||||||
|
|
||||||
// Added in v2.3.0
|
|
||||||
@RenameColumn(tableName = "profiles", fromColumnName = "compatFlags", toColumnName = "flags")
|
|
||||||
@RenameColumn(tableName = "profiles", fromColumnName = "shortcutRank", toColumnName = "useCount")
|
|
||||||
class MigrationSpec4to5 : AutoMigrationSpec
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.model.db
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.Query
|
|
||||||
import androidx.room.Update
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface ServerProfileDao {
|
|
||||||
|
|
||||||
@Query("SELECT * FROM profiles")
|
|
||||||
fun getLiveList(): LiveData<List<ServerProfile>>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM profiles ORDER BY name COLLATE NOCASE")
|
|
||||||
fun getSortedLiveList(): LiveData<List<ServerProfile>>
|
|
||||||
|
|
||||||
//Synchronous version
|
|
||||||
@Query("SELECT * FROM profiles")
|
|
||||||
suspend fun getList(): List<ServerProfile>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM profiles WHERE ID = :id")
|
|
||||||
suspend fun getByID(id: Long): ServerProfile?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM profiles WHERE name = :name")
|
|
||||||
suspend fun getByName(name: String): List<ServerProfile>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM profiles WHERE name LIKE :query OR host LIKE :query OR sshHost LIKE :query ORDER BY useCount DESC")
|
|
||||||
fun search(query: String): LiveData<List<ServerProfile>>
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
suspend fun insert(profile: ServerProfile): Long
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
suspend fun insert(profiles: List<ServerProfile>)
|
|
||||||
|
|
||||||
@Update
|
|
||||||
suspend fun update(profile: ServerProfile)
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
suspend fun delete(profile: ServerProfile)
|
|
||||||
|
|
||||||
@Query("DELETE FROM profiles")
|
|
||||||
suspend fun deleteAll()
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.about
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity for app details.
|
|
||||||
*/
|
|
||||||
class AboutActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val GIT_REPO_URL = "https://github.com/gujjwal00/avnc"
|
|
||||||
const val BUG_REPORT_URL = "https://github.com/gujjwal00/avnc/issues/new"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_about)
|
|
||||||
setSupportActionBar(findViewById(R.id.toolbar))
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_host, AboutFragment())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
|
||||||
onBackPressedDispatcher.onBackPressed()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.about
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.example.tiny_computer.BuildConfig
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
import com.example.tiny_computer.databinding.FragmentAboutBinding
|
|
||||||
|
|
||||||
class AboutFragment : Fragment() {
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
val binding = FragmentAboutBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
binding.apply {
|
|
||||||
repoBtn.setOnClickListener { openUrl(AboutActivity.GIT_REPO_URL) }
|
|
||||||
libraryBtn.setOnClickListener { showFragment(LibrariesFragment()) }
|
|
||||||
licenceBtn.setOnClickListener { showFragment(LicenseFragment()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
requireActivity().setTitle(R.string.title_about)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openUrl(url: String) {
|
|
||||||
if (url.isNotEmpty()) {
|
|
||||||
runCatching { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showFragment(fragment: Fragment) {
|
|
||||||
parentFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_host, fragment)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.about
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
import com.example.tiny_computer.databinding.FragmentLibrariesBinding
|
|
||||||
|
|
||||||
class LibrariesFragment : Fragment() {
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
|
|
||||||
val binding = FragmentLibrariesBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
for (library in libraries) {
|
|
||||||
val textView = inflater.inflate(android.R.layout.simple_list_item_1, binding.libraryList, false) as TextView
|
|
||||||
|
|
||||||
textView.text = library.name
|
|
||||||
textView.setOnClickListener { openUrl(library.homepage) }
|
|
||||||
|
|
||||||
//Apply ripple background
|
|
||||||
with(TypedValue()) {
|
|
||||||
requireContext().theme.resolveAttribute(android.R.attr.selectableItemBackground, this, true)
|
|
||||||
textView.setBackgroundResource(resourceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.libraryList.addView(textView)
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
requireActivity().setTitle(R.string.title_open_source_libraries)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openUrl(url: String) {
|
|
||||||
if (url.isNotEmpty()) {
|
|
||||||
runCatching { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private data class Library(
|
|
||||||
val name: String,
|
|
||||||
val homepage: String
|
|
||||||
)
|
|
||||||
|
|
||||||
private val libraries = listOf(
|
|
||||||
Library("LibVNCClient",
|
|
||||||
"https://github.com/LibVNC/libvncserver"),
|
|
||||||
|
|
||||||
Library("Libjpeg-turbo",
|
|
||||||
"https://github.com/libjpeg-turbo/libjpeg-turbo"),
|
|
||||||
|
|
||||||
Library("wolfSSL",
|
|
||||||
"https://github.com/wolfSSL/wolfssl"),
|
|
||||||
|
|
||||||
Library("ConnectBot's SSH library",
|
|
||||||
"https://github.com/connectbot/sshlib/"),
|
|
||||||
|
|
||||||
Library("Android Jetpack (Androidx)",
|
|
||||||
"https://github.com/libjpeg-turbo/libjpeg-turbo"),
|
|
||||||
|
|
||||||
Library("Material Components for Android",
|
|
||||||
"https://github.com/material-components/material-components-android"),
|
|
||||||
|
|
||||||
Library("Material Icons",
|
|
||||||
"https://fonts.google.com/icons"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.about
|
|
||||||
|
|
||||||
import android.content.res.AssetManager
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
import com.example.tiny_computer.databinding.FragmentLicenseBinding
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class LicenseFragment : Fragment() {
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
val binding = FragmentLicenseBinding.inflate(inflater, container, false)
|
|
||||||
loadLicenses(binding.licenseText, resources.assets)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
requireActivity().setTitle(R.string.title_license)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// These are relative to assets directory
|
|
||||||
private val licenseFiles = listOf(
|
|
||||||
"license/GPL-3.0.txt",
|
|
||||||
"license/Apache-2.0.txt",
|
|
||||||
"license/BSD-libjpeg-turbo.txt",
|
|
||||||
"license/sshlib.txt",
|
|
||||||
"license/X11.txt",
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun loadLicenses(tv: TextView, assets: AssetManager) {
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
var combinedText = ""
|
|
||||||
|
|
||||||
licenseFiles.forEach {
|
|
||||||
val reader = assets.open(it).reader()
|
|
||||||
val text = reader.readText()
|
|
||||||
reader.close()
|
|
||||||
|
|
||||||
combinedText += text
|
|
||||||
combinedText += "\n\n------------------------------------------------------------------------------\n\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
tv.text = combinedText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2022 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.prefs
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceViewHolder
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
import com.gaurav.avnc.util.MsgDialog
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List preference with some extra features.
|
|
||||||
*/
|
|
||||||
class ListPreferenceEx(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Summary used when preference is disabled.
|
|
||||||
*/
|
|
||||||
var disabledStateSummary: CharSequence? = null
|
|
||||||
|
|
||||||
override fun getSummary(): CharSequence? {
|
|
||||||
if (!isEnabled && disabledStateSummary != null)
|
|
||||||
return disabledStateSummary
|
|
||||||
return super.getSummary()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message shown in a dialog, when help button of the preference is clicked.
|
|
||||||
* This will only work if [R.layout.help_btn] is used as widget layout.
|
|
||||||
*/
|
|
||||||
var helpMessage: CharSequence? = null
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
|
||||||
super.onBindViewHolder(holder)
|
|
||||||
(holder.findViewById(R.id.help_btn) as? ImageButton)?.setOnClickListener { showHelp() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showHelp() {
|
|
||||||
helpMessage?.let { helpMessage ->
|
|
||||||
(context as? FragmentActivity)?.let { fragmentActivity ->
|
|
||||||
MsgDialog.show(fragmentActivity.supportFragmentManager,
|
|
||||||
fragmentActivity.getString(R.string.desc_help_btn),
|
|
||||||
helpMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.prefs
|
|
||||||
|
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.annotation.Keep
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.text.HtmlCompat
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import androidx.preference.SwitchPreference
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
import com.gaurav.avnc.util.DeviceAuthPrompt
|
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
|
||||||
|
|
||||||
class PrefsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
DeviceAuthPrompt.applyFingerprintDialogFix(supportFragmentManager)
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_settings)
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
supportFragmentManager
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(R.id.fragment_host, Main())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
setSupportActionBar(findViewById<MaterialToolbar>(R.id.toolbar))
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
|
||||||
onBackPressedDispatcher.onBackPressed()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts new fragment corresponding to given [pref].
|
|
||||||
*/
|
|
||||||
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean {
|
|
||||||
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, pref.fragment!!)
|
|
||||||
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_host, fragment)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Preference Fragments
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
abstract class PrefFragment(private val prefResource: Int) : PreferenceFragmentCompat() {
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
activity?.title = preferenceScreen.title
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
setPreferencesFromResource(prefResource, rootKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep class Main : PrefFragment(R.xml.pref_main)
|
|
||||||
@Keep class Appearance : PrefFragment(R.xml.pref_appearance)
|
|
||||||
|
|
||||||
@Keep class Viewer : PrefFragment(R.xml.pref_viewer) {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
// If system does not support PiP, disable its preference
|
|
||||||
val hasPiPSupport = Build.VERSION.SDK_INT >= 26 &&
|
|
||||||
requireActivity().packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
|
||||||
|
|
||||||
if (!hasPiPSupport) {
|
|
||||||
findPreference<SwitchPreference>("pip_enabled")!!.apply {
|
|
||||||
isEnabled = false
|
|
||||||
summary = getString(R.string.msg_pip_not_supported)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep class Input : PrefFragment(R.xml.pref_input) {
|
|
||||||
private var invertScrollingUpdater: OnSharedPreferenceChangeListener? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
val canChangePtrIcon = Build.VERSION.SDK_INT >= 24
|
|
||||||
|
|
||||||
if (!canChangePtrIcon) {
|
|
||||||
findPreference<SwitchPreference>("hide_local_cursor")!!.apply {
|
|
||||||
isEnabled = false
|
|
||||||
summary = getString(R.string.msg_ptr_hiding_not_supported)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val style = findPreference<ListPreferenceEx>("gesture_style")!!
|
|
||||||
val swipe1 = findPreference<ListPreferenceEx>("gesture_swipe1")!!
|
|
||||||
val longPressSwipe = findPreference<ListPreferenceEx>("gesture_long_press_swipe")!!
|
|
||||||
|
|
||||||
swipe1.disabledStateSummary = getString(R.string.pref_gesture_action_move_pointer)
|
|
||||||
longPressSwipe.helpMessage = getText(R.string.msg_drag_gesture_help)
|
|
||||||
|
|
||||||
swipe1.isEnabled = style.value != "touchpad"
|
|
||||||
style.setOnPreferenceChangeListener { _, value -> swipe1.isEnabled = value != "touchpad"; true }
|
|
||||||
|
|
||||||
val styleHelp = "<b>${getString(R.string.pref_gesture_style_touchscreen)}</b><br/>" +
|
|
||||||
getString(R.string.pref_gesture_style_touchscreen_summary) + "<br/><br/>" +
|
|
||||||
"<b>${getString(R.string.pref_gesture_style_touchpad)}</b><br/>" +
|
|
||||||
getString(R.string.pref_gesture_style_touchpad_summary)
|
|
||||||
|
|
||||||
style.helpMessage = HtmlCompat.fromHtml(styleHelp, 0)
|
|
||||||
|
|
||||||
// To reduce clutter & avoid 'UI overload', pref to invert vertical scrolling is
|
|
||||||
// only visible when 'Scroll remote content' option is used.
|
|
||||||
invertScrollingUpdater = OnSharedPreferenceChangeListener { prefs, _ ->
|
|
||||||
findPreference<SwitchPreference>("invert_vertical_scrolling")!!.apply {
|
|
||||||
isVisible = prefs.all.values.contains("remote-scroll")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
invertScrollingUpdater?.onSharedPreferenceChanged(swipe1.sharedPreferences, null) //Initial update
|
|
||||||
swipe1.sharedPreferences?.registerOnSharedPreferenceChangeListener(invertScrollingUpdater)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep class Server : PrefFragment(R.xml.pref_server)
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.graphics.PointF
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
|
||||||
import com.gaurav.avnc.vnc.Messenger
|
|
||||||
import com.gaurav.avnc.vnc.PointerButton
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We allow users to customize the actions for different events.
|
|
||||||
* This class reads those preferences and invokes proper handlers.
|
|
||||||
*
|
|
||||||
* Input handling overview:
|
|
||||||
*
|
|
||||||
*- +----------------+ +--------------------+ +--------------+
|
|
||||||
*- | Touch events | | Key events | | Virtual keys |
|
|
||||||
*- +----------------+ +--------------------+ +--------------+
|
|
||||||
*- | | |
|
|
||||||
*- v v |
|
|
||||||
*- +----------------+ +--------------------+ |
|
|
||||||
*- | [TouchHandler] | | [KeyHandler] |<------------+
|
|
||||||
*- +----------------+ +--------------------+
|
|
||||||
*- | |
|
|
||||||
*- | v
|
|
||||||
*- | +--------------------+
|
|
||||||
*- +------------->+ [Dispatcher] +
|
|
||||||
*- +--------------------+
|
|
||||||
*- |
|
|
||||||
*- |
|
|
||||||
*- +--------------------+---------------------+
|
|
||||||
*- | | |
|
|
||||||
*- v v v
|
|
||||||
*- +---------------+ +----------------+ +---------------+
|
|
||||||
*- | [Messenger] | | [VncViewModel] | | [VncActivity] |
|
|
||||||
*- +---------------+ +----------------+ +---------------+
|
|
||||||
*-
|
|
||||||
*-
|
|
||||||
*
|
|
||||||
* 1. First we identify which gesture/key was input by the user.
|
|
||||||
* 2. Then we select an action based on user preferences. This is done here in [Dispatcher].
|
|
||||||
* 3. Then that action is executed. Some actions change local app state (e.g. zoom in/out),
|
|
||||||
* while others send events to remote server (e.g. mouse click).
|
|
||||||
*/
|
|
||||||
class Dispatcher(private val activity: VncActivity) {
|
|
||||||
|
|
||||||
private val viewModel = activity.viewModel
|
|
||||||
private val profile = viewModel.profile
|
|
||||||
private val messenger = viewModel.messenger
|
|
||||||
private val gesturePref = viewModel.pref.input.gesture
|
|
||||||
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Action configuration
|
|
||||||
**************************************************************************/
|
|
||||||
private val directMode = DirectMode()
|
|
||||||
private val relativeMode = RelativeMode()
|
|
||||||
private var config = Config()
|
|
||||||
|
|
||||||
private inner class Config {
|
|
||||||
val gestureStyle = if (profile.gestureStyle == "auto") gesturePref.style else profile.gestureStyle
|
|
||||||
val defaultMode = if (gestureStyle == "touchscreen") directMode else relativeMode
|
|
||||||
|
|
||||||
val tap1Action = selectPointAction(gesturePref.tap1)
|
|
||||||
val tap2Action = selectPointAction(gesturePref.tap2)
|
|
||||||
val doubleTapAction = selectPointAction(gesturePref.doubleTap)
|
|
||||||
val longPressAction = selectPointAction(gesturePref.longPress)
|
|
||||||
|
|
||||||
val swipe1Pref = if (gestureStyle == "touchpad") "move-pointer" else gesturePref.swipe1
|
|
||||||
val swipe1Action = selectSwipeAction(swipe1Pref)
|
|
||||||
val swipe2Action = selectSwipeAction(gesturePref.swipe2)
|
|
||||||
val doubleTapSwipeAction = selectSwipeAction(gesturePref.doubleTapSwipe)
|
|
||||||
val longPressSwipeAction = selectSwipeAction(gesturePref.longPressSwipe)
|
|
||||||
val flingAction = selectFlingAction()
|
|
||||||
|
|
||||||
val mouseBackAction = selectPointAction(viewModel.pref.input.mouseBack)
|
|
||||||
|
|
||||||
private fun selectPointAction(actionName: String): (PointF) -> Unit {
|
|
||||||
return when (actionName) {
|
|
||||||
"left-click" -> { p -> defaultMode.doClick(PointerButton.Left, p) }
|
|
||||||
"double-click" -> { p -> defaultMode.doDoubleClick(PointerButton.Left, p) }
|
|
||||||
"middle-click" -> { p -> defaultMode.doClick(PointerButton.Middle, p) }
|
|
||||||
"right-click" -> { p -> defaultMode.doClick(PointerButton.Right, p) }
|
|
||||||
"open-keyboard" -> { _ -> doOpenKeyboard() }
|
|
||||||
else -> { _ -> } //Nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a lambda which accepts four arguments:
|
|
||||||
*
|
|
||||||
* sp: Start point of the gesture
|
|
||||||
* cp: Current point of the gesture
|
|
||||||
* dx: Change along x-axis since last event
|
|
||||||
* dy: Change along y-axis since last event
|
|
||||||
*/
|
|
||||||
private fun selectSwipeAction(actionName: String): (PointF, PointF, Float, Float) -> Unit {
|
|
||||||
return when (actionName) {
|
|
||||||
"pan" -> { _, _, dx, dy -> doPan(dx, dy) }
|
|
||||||
"move-pointer" -> { _, cp, dx, dy -> defaultMode.doMovePointer(cp, dx, dy) }
|
|
||||||
"remote-scroll" -> { sp, _, dx, dy -> defaultMode.doRemoteScroll(sp, dx, dy) }
|
|
||||||
"remote-drag" -> { _, cp, dx, dy -> defaultMode.doRemoteDrag(PointerButton.Left, cp, dx, dy) }
|
|
||||||
"remote-drag-middle" -> { _, cp, dx, dy -> defaultMode.doRemoteDrag(PointerButton.Middle, cp, dx, dy) }
|
|
||||||
else -> { _, _, _, _ -> } //Nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fling is only used for smooth-scrolling the frame.
|
|
||||||
* So it only makes sense when 1-finger-swipe is set to "pan".
|
|
||||||
*/
|
|
||||||
private fun selectFlingAction(): (Float, Float) -> Unit {
|
|
||||||
return if (swipe1Pref == "pan") { vx, vy -> startFrameFling(vx, vy) }
|
|
||||||
else { _, _ -> }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Event receivers
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
fun onGestureStart() = config.defaultMode.onGestureStart()
|
|
||||||
fun onGestureStop(p: PointF) = config.defaultMode.onGestureStop(p)
|
|
||||||
|
|
||||||
fun onTap1(p: PointF) = config.tap1Action(p)
|
|
||||||
fun onTap2(p: PointF) = config.tap2Action(p)
|
|
||||||
fun onDoubleTap(p: PointF) = config.doubleTapAction(p)
|
|
||||||
fun onLongPress(p: PointF) = config.longPressAction(p)
|
|
||||||
|
|
||||||
fun onSwipe1(sp: PointF, cp: PointF, dx: Float, dy: Float) = config.swipe1Action(sp, cp, dx, dy)
|
|
||||||
fun onSwipe2(sp: PointF, cp: PointF, dx: Float, dy: Float) = config.swipe2Action(sp, cp, dx, dy)
|
|
||||||
fun onDoubleTapSwipe(sp: PointF, cp: PointF, dx: Float, dy: Float) = config.doubleTapSwipeAction(sp, cp, dx, dy)
|
|
||||||
fun onLongPressSwipe(sp: PointF, cp: PointF, dx: Float, dy: Float) = config.longPressSwipeAction(sp, cp, dx, dy)
|
|
||||||
|
|
||||||
fun onScale(scaleFactor: Float, fx: Float, fy: Float) = doScale(scaleFactor, fx, fy)
|
|
||||||
fun onFling(vx: Float, vy: Float) = config.flingAction(vx, vy)
|
|
||||||
|
|
||||||
fun onMouseButtonDown(button: PointerButton, p: PointF) = directMode.doButtonDown(button, p)
|
|
||||||
fun onMouseButtonUp(button: PointerButton, p: PointF) = directMode.doButtonUp(button, p)
|
|
||||||
fun onMouseMove(p: PointF) = directMode.doMovePointer(p, 0f, 0f)
|
|
||||||
fun onMouseScroll(p: PointF, hs: Float, vs: Float) = directMode.doRemoteScrollFromMouse(p, hs, vs)
|
|
||||||
fun onMouseBack(p: PointF) = config.mouseBackAction(p)
|
|
||||||
|
|
||||||
fun onStylusTap(p: PointF) = directMode.doClick(PointerButton.Left, p)
|
|
||||||
fun onStylusDoubleTap(p: PointF) = directMode.doDoubleClick(PointerButton.Left, p)
|
|
||||||
fun onStylusLongPress(p: PointF) = directMode.doClick(PointerButton.Right, p)
|
|
||||||
fun onStylusScroll(p: PointF) = directMode.doButtonDown(PointerButton.Left, p)
|
|
||||||
|
|
||||||
fun onXKey(keySym: Int, xtCode: Int, isDown: Boolean) = messenger.sendKey(keySym, xtCode, isDown)
|
|
||||||
|
|
||||||
fun onGestureStyleChanged() {
|
|
||||||
config = Config()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Available actions
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
private fun doOpenKeyboard() = activity.showKeyboard()
|
|
||||||
private fun doScale(scaleFactor: Float, fx: Float, fy: Float) = viewModel.updateZoom(scaleFactor, fx, fy)
|
|
||||||
private fun doPan(dx: Float, dy: Float) = viewModel.panFrame(dx, dy)
|
|
||||||
private fun startFrameFling(vx: Float, vy: Float) = viewModel.frameScroller.fling(vx, vy)
|
|
||||||
private fun stopFrameFling() = viewModel.frameScroller.stop()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Most actions have the same implementation in both modes, only difference being
|
|
||||||
* the point where event is sent. [transformPoint] is used for this mode-specific
|
|
||||||
* point selection.
|
|
||||||
*/
|
|
||||||
private abstract inner class AbstractMode {
|
|
||||||
//Used for remote scrolling
|
|
||||||
private var accumulatedDx = 0F
|
|
||||||
private var accumulatedDy = 0F
|
|
||||||
private val deltaPerScroll = 20F //For how much dx/dy, one scroll event will be sent
|
|
||||||
private val yScrollDirection = (if (gesturePref.invertVerticalScrolling) -1 else 1)
|
|
||||||
|
|
||||||
abstract fun transformPoint(p: PointF): PointF?
|
|
||||||
abstract fun doMovePointer(p: PointF, dx: Float, dy: Float)
|
|
||||||
abstract fun doRemoteDrag(button: PointerButton, p: PointF, dx: Float, dy: Float)
|
|
||||||
|
|
||||||
open fun onGestureStart() = stopFrameFling()
|
|
||||||
open fun onGestureStop(p: PointF) = doButtonRelease(p)
|
|
||||||
|
|
||||||
fun doButtonDown(button: PointerButton, p: PointF) {
|
|
||||||
transformPoint(p)?.let { messenger.sendPointerButtonDown(button, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doButtonUp(button: PointerButton, p: PointF) {
|
|
||||||
transformPoint(p)?.let { messenger.sendPointerButtonUp(button, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doButtonRelease(p: PointF) {
|
|
||||||
transformPoint(p)?.let { messenger.sendPointerButtonRelease(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doClick(button: PointerButton, p: PointF) {
|
|
||||||
doButtonDown(button, p)
|
|
||||||
// Some (obscure) apps seems to ignore click event if button-up is received too early
|
|
||||||
if (button == PointerButton.Left && profile.fButtonUpDelay)
|
|
||||||
messenger.insertButtonUpDelay()
|
|
||||||
doButtonUp(button, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doDoubleClick(button: PointerButton, p: PointF) {
|
|
||||||
doClick(button, p)
|
|
||||||
doClick(button, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doRemoteScroll(focus: PointF, dx: Float, dy: Float) {
|
|
||||||
accumulatedDx += dx
|
|
||||||
accumulatedDy += dy * yScrollDirection
|
|
||||||
|
|
||||||
//Drain horizontal change
|
|
||||||
while (abs(accumulatedDx) >= deltaPerScroll) {
|
|
||||||
if (accumulatedDx > 0) {
|
|
||||||
doClick(PointerButton.WheelLeft, focus)
|
|
||||||
accumulatedDx -= deltaPerScroll
|
|
||||||
} else {
|
|
||||||
doClick(PointerButton.WheelRight, focus)
|
|
||||||
accumulatedDx += deltaPerScroll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Drain vertical change
|
|
||||||
while (abs(accumulatedDy) >= deltaPerScroll) {
|
|
||||||
if (accumulatedDy > 0) {
|
|
||||||
doClick(PointerButton.WheelUp, focus)
|
|
||||||
accumulatedDy -= deltaPerScroll
|
|
||||||
} else {
|
|
||||||
doClick(PointerButton.WheelDown, focus)
|
|
||||||
accumulatedDy += deltaPerScroll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [hs] Movement of horizontal scroll wheel
|
|
||||||
* [vs] Movement of vertical scroll wheel
|
|
||||||
*/
|
|
||||||
fun doRemoteScrollFromMouse(p: PointF, hs: Float, vs: Float) {
|
|
||||||
doRemoteScroll(p, hs * deltaPerScroll, vs * deltaPerScroll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actions happen at touch-point, which is simply transformed from
|
|
||||||
* viewport coordinates into corresponding position in framebuffer.
|
|
||||||
*/
|
|
||||||
private inner class DirectMode : AbstractMode() {
|
|
||||||
override fun transformPoint(p: PointF) = viewModel.frameState.toFb(p)
|
|
||||||
override fun doMovePointer(p: PointF, dx: Float, dy: Float) = doButtonDown(PointerButton.None, p)
|
|
||||||
override fun doRemoteDrag(button: PointerButton, p: PointF, dx: Float, dy: Float) = doButtonDown(button, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actions happen at [pointerPosition], which is updated by [doMovePointer].
|
|
||||||
*/
|
|
||||||
private inner class RelativeMode : AbstractMode() {
|
|
||||||
private val pointerPosition = PointF(0f, 0f)
|
|
||||||
|
|
||||||
override fun onGestureStart() {
|
|
||||||
super.onGestureStart()
|
|
||||||
//Initialize with the latest pointer position
|
|
||||||
pointerPosition.apply {
|
|
||||||
x = viewModel.client.pointerX.toFloat()
|
|
||||||
y = viewModel.client.pointerY.toFloat()
|
|
||||||
}
|
|
||||||
viewModel.client.ignorePointerMovesByServer = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGestureStop(p: PointF) {
|
|
||||||
super.onGestureStop(p)
|
|
||||||
viewModel.client.ignorePointerMovesByServer = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun transformPoint(p: PointF) = pointerPosition
|
|
||||||
|
|
||||||
override fun doMovePointer(p: PointF, dx: Float, dy: Float) {
|
|
||||||
val xLimit = viewModel.frameState.fbWidth - 1
|
|
||||||
val yLimit = viewModel.frameState.fbHeight - 1
|
|
||||||
if (xLimit < 0 || yLimit < 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
pointerPosition.apply {
|
|
||||||
offset(dx, dy)
|
|
||||||
x = x.coerceIn(0f, xLimit)
|
|
||||||
y = y.coerceIn(0f, yLimit)
|
|
||||||
}
|
|
||||||
doButtonDown(PointerButton.None, pointerPosition)
|
|
||||||
|
|
||||||
//Try to keep the pointer centered on screen
|
|
||||||
val vp = viewModel.frameState.toVP(pointerPosition)
|
|
||||||
val centerDiffX = viewModel.frameState.safeArea.centerX() - vp.x
|
|
||||||
val centerDiffY = viewModel.frameState.safeArea.centerY() - vp.y
|
|
||||||
viewModel.panFrame(centerDiffX, centerDiffY)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doRemoteDrag(button: PointerButton, p: PointF, dx: Float, dy: Float) {
|
|
||||||
doButtonDown(button, p)
|
|
||||||
doMovePointer(p, dx, dy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import androidx.dynamicanimation.animation.FlingAnimation
|
|
||||||
import androidx.dynamicanimation.animation.FloatValueHolder
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements fling animation for the frame.
|
|
||||||
*/
|
|
||||||
class FrameScroller(val viewModel: VncViewModel) {
|
|
||||||
|
|
||||||
private val fs = viewModel.frameState
|
|
||||||
private val xAnimator = FlingAnimation(FloatValueHolder())
|
|
||||||
private val yAnimator = FlingAnimation(FloatValueHolder())
|
|
||||||
|
|
||||||
init {
|
|
||||||
xAnimator.addUpdateListener { _, x, _ ->
|
|
||||||
viewModel.moveFrameTo(x, fs.frameY)
|
|
||||||
}
|
|
||||||
|
|
||||||
yAnimator.addUpdateListener { _, y, _ ->
|
|
||||||
viewModel.moveFrameTo(fs.frameX, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop current animation
|
|
||||||
*/
|
|
||||||
fun stop() {
|
|
||||||
xAnimator.cancel()
|
|
||||||
yAnimator.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts fling animation according to given velocities
|
|
||||||
*/
|
|
||||||
fun fling(vx: Float, vy: Float) {
|
|
||||||
stop()
|
|
||||||
|
|
||||||
val x = fs.frameX
|
|
||||||
val y = fs.frameY
|
|
||||||
val safe = fs.safeArea
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fling limits.
|
|
||||||
*
|
|
||||||
* There are two cases:
|
|
||||||
*
|
|
||||||
* 1) x >= safeLeft : It means frame is completely visible and centered horizontally.
|
|
||||||
* In this case both minX,maxX = x (ie. no movement possible).
|
|
||||||
|
|
||||||
* 2) x < safeLeft : Here, 'scaled frame width' > 'safe width'. In this case
|
|
||||||
* minX is negative and maxX = safeLeft.
|
|
||||||
*
|
|
||||||
* minY,maxY are calculated similarly.
|
|
||||||
*/
|
|
||||||
val minX = if (x >= safe.left) x else (safe.width()) - (fs.fbWidth * fs.scale)
|
|
||||||
val maxX = if (x >= safe.left) x else safe.left
|
|
||||||
val minY = if (y >= safe.top) y else (safe.height()) - (fs.fbHeight * fs.scale)
|
|
||||||
val maxY = if (y >= safe.top) y else safe.top
|
|
||||||
|
|
||||||
xAnimator.apply {
|
|
||||||
setStartValue(x)
|
|
||||||
setStartVelocity(vx)
|
|
||||||
setMinValue(minX)
|
|
||||||
setMaxValue(maxX)
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
yAnimator.apply {
|
|
||||||
setStartValue(y)
|
|
||||||
setStartVelocity(vy)
|
|
||||||
setMinValue(minY)
|
|
||||||
setMaxValue(maxY)
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.graphics.PointF
|
|
||||||
import android.graphics.RectF
|
|
||||||
import com.gaurav.avnc.ui.vnc.FrameState.Snapshot
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents current 'view' state of the frame.
|
|
||||||
*
|
|
||||||
* Terminology
|
|
||||||
* ===========
|
|
||||||
*
|
|
||||||
* Framebuffer: This is the buffer holding pixel data. It resides in native memory.
|
|
||||||
*
|
|
||||||
* Frame: This is the actual content rendered on screen. It can be thought of as
|
|
||||||
* 'rendered framebuffer'. Its size changes based on current [scale] and its position
|
|
||||||
* is stored in [frameX] & [frameY].
|
|
||||||
*
|
|
||||||
* Window: Top-level window of the application/activity.
|
|
||||||
*
|
|
||||||
* Viewport: This is the area of window where frame is rendered. It is denoted by [FrameView].
|
|
||||||
*
|
|
||||||
* Safe area: Area inside viewport which is safe for interaction with frame, maintained in [safeArea].
|
|
||||||
*
|
|
||||||
* Window denotes 'total' area available to our activity, viewport denotes 'visible to user'
|
|
||||||
* area, and safe area denotes 'able to click' area. Most of the time all three will be equal,
|
|
||||||
* but viewport can be smaller than window (e.g. if soft keyboard is visible), and safe area
|
|
||||||
* can be smaller than viewport (e.g. due to display cutout).
|
|
||||||
*
|
|
||||||
* +---------------------------+ - -
|
|
||||||
* | \Cutout/ | | | Viewport
|
|
||||||
* | | | | -
|
|
||||||
* | | | | | SafeArea
|
|
||||||
* +---------------------------+ | - -
|
|
||||||
* | Soft Keyboard | | Window
|
|
||||||
* +---------------------------+ -
|
|
||||||
*
|
|
||||||
* Differentiating between these allows us to handle layout changes more easily and cleanly.
|
|
||||||
* We use window size to calculate base scale because we don't want to change scale when
|
|
||||||
* keyboard is shown/hidden. Viewport size is used for rendering the frame, fully immersive.
|
|
||||||
* Safe area is used to coerce frame position so that user can pan every part of frame inside
|
|
||||||
* safe area to interact with it.
|
|
||||||
*
|
|
||||||
* See [LayoutManager] for more information about these values.
|
|
||||||
*
|
|
||||||
* State & Coordinates
|
|
||||||
* ===================
|
|
||||||
*
|
|
||||||
* Both frame & viewport are in same coordinate space. Viewport is assumed to be fixed
|
|
||||||
* in its place with [0,0] represented by top-left corner. Only frame is scaled/moved.
|
|
||||||
* To make sure frame does not move off-screen, after each state change, values are
|
|
||||||
* coerced within range by [coerceValues].
|
|
||||||
*
|
|
||||||
* Rendering is done by [com.gaurav.avnc.ui.vnc.gl.Renderer] based on these values.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Scaling
|
|
||||||
* =======
|
|
||||||
*
|
|
||||||
* Scaling controls the 'size' of rendered frame. It involves multiple factors, like window size,
|
|
||||||
* framebuffer size, user choice etc. To achieve best experience, we split scaling in two parts.
|
|
||||||
* One automatic, and one user controlled.
|
|
||||||
*
|
|
||||||
* 1. Base Scale [baseScale] :
|
|
||||||
* Motivation behind base scale is to start with the most optimal frame size. It is automatically
|
|
||||||
* calculated (and updated) using window size & framebuffer size. When orientation of local
|
|
||||||
* device is such that longer edge of the window is aligned with longer edge of the frame,
|
|
||||||
* base scale will satisfy following constraints (see [calculateBaseScale]):
|
|
||||||
*
|
|
||||||
* - Frame is completely visible
|
|
||||||
* - Frame's aspect ratio is maintained
|
|
||||||
* - Maximum window space is utilized
|
|
||||||
*
|
|
||||||
* 2. Zoom Scale [zoomScale] :
|
|
||||||
* This is the user controlled part. It is updated only in response to pinch gestures.
|
|
||||||
* To allow different zoom in different orientations, two separate zoom scales are maintained
|
|
||||||
* in [zoomScale1] & [zoomScale2]. Based on user preference and current orientation, [zoomScale]
|
|
||||||
* will be delegated to one of these.
|
|
||||||
*
|
|
||||||
* Conceptually, zoom scale works 'on top of' the base scale.
|
|
||||||
* Effective scale [scale] is calculated as the product of these two parts, so:
|
|
||||||
|
|
||||||
* FrameSize = (FramebufferSize * BaseScale) * ZoomScale
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Thread safety
|
|
||||||
* =============
|
|
||||||
*
|
|
||||||
* Frame state is accessed from two threads: Its properties are updated in UI thread
|
|
||||||
* and consumed by the renderer thread. There is a chance that Renderer thread may see
|
|
||||||
* half-updated state (e.g. [frameX] is changed inside [pan] but [coerceValues] is not yet called).
|
|
||||||
* This half-updated state can cause flickering issues.
|
|
||||||
*
|
|
||||||
* To avoid this we use [Snapshot]. All updates to frame state are guarded by [lock].
|
|
||||||
* Render thread uses [getSnapshot] to retrieve a consistent state to render the frame.
|
|
||||||
*/
|
|
||||||
class FrameState(
|
|
||||||
private val minZoomScale: Float = 0.5F,
|
|
||||||
private val maxZoomScale: Float = 5F,
|
|
||||||
private val usePerOrientationZoom: Boolean = false
|
|
||||||
) {
|
|
||||||
|
|
||||||
//Frame position, relative to top-left corner (0,0)
|
|
||||||
var frameX = 0F; private set
|
|
||||||
var frameY = 0F; private set
|
|
||||||
|
|
||||||
//VNC framebuffer size
|
|
||||||
var fbWidth = 0F; private set
|
|
||||||
var fbHeight = 0F; private set
|
|
||||||
|
|
||||||
//Viewport/FrameView size
|
|
||||||
var vpWidth = 0F; private set
|
|
||||||
var vpHeight = 0F; private set
|
|
||||||
|
|
||||||
//Size of activity window
|
|
||||||
var windowWidth = 0F; private set
|
|
||||||
var windowHeight = 0F; private set
|
|
||||||
|
|
||||||
var safeArea = RectF(); private set
|
|
||||||
|
|
||||||
//Scaling
|
|
||||||
var zoomScale1 = 1F; private set
|
|
||||||
var zoomScale2 = 1F; private set
|
|
||||||
private val useZoomScale1 get() = (!usePerOrientationZoom || windowHeight > windowWidth)
|
|
||||||
|
|
||||||
var baseScale = 1F; private set
|
|
||||||
var zoomScale
|
|
||||||
get() = if (useZoomScale1) zoomScale1 else zoomScale2
|
|
||||||
private set(value) = if (useZoomScale1) zoomScale1 = value else zoomScale2 = value
|
|
||||||
|
|
||||||
val scale get() = baseScale * zoomScale
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Immutable wrapper for frame state
|
|
||||||
*/
|
|
||||||
data class Snapshot(
|
|
||||||
val frameX: Float,
|
|
||||||
val frameY: Float,
|
|
||||||
val fbWidth: Float,
|
|
||||||
val fbHeight: Float,
|
|
||||||
val vpWidth: Float,
|
|
||||||
val vpHeight: Float,
|
|
||||||
val scale: Float
|
|
||||||
)
|
|
||||||
|
|
||||||
private val lock = Any()
|
|
||||||
private inline fun <T> withLock(block: () -> T) = synchronized(lock) { block() }
|
|
||||||
|
|
||||||
fun setFramebufferSize(w: Float, h: Float) = withLock {
|
|
||||||
fbWidth = w
|
|
||||||
fbHeight = h
|
|
||||||
calculateBaseScale()
|
|
||||||
coerceValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setViewportSize(w: Float, h: Float) = withLock {
|
|
||||||
vpWidth = w
|
|
||||||
vpHeight = h
|
|
||||||
coerceValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setWindowSize(w: Float, h: Float) = withLock {
|
|
||||||
windowWidth = w
|
|
||||||
windowHeight = h
|
|
||||||
calculateBaseScale()
|
|
||||||
coerceValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSafeArea(rect: RectF) = withLock {
|
|
||||||
safeArea = RectF(rect)
|
|
||||||
coerceValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust zoom scale according to give [scaleFactor].
|
|
||||||
*
|
|
||||||
* Returns 'how much' scale factor is actually applied (after coercing).
|
|
||||||
*/
|
|
||||||
fun updateZoom(scaleFactor: Float): Float = withLock {
|
|
||||||
val oldScale = zoomScale
|
|
||||||
|
|
||||||
zoomScale *= scaleFactor
|
|
||||||
coerceValues()
|
|
||||||
|
|
||||||
return zoomScale / oldScale //Applied scale factor
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setZoom(zoom1: Float, zoom2: Float) = withLock {
|
|
||||||
zoomScale1 = zoom1
|
|
||||||
zoomScale2 = zoom2
|
|
||||||
coerceValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shift frame by given delta.
|
|
||||||
*/
|
|
||||||
fun pan(deltaX: Float, deltaY: Float) = withLock {
|
|
||||||
frameX += deltaX
|
|
||||||
frameY += deltaY
|
|
||||||
coerceValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move frame to given position.
|
|
||||||
*/
|
|
||||||
fun moveTo(x: Float, y: Float) = withLock {
|
|
||||||
frameX = x
|
|
||||||
frameY = y
|
|
||||||
coerceValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if given point is inside of framebuffer.
|
|
||||||
*/
|
|
||||||
fun isValidFbPoint(x: Float, y: Float) = (x >= 0F && x < fbWidth) && (y >= 0F && y < fbHeight)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts given viewport point to corresponding framebuffer point.
|
|
||||||
* Returns null if given point lies outside of framebuffer.
|
|
||||||
*/
|
|
||||||
fun toFb(vpPoint: PointF): PointF? {
|
|
||||||
val fbX = (vpPoint.x - frameX) / scale
|
|
||||||
val fbY = (vpPoint.y - frameY) / scale
|
|
||||||
|
|
||||||
if (isValidFbPoint(fbX, fbY))
|
|
||||||
return PointF(fbX, fbY)
|
|
||||||
else
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts given framebuffer point to corresponding point in viewport.
|
|
||||||
*/
|
|
||||||
fun toVP(fbPoint: PointF): PointF {
|
|
||||||
return PointF(fbPoint.x * scale + frameX, fbPoint.y * scale + frameY)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns immutable & consistent snapshot of frame state.
|
|
||||||
*/
|
|
||||||
fun getSnapshot(): Snapshot = withLock {
|
|
||||||
return Snapshot(frameX = frameX, frameY = frameY,
|
|
||||||
fbWidth = fbWidth, fbHeight = fbHeight,
|
|
||||||
vpWidth = vpWidth, vpHeight = vpHeight, scale = scale)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateBaseScale() {
|
|
||||||
if (fbHeight == 0F || fbWidth == 0F || windowHeight == 0F)
|
|
||||||
return //Not enough info yet
|
|
||||||
|
|
||||||
val s1 = max(windowWidth, windowHeight) / max(fbWidth, fbHeight)
|
|
||||||
val s2 = min(windowWidth, windowHeight) / min(fbWidth, fbHeight)
|
|
||||||
|
|
||||||
baseScale = min(s1, s2)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes sure state values are within constraints.
|
|
||||||
*/
|
|
||||||
private fun coerceValues() {
|
|
||||||
zoomScale1 = zoomScale1.coerceIn(minZoomScale, maxZoomScale)
|
|
||||||
zoomScale2 = zoomScale2.coerceIn(minZoomScale, maxZoomScale)
|
|
||||||
|
|
||||||
if (safeArea.isEmpty || !safeArea.intersect(0f, 0f, vpWidth, vpHeight))
|
|
||||||
safeArea.set(0f, 0f, vpWidth, vpHeight)
|
|
||||||
|
|
||||||
frameX = coercePosition(frameX, safeArea.left, safeArea.right, fbWidth)
|
|
||||||
frameY = coercePosition(frameY, safeArea.top, safeArea.bottom, fbHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Coerce position value in a direction (horizontal/vertical).
|
|
||||||
*/
|
|
||||||
private fun coercePosition(current: Float, safeMin: Float, safeMax: Float, fb: Float): Float {
|
|
||||||
val scaledFb = (fb * scale)
|
|
||||||
val diff = (safeMax - safeMin) - scaledFb
|
|
||||||
|
|
||||||
return if (diff >= 0) diff / 2 + safeMin //Frame will be smaller than safe area, so center it
|
|
||||||
else current.coerceIn(diff + safeMin, safeMin) //otherwise, make sure safe area is completely filled.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.opengl.GLSurfaceView
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.PointerIcon
|
|
||||||
import android.view.inputmethod.BaseInputConnection
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import com.gaurav.avnc.ui.vnc.gl.Renderer
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
|
||||||
import com.gaurav.avnc.vnc.VncClient
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class renders the VNC framebuffer on screen.
|
|
||||||
*
|
|
||||||
* It derives from [GLSurfaceView], which creates an EGL Display, where we can
|
|
||||||
* render the framebuffer using OpenGL ES. See [GLSurfaceView] for more details.
|
|
||||||
*
|
|
||||||
* Actual rendering is done by [Renderer], which is executed on a dedicated
|
|
||||||
* thread by [GLSurfaceView].
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*- +-------------------+ +--------------------+ +--------------------+
|
|
||||||
*- | [FrameView] | | [VncViewModel] | | [VncClient] |
|
|
||||||
*- +--------+----------+ +----------+---------+ +----------+---------+
|
|
||||||
*- | | |
|
|
||||||
*- | | |
|
|
||||||
*- | Render Request | [FrameState] | Framebuffer
|
|
||||||
*- | v |
|
|
||||||
*- | +----------+---------+ |
|
|
||||||
*- +-------------------> | [Renderer] | <------------------+
|
|
||||||
*- +--------------------+
|
|
||||||
|
|
||||||
*/
|
|
||||||
class FrameView(context: Context?, attrs: AttributeSet? = null) : GLSurfaceView(context, attrs) {
|
|
||||||
|
|
||||||
private lateinit var touchHandler: TouchHandler
|
|
||||||
private lateinit var keyHandler: KeyHandler
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Input connection used for intercepting key events
|
|
||||||
*/
|
|
||||||
inner class InputConnection : BaseInputConnection(this, false) {
|
|
||||||
override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
|
|
||||||
return keyHandler.onCommitText(text) || super.commitText(text, newCursorPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sendKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
return keyHandler.onKeyEvent(event) || super.sendKeyEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should be called from [VncActivity.onCreate].
|
|
||||||
*/
|
|
||||||
fun initialize(activity: VncActivity) {
|
|
||||||
val viewModel = activity.viewModel
|
|
||||||
|
|
||||||
touchHandler = activity.touchHandler
|
|
||||||
keyHandler = activity.keyHandler
|
|
||||||
|
|
||||||
setEGLContextClientVersion(2)
|
|
||||||
setRenderer(Renderer(viewModel))
|
|
||||||
renderMode = RENDERMODE_WHEN_DIRTY
|
|
||||||
|
|
||||||
// Hide local cursor if requested and supported
|
|
||||||
if (Build.VERSION.SDK_INT >= 24 && viewModel.pref.input.hideLocalCursor)
|
|
||||||
pointerIcon = PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
|
|
||||||
outAttrs.imeOptions = outAttrs.imeOptions or
|
|
||||||
EditorInfo.IME_FLAG_NO_EXTRACT_UI or
|
|
||||||
EditorInfo.IME_FLAG_NO_FULLSCREEN
|
|
||||||
return InputConnection()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCheckIsTextEditor(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
return touchHandler.onTouchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGenericMotionEvent(event: MotionEvent): Boolean {
|
|
||||||
return touchHandler.onGenericMotionEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onHoverEvent(event: MotionEvent): Boolean {
|
|
||||||
return touchHandler.onHoverEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This dialog is used to get user-confirmation before connecting to unknown SSH servers.
|
|
||||||
*/
|
|
||||||
class HostKeyFragment : DialogFragment() {
|
|
||||||
val viewModel by activityViewModels<VncViewModel>()
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val request = viewModel.sshHostKeyVerifyRequest
|
|
||||||
val hostKey = request.value!!
|
|
||||||
val titleRes = if (hostKey.isKnownHost) R.string.title_ssh_host_key_changed else R.string.title_unknown_ssh_host
|
|
||||||
|
|
||||||
val message = """
|
|
||||||
|
|
|
||||||
|Host: ${hostKey.host}
|
|
||||||
|Key type: ${hostKey.algo.uppercase()}
|
|
||||||
|Key fingerprint:
|
|
||||||
|
|
|
||||||
|${hostKey.getFingerprint()}
|
|
||||||
|
|
|
||||||
|Please make sure your are connecting to the valid host.
|
|
||||||
|
|
|
||||||
|If you continue, this host & key will be marked as known.
|
|
||||||
""".trimMargin()
|
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(titleRes)
|
|
||||||
.setMessage(message)
|
|
||||||
.setPositiveButton(R.string.title_continue) { _, _ -> request.offerResponse(true) }
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> request.offerResponse(false) }
|
|
||||||
.setCancelable(false)
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
import com.gaurav.avnc.model.db.MainDb
|
|
||||||
import com.gaurav.avnc.vnc.VncUri
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles "external" intents and launches [VncActivity] with appropriate profiles.
|
|
||||||
*
|
|
||||||
* Current intent types:
|
|
||||||
* - vnc:// URIs
|
|
||||||
* - App shortcuts
|
|
||||||
*/
|
|
||||||
class IntentReceiverActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SHORTCUT_PROFILE_ID_KEY = "com.gaurav.avnc.shortcut_profile_id"
|
|
||||||
|
|
||||||
fun createShortcutIntent(context: Context, profileId: Long): Intent {
|
|
||||||
check(profileId != 0L) { "Cannot create shortcut with profileId = 0." }
|
|
||||||
return Intent(context, IntentReceiverActivity::class.java).apply {
|
|
||||||
action = Intent.ACTION_VIEW
|
|
||||||
putExtra(SHORTCUT_PROFILE_ID_KEY, profileId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val profileDao by lazy { MainDb.getInstance(this).serverProfileDao }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
handleIntent()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleIntent() = lifecycleScope.launch {
|
|
||||||
if (intent.data?.scheme == "vnc")
|
|
||||||
launchFromVncUri(VncUri(intent.data!!.toString()))
|
|
||||||
else if (intent.hasExtra(SHORTCUT_PROFILE_ID_KEY))
|
|
||||||
launchFromProfileId(intent.getLongExtra(SHORTCUT_PROFILE_ID_KEY, 0))
|
|
||||||
else
|
|
||||||
toast("Invalid intent: Server info is missing!")
|
|
||||||
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun launchFromVncUri(uri: VncUri) {
|
|
||||||
if (uri.connectionName.isNotBlank()) launchFromProfileName(uri.connectionName)
|
|
||||||
else launchVncUri(uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchVncUri(uri: VncUri) {
|
|
||||||
if (uri.host.isEmpty()) toast(getString(R.string.msg_invalid_vnc_uri))
|
|
||||||
else startVncActivity(this, uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun launchFromProfileName(name: String) {
|
|
||||||
val profile = profileDao.getByName(name).firstOrNull()
|
|
||||||
if (profile == null) toast("No server found with name '$name'")
|
|
||||||
else startVncActivity(this, profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun launchFromProfileId(profileId: Long) {
|
|
||||||
val profile = profileDao.getByID(profileId)
|
|
||||||
if (profile == null) toast(getString(R.string.msg_shortcut_server_deleted))
|
|
||||||
else startVncActivity(this, profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toast(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
@@ -1,386 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.KeyCharacterMap
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import com.gaurav.avnc.util.AppPreferences
|
|
||||||
import com.gaurav.avnc.vnc.XKeySym
|
|
||||||
import com.gaurav.avnc.vnc.XKeySymAndroid
|
|
||||||
import com.gaurav.avnc.vnc.XKeySymAndroid.updateKeyMap
|
|
||||||
import com.gaurav.avnc.vnc.XKeySymUnicode
|
|
||||||
import com.gaurav.avnc.vnc.XTKeyCode
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for key events
|
|
||||||
*
|
|
||||||
* Key handling in RFB protocol works on 'key symbols' instead of key-codes/scan-codes
|
|
||||||
* which makes it dependent on keyboard layout. VNC servers implement various heuristics
|
|
||||||
* to compensate for this & maximize portability. Our implementation is derived after
|
|
||||||
* testing with some popular servers. It might not handle all the edge cases.
|
|
||||||
*
|
|
||||||
* There is an extension to RFB protocol (ExtendedKeyEvent) implemented by some servers.
|
|
||||||
* It includes support for sending XT keycodes along with key symbol. This extension
|
|
||||||
* greatly reduces the key handling complexity. Unfortunately, as soft keyboards are
|
|
||||||
* more common on Android, most [KeyEvent]s don't provide raw scan codes.
|
|
||||||
*
|
|
||||||
* Basically, job of this class is to convert the received [KeyEvent] into a 'KeySym'.
|
|
||||||
* That KeySym will be sent to the server.
|
|
||||||
*
|
|
||||||
*- [KeyEvent] +----------------+ KeySym +----------------+
|
|
||||||
*- ----------------> | [KeyHandler] | ------------> | [Dispatcher] |
|
|
||||||
*- +----------------+ +----------------+
|
|
||||||
*
|
|
||||||
* This class emits (conceptually) three types of key symbols:
|
|
||||||
*
|
|
||||||
* 1. X KeySym - Individual symbols defined by X Windows System
|
|
||||||
* 2. Unicode KeySym - Unicode code points encoded as X KeySym
|
|
||||||
* 3. Legacy X KeySym - Old KeySyms which are now superseded by their Unicode KeySym equivalents
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* To decide which one to emit, we look at following things:
|
|
||||||
*
|
|
||||||
* a. Key code of [KeyEvent] (may not be available, e.g. in case of [KeyEvent.ACTION_MULTIPLE])
|
|
||||||
* b. Unicode character of [KeyEvent] (may not be available, e.g. in case of [KeyEvent.KEYCODE_F1])
|
|
||||||
* c. Current [cfLegacyKeysym]
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*- [KeyEvent]
|
|
||||||
*- |
|
|
||||||
*- v
|
|
||||||
*- +----------------------------+
|
|
||||||
*- | Is Unicode Char Available? |
|
|
||||||
*- +-------------+--------------+
|
|
||||||
*- |
|
|
||||||
*- Yes | No
|
|
||||||
*- +-------------+--------------+
|
|
||||||
*- | |
|
|
||||||
*- +---------v----------+ +-------v-------+
|
|
||||||
*- | Use Unicode Char | | Use Key Code |
|
|
||||||
*- +---------+----------+ +-------+-------+
|
|
||||||
*- | |
|
|
||||||
*- +---------v----------+ |
|
|
||||||
*- | In compat mode? | |
|
|
||||||
*- +---------+----------+ |
|
|
||||||
*- | |
|
|
||||||
*- Yes | No |
|
|
||||||
*- +---------+----------+ |
|
|
||||||
*- | | |
|
|
||||||
*- v v v
|
|
||||||
*- (Legacy X KeySym) (Unicode KeySym) (X KeySym)
|
|
||||||
*
|
|
||||||
* See [handleKeyEvent] as a starting point.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Reference:
|
|
||||||
* [X Windows System Protocol](https://www.x.org/releases/X11R7.7/doc/xproto/x11protocol.html#keysym_encoding)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class KeyHandler(private val dispatcher: Dispatcher, private val cfLegacyKeysym: Boolean, prefs: AppPreferences) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pre-KeyEvent hook.
|
|
||||||
* This is NOT triggered for all characters.
|
|
||||||
*/
|
|
||||||
fun onCommitText(text: CharSequence?): Boolean {
|
|
||||||
return handleCCedilla(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shortcut to send both up & down events. Useful for Virtual Keys.
|
|
||||||
*/
|
|
||||||
fun onKey(keyCode: Int) {
|
|
||||||
onKeyEvent(keyCode, true)
|
|
||||||
onKeyEvent(keyCode, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onKeyEvent(keyCode: Int, isDown: Boolean) {
|
|
||||||
val action = if (isDown) KeyEvent.ACTION_DOWN else KeyEvent.ACTION_UP
|
|
||||||
onKeyEvent(KeyEvent(action, keyCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
if (shouldIgnoreEvent(event))
|
|
||||||
return false
|
|
||||||
|
|
||||||
return handleKeyEvent(preProcessEvent(event))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will parse the [event] and call [emitForKeyEvent] appropriately.
|
|
||||||
*/
|
|
||||||
private fun handleKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
|
|
||||||
//Deprecated action types are still received for non-ASCII characters
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
when (event.action) {
|
|
||||||
|
|
||||||
KeyEvent.ACTION_DOWN -> return emitForKeyEvent(event.keyCode, getUnicodeChar(event), true, event.scanCode)
|
|
||||||
KeyEvent.ACTION_UP -> return emitForKeyEvent(event.keyCode, getUnicodeChar(event), false, event.scanCode)
|
|
||||||
|
|
||||||
KeyEvent.ACTION_MULTIPLE -> {
|
|
||||||
if (event.keyCode == KeyEvent.KEYCODE_UNKNOWN) {
|
|
||||||
|
|
||||||
// Here, only Unicode characters are available.
|
|
||||||
for (uChar in toCodePoints(event.characters)) {
|
|
||||||
emitForKeyEvent(0, uChar, true)
|
|
||||||
emitForKeyEvent(0, uChar, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Here, only keyCode is available.
|
|
||||||
// According to Android docs, this case doesn't happen anymore.
|
|
||||||
for (i in 1..event.repeatCount) {
|
|
||||||
emitForKeyEvent(event.keyCode, 0, true)
|
|
||||||
emitForKeyEvent(event.keyCode, 0, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits an event for given details.
|
|
||||||
* It will call [emitForAndroidKeyCode] or [emitForUnicodeChar] depending on arguments.
|
|
||||||
*/
|
|
||||||
private fun emitForKeyEvent(keyCode: Int, unicodeChar: Int, isDown: Boolean, scanCode: Int = 0): Boolean {
|
|
||||||
val xtCode = if (scanCode == 0) 0 else XTKeyCode.fromAndroidScancode(scanCode)
|
|
||||||
|
|
||||||
if (handleDiacritics(keyCode, unicodeChar, isDown))
|
|
||||||
return true
|
|
||||||
|
|
||||||
// Always emit using keyCode for these because Android returns a unicodeChar
|
|
||||||
// for these but most servers don't handle their Unicode characters.
|
|
||||||
when (keyCode) {
|
|
||||||
KeyEvent.KEYCODE_ENTER,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
||||||
KeyEvent.KEYCODE_SPACE,
|
|
||||||
KeyEvent.KEYCODE_TAB ->
|
|
||||||
return emitForAndroidKeyCode(keyCode, isDown, xtCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We prefer to use unicodeChar even when keyCode is available because
|
|
||||||
// most servers ignore previously sent SHIFT/CAPS_LOCK keys.
|
|
||||||
// As Android takes meta keys into account when calculating unicodeChar,
|
|
||||||
// it works well with these servers.
|
|
||||||
|
|
||||||
if (unicodeChar != 0)
|
|
||||||
return emitForUnicodeChar(unicodeChar, isDown, xtCode)
|
|
||||||
else
|
|
||||||
return emitForAndroidKeyCode(keyCode, isDown, xtCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits X KeySym corresponding to [keyCode]
|
|
||||||
*/
|
|
||||||
private fun emitForAndroidKeyCode(keyCode: Int, isDown: Boolean, xtCode: Int = 0): Boolean {
|
|
||||||
val keySym = XKeySymAndroid.getKeySymForAndroidKeyCode(keyCode)
|
|
||||||
return emit(keySym, isDown, xtCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits either Unicode KeySym or legacy KeySym for [uChar], depending on [cfLegacyKeysym].
|
|
||||||
*/
|
|
||||||
private fun emitForUnicodeChar(uChar: Int, isDown: Boolean, xtCode: Int = 0): Boolean {
|
|
||||||
var uKeySym = 0
|
|
||||||
|
|
||||||
if (cfLegacyKeysym)
|
|
||||||
uKeySym = XKeySymUnicode.getLegacyKeySymForUnicodeChar(uChar)
|
|
||||||
|
|
||||||
if (uKeySym == 0)
|
|
||||||
uKeySym = XKeySymUnicode.getKeySymForUnicodeChar(uChar)
|
|
||||||
|
|
||||||
|
|
||||||
// If we are generating legacy KeySym and the character is uppercase,
|
|
||||||
// we need to fake press the Shift key. Otherwise, most servers can't
|
|
||||||
// handle them. This is just a compat shim and ideally server should
|
|
||||||
// support Unicode KeySym.
|
|
||||||
val shouldFakeShift = uKeySym in 0x100..0xfffe && uChar.toChar().isUpperCase()
|
|
||||||
if (shouldFakeShift)
|
|
||||||
emitForAndroidKeyCode(KeyEvent.KEYCODE_SHIFT_LEFT, true)
|
|
||||||
|
|
||||||
emit(uKeySym, isDown, xtCode)
|
|
||||||
|
|
||||||
if (shouldFakeShift)
|
|
||||||
emitForAndroidKeyCode(KeyEvent.KEYCODE_SHIFT_LEFT, false)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends given X key to [dispatcher].
|
|
||||||
*/
|
|
||||||
private fun emit(keySym: Int, isDown: Boolean, xtCode: Int = 0): Boolean {
|
|
||||||
if (keySym == 0)
|
|
||||||
return false
|
|
||||||
|
|
||||||
return dispatcher.onXKey(keySym, xtCode, isDown)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns unicode character for given event.
|
|
||||||
* Normally [KeyEvent.getUnicodeChar] is sufficient for our need, but sometimes
|
|
||||||
* we have to fiddle with meta state to extract a suitable character.
|
|
||||||
*
|
|
||||||
* Consider Ctrl+Shift+A: [KeyEvent.getUnicodeChar] returns 0 for this case,
|
|
||||||
* because there is no character mapping for A when Ctrl & Shift both are pressed.
|
|
||||||
* But we want to obtain capital 'A' here, so that we can send it to server.
|
|
||||||
* This ensures proper working of keyboard shortcuts.
|
|
||||||
*/
|
|
||||||
private fun getUnicodeChar(event: KeyEvent): Int {
|
|
||||||
val uChar = event.unicodeChar
|
|
||||||
if (uChar != 0 || event.metaState == 0)
|
|
||||||
return uChar
|
|
||||||
|
|
||||||
// Try without Alt/Ctrl
|
|
||||||
val altCtrl = KeyEvent.META_ALT_MASK or KeyEvent.META_CTRL_MASK
|
|
||||||
return event.getUnicodeChar(event.metaState and altCtrl.inv())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some cases where we want to ignore events.
|
|
||||||
*/
|
|
||||||
private fun shouldIgnoreEvent(event: KeyEvent): Boolean {
|
|
||||||
val keyCode = event.keyCode
|
|
||||||
|
|
||||||
// As if our key-handling wasn't already complex enough, Android
|
|
||||||
// decided to mess-up NumLock handling. When any numpad number-key
|
|
||||||
// is pressed (e.g. 7) and NumLock is off, it will _still_ send
|
|
||||||
// the number keycode (e.g. KEYCODE_NUMPAD_7) first. And if apps don't
|
|
||||||
// handle that, it will fallback to secondary action (e.g. KEYCODE_MOVE_HOME).
|
|
||||||
// So we have to ignore the first events when NumLock is off.
|
|
||||||
return (keyCode in KeyEvent.KEYCODE_NUMPAD_0..KeyEvent.KEYCODE_NUMPAD_9
|
|
||||||
|| keyCode == KeyEvent.KEYCODE_NUMPAD_DOT) && !event.isNumLockOn
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************************************************************************
|
|
||||||
* Diacritics (Accents) Support
|
|
||||||
*
|
|
||||||
* Instead of sending diacritics directly to server, we handle their composition here.
|
|
||||||
* This is done because:
|
|
||||||
*
|
|
||||||
* - Android does not report real 'combining' accents to us. Instead we get the
|
|
||||||
* corresponding 'printing' characters, with the `COMBINING_ACCENT` flag set.
|
|
||||||
* See the source code of [KeyCharacterMap] for more details.
|
|
||||||
*
|
|
||||||
* - Although most servers don't support diacritics directly, some of them can
|
|
||||||
* handle the final composed characters (e.g. TightVNC).
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Behaviour:
|
|
||||||
*
|
|
||||||
* - Until an accent is received, all events are ignored by [handleDiacritics].
|
|
||||||
* - When first accent is received, we start tracking by adding it to [accentSequence].
|
|
||||||
* - When next key is received (can be another accent), we add it to [accentSequence].
|
|
||||||
* - Then we try to compose a printable character from [accentSequence], and if successful,
|
|
||||||
* the composed character is sent to the server.
|
|
||||||
* - If composition was successful, or we received non-accent key, we stop tracking
|
|
||||||
* by clearing [accentSequence].
|
|
||||||
*
|
|
||||||
************************************************************************************/
|
|
||||||
private var accentSequence = ArrayList<Int>(2)
|
|
||||||
|
|
||||||
private fun handleDiacritics(keyCode: Int, uChar: Int, isDown: Boolean): Boolean {
|
|
||||||
val isUp = !isDown
|
|
||||||
val isAccent = uChar and KeyCharacterMap.COMBINING_ACCENT != 0
|
|
||||||
val maskedChar = uChar and KeyCharacterMap.COMBINING_ACCENT_MASK
|
|
||||||
|
|
||||||
if (!isAccent && accentSequence.size == 0) return false // No tracking yet (most common case)
|
|
||||||
if (!isAccent && isUp && !accentSequence.contains(maskedChar)) return false // Spurious key-ups
|
|
||||||
if (KeyEvent.isModifierKey(keyCode)) return false // Modifier keys are passed-on to the server
|
|
||||||
|
|
||||||
if (isDown)
|
|
||||||
accentSequence.add(maskedChar)
|
|
||||||
|
|
||||||
if (accentSequence.size <= 1) // Nothing to compose yet
|
|
||||||
return true
|
|
||||||
|
|
||||||
var composed = accentSequence.last()
|
|
||||||
for (i in 0 until accentSequence.lastIndex)
|
|
||||||
composed = KeyEvent.getDeadChar(accentSequence[i], composed)
|
|
||||||
|
|
||||||
if (composed != 0)
|
|
||||||
emitForUnicodeChar(composed, isDown)
|
|
||||||
|
|
||||||
if (isUp && (composed != 0 || !isAccent))
|
|
||||||
accentSequence.clear()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 'Ç' & 'ç' requires special handling because Android generates them with extra ALT key press,
|
|
||||||
* and gives no indication in KeyEvents that accents are involved. So we have to handle these
|
|
||||||
* before events are synthesized by InputConnection in FrameView.
|
|
||||||
*/
|
|
||||||
private fun handleCCedilla(text: CharSequence?): Boolean {
|
|
||||||
if (text == "ç") {
|
|
||||||
emitForUnicodeChar('ç'.code, true)
|
|
||||||
emitForUnicodeChar('ç'.code, false)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text == "Ç") {
|
|
||||||
emitForAndroidKeyCode(KeyEvent.KEYCODE_SHIFT_LEFT, true)
|
|
||||||
emitForUnicodeChar('Ç'.code, true)
|
|
||||||
emitForUnicodeChar('Ç'.code, false)
|
|
||||||
emitForAndroidKeyCode(KeyEvent.KEYCODE_SHIFT_LEFT, false)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************************************************************************
|
|
||||||
* Custom key-mappings
|
|
||||||
***********************************************************************************/
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (prefs.input.kmLanguageSwitchToSuper) updateKeyMap(KeyEvent.KEYCODE_LANGUAGE_SWITCH, XKeySym.XK_Super_L)
|
|
||||||
if (prefs.input.kmRightAltToSuper) updateKeyMap(KeyEvent.KEYCODE_ALT_RIGHT, XKeySym.XK_Super_L)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can't map Back key to Escape inside init because we don't
|
|
||||||
// want to affect Back key events coming from Navigation Bar.
|
|
||||||
// So we have to test each event.
|
|
||||||
private val kmBackToEscape = prefs.input.kmBackToEscape
|
|
||||||
|
|
||||||
private fun preProcessEvent(event: KeyEvent): KeyEvent {
|
|
||||||
if (event.keyCode == KeyEvent.KEYCODE_BACK && kmBackToEscape
|
|
||||||
&& (event.flags and KeyEvent.FLAG_VIRTUAL_HARD_KEY == 0))
|
|
||||||
return KeyEvent(event.action, KeyEvent.KEYCODE_ESCAPE)
|
|
||||||
|
|
||||||
return event
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************************************************************************
|
|
||||||
* Convert String to Array of Unicode code-points
|
|
||||||
***********************************************************************************/
|
|
||||||
|
|
||||||
private val cpCache = intArrayOf(0)
|
|
||||||
|
|
||||||
private fun toCodePoints(string: String): IntArray {
|
|
||||||
//Handle simple & most probable case
|
|
||||||
if (string.length == 1)
|
|
||||||
return cpCache.apply { this[0] = string[0].code }
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 24)
|
|
||||||
return string.codePoints().toArray()
|
|
||||||
|
|
||||||
//Otherwise, do simple conversion (will be incorrect non-MBP code points)
|
|
||||||
return string.map { it.code }.toIntArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2022 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.RectF
|
|
||||||
import android.os.Build.VERSION.SDK_INT
|
|
||||||
import android.view.RoundedCorner
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat.Type
|
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is responsible for managing fullscreen layout & insets handling.
|
|
||||||
* Layout handling is quite complex because of Android APIs (or lack thereof).
|
|
||||||
* It gets way worse here because AVNC shows fullscreen content.
|
|
||||||
*
|
|
||||||
* Situation is so bad, it's almost funny.
|
|
||||||
*
|
|
||||||
* Almost every Android version has introduced some new APIs, deprecated others,
|
|
||||||
* or downright changed existing behaviour (sometimes just for ONE version).
|
|
||||||
* AndroidX compat library has been the only respite. It has at least hidden a
|
|
||||||
* bunch of if-else statement from our own code. But all that mess is still there.
|
|
||||||
*/
|
|
||||||
class LayoutManager(activity: VncActivity) {
|
|
||||||
private val viewModel = activity.viewModel
|
|
||||||
private val rootView = activity.binding.root
|
|
||||||
private val frameView = activity.binding.frameView
|
|
||||||
private val virtualKeys = activity.virtualKeys
|
|
||||||
private val window = activity.window
|
|
||||||
private val insetController = WindowCompat.getInsetsController(window, window.decorView)
|
|
||||||
|
|
||||||
|
|
||||||
fun initialize() {
|
|
||||||
if (SDK_INT >= 30)
|
|
||||||
hookInsetsListener()
|
|
||||||
else
|
|
||||||
hookSystemUiChangeListener()
|
|
||||||
|
|
||||||
hookGlobalLayoutListener()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onConnectionStateChanged() {
|
|
||||||
updateFullscreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onWindowFocusChanged(hasFocus: Boolean) {
|
|
||||||
if (hasFocus && SDK_INT < 30)
|
|
||||||
updateFullscreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(30)
|
|
||||||
private fun hookInsetsListener() {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v, insets ->
|
|
||||||
updateWindowInsets(insets)
|
|
||||||
updateNavigationBarVisibility(insets)
|
|
||||||
ViewCompat.onApplyWindowInsets(v, insets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hookGlobalLayoutListener() {
|
|
||||||
rootView.viewTreeObserver.addOnGlobalLayoutListener {
|
|
||||||
viewModel.frameState.setWindowSize(rootView.width.toFloat(), rootView.height.toFloat())
|
|
||||||
viewModel.frameState.setViewportSize(frameView.width.toFloat(), frameView.height.toFloat())
|
|
||||||
virtualKeys.container?.let { updateVirtualKeyInsets(it) }
|
|
||||||
|
|
||||||
if (SDK_INT < 30)
|
|
||||||
manuallyGenerateWindowInsets()
|
|
||||||
|
|
||||||
applyInsets()
|
|
||||||
viewModel.resizeRemoteDesktop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* System's WindowInsets are basically useless for us on API < 30. Neither are
|
|
||||||
* they dispatched in all cases, nor they always include necessary information.
|
|
||||||
* So, on these platforms, insets are generated manually.
|
|
||||||
*
|
|
||||||
* We could apply required padding directly here, but simulating insets means that
|
|
||||||
* [applyInsets] has a single implementation for all platforms.
|
|
||||||
*/
|
|
||||||
private fun manuallyGenerateWindowInsets() {
|
|
||||||
val decorView = window.decorView
|
|
||||||
val visibleFrame = Rect()
|
|
||||||
decorView.getWindowVisibleDisplayFrame(visibleFrame)
|
|
||||||
|
|
||||||
// Transform from screen coordinates to window coordinates
|
|
||||||
// Both can be different, e.g. when in PiP mode
|
|
||||||
val locationOnScreen = intArrayOf(0, 0)
|
|
||||||
decorView.getLocationOnScreen(locationOnScreen)
|
|
||||||
visibleFrame.offset(-locationOnScreen[0], -locationOnScreen[1])
|
|
||||||
|
|
||||||
// Generate insets (left & top are always 0)
|
|
||||||
val right = max(0, decorView.right - visibleFrame.right)
|
|
||||||
val bottom = max(0, decorView.bottom - visibleFrame.bottom)
|
|
||||||
val insets = WindowInsetsCompat.Builder()
|
|
||||||
.setInsets(Type.ime(), Insets.of(0, 0, 0, bottom))
|
|
||||||
.setInsets(Type.navigationBars(), Insets.of(0, 0, right, 0))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
updateWindowInsets(insets)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flags to hide system bars are not 'sticky' on API < 30. System will automatically
|
|
||||||
* clear them in a lot of situations, e.g. when IME is shown. So we have to keep
|
|
||||||
* reminding it that we still want to remain fullscreen.
|
|
||||||
* Same reason why [onWindowFocusChanged] exists.
|
|
||||||
*/
|
|
||||||
private fun hookSystemUiChangeListener() {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
window.decorView.setOnSystemUiVisibilityChangeListener { updateFullscreen() }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/************************************************************************************
|
|
||||||
* Fullscreen
|
|
||||||
************************************************************************************/
|
|
||||||
private val fullscreenEnabled = viewModel.pref.viewer.fullscreen
|
|
||||||
private val defaultSystemBarBehaviour = insetController.systemBarsBehavior
|
|
||||||
|
|
||||||
private fun updateFullscreen() {
|
|
||||||
if (!fullscreenEnabled)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (viewModel.client.connected)
|
|
||||||
enterFullscreen()
|
|
||||||
else
|
|
||||||
leaveFullscreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enterFullscreen() {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
insetController.hide(Type.systemBars())
|
|
||||||
insetController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
||||||
|
|
||||||
if (SDK_INT < 30) {
|
|
||||||
// This flag is required to keep the status bar hidden when IME is visible
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun leaveFullscreen() {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
|
||||||
insetController.show(Type.systemBars())
|
|
||||||
insetController.systemBarsBehavior = defaultSystemBarBehaviour
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(30)
|
|
||||||
private fun updateNavigationBarVisibility(insets: WindowInsetsCompat) {
|
|
||||||
// On API 30, Android doesn't automatically show the navigation bar with keyboard.
|
|
||||||
// So to hide the keyboard, user has to first swipe to un-hide the navigation bar
|
|
||||||
// and then tap on Back button. To avoid this, we manually show the navigation bar
|
|
||||||
// whenever keyboard is visible. Although only API 30 seems to be affected, fix is
|
|
||||||
// applied on 30+ APIs to ensure consistency.
|
|
||||||
if (insets.isVisible(Type.ime()))
|
|
||||||
insetController.show(Type.navigationBars())
|
|
||||||
else if (fullscreenEnabled && viewModel.client.connected) {
|
|
||||||
insetController.hide(Type.navigationBars())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************************************************************************
|
|
||||||
* Insets
|
|
||||||
*
|
|
||||||
* Insets are Android's way of telling us when our window is obscured by something.
|
|
||||||
* To provide optimal experience, insets are divided into two categories:
|
|
||||||
*
|
|
||||||
* Opaque Insets: These insets don't allow interaction with app's window behind them.
|
|
||||||
* IME & nav bar are such insets. These are handled by adding padding to [rootView],
|
|
||||||
* effectively resizing [frameView] to visible portion of the screen.
|
|
||||||
*
|
|
||||||
* Safe Area Insets: These obstructs [frameView] only partially, e.g. display cutout.
|
|
||||||
* We want the frame to be rendered behind these cutouts to provide fully immersive
|
|
||||||
* experience, but also allow user to pan out of these insets when they need to
|
|
||||||
* interact with remote content. These are implemented as 'safe area' in [FrameState].
|
|
||||||
*
|
|
||||||
* All insets are in window coordinates.
|
|
||||||
*
|
|
||||||
************************************************************************************/
|
|
||||||
private var windowInsets = WindowInsetsCompat.Builder().build()
|
|
||||||
private var virtualKeyInsets = Insets.NONE // Insets caused by virtual keys
|
|
||||||
|
|
||||||
private fun updateWindowInsets(insetsCompat: WindowInsetsCompat) {
|
|
||||||
// Copy constructor of WindowInsetsCompat is partially broken on API below 30.
|
|
||||||
// We are manually generating insets on API <30 anyway, so don't need to create a copy.
|
|
||||||
windowInsets = if (SDK_INT < 30) insetsCompat else WindowInsetsCompat(insetsCompat)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateVirtualKeyInsets(vkRoot: View) {
|
|
||||||
if (vkRoot.isVisible) {
|
|
||||||
val vkLocation = intArrayOf(0, 0)
|
|
||||||
vkRoot.getLocationInWindow(vkLocation)
|
|
||||||
val b = max(0, window.decorView.height - vkLocation[1])
|
|
||||||
|
|
||||||
if (virtualKeyInsets.bottom != b)
|
|
||||||
virtualKeyInsets = Insets.of(0, 0, 0, b)
|
|
||||||
} else {
|
|
||||||
virtualKeyInsets = Insets.NONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applyInsets() {
|
|
||||||
val opaqueInsets = listOf(windowInsets.getInsets(Type.ime()), windowInsets.getInsets(Type.navigationBars()))
|
|
||||||
val maxOpaqueInsets = opaqueInsets.fold(Insets.NONE) { a, i -> Insets.max(a, i) }
|
|
||||||
applyOpaqueInsets(maxOpaqueInsets)
|
|
||||||
|
|
||||||
//val cutoutInsets = windowInsets.getInsets(Type.displayCutout())
|
|
||||||
//val cornerInsets = calculateCornerInsets(windowInsets)
|
|
||||||
val safeAreaInsets = listOf(maxOpaqueInsets, /*cutoutInsets, cornerInsets,*/ virtualKeyInsets)
|
|
||||||
val maxSafeAreaInsets = safeAreaInsets.fold(Insets.NONE) { a, i -> Insets.max(a, i) }
|
|
||||||
applySafeAreaInsets(maxSafeAreaInsets)
|
|
||||||
|
|
||||||
// TODO: Apply insets to drawer
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applyOpaqueInsets(opaqueInsets: Insets) {
|
|
||||||
// Guess if IME is closing
|
|
||||||
if (!windowInsets.isVisible(Type.ime()) && rootView.paddingBottom != 0)
|
|
||||||
virtualKeys.onKeyboardClose()
|
|
||||||
|
|
||||||
val insets = windowInsetsToViewInsets(opaqueInsets, rootView)
|
|
||||||
if (rootView.paddingRight != insets.right || rootView.paddingBottom != insets.bottom)
|
|
||||||
rootView.updatePadding(0, 0, insets.right, insets.bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applySafeAreaInsets(safeAreaInsets: Insets) {
|
|
||||||
val insets = windowInsetsToViewInsets(safeAreaInsets, frameView)
|
|
||||||
val safeArea = Rect(insets.left,
|
|
||||||
insets.top,
|
|
||||||
frameView.width - insets.right,
|
|
||||||
frameView.height - insets.bottom)
|
|
||||||
|
|
||||||
viewModel.setSafeArea(RectF(safeArea))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns insets caused by rounded corners.
|
|
||||||
*
|
|
||||||
* Android does not provide pre-calculated insets for rounded screen corners.
|
|
||||||
* Information about corners is available since API 31. We use this information
|
|
||||||
* to calculate effective insets.
|
|
||||||
*/
|
|
||||||
private fun calculateCornerInsets(windowInsetsCompat: WindowInsetsCompat): Insets {
|
|
||||||
val windowInsets = windowInsetsCompat.toWindowInsets()
|
|
||||||
if (SDK_INT < 31 || windowInsets == null)
|
|
||||||
return Insets.NONE
|
|
||||||
|
|
||||||
val wWidth = window.decorView.width
|
|
||||||
val wHeight = window.decorView.height
|
|
||||||
|
|
||||||
var l = 0
|
|
||||||
var t = 0
|
|
||||||
var r = 0
|
|
||||||
var b = 0
|
|
||||||
|
|
||||||
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)?.center?.let {
|
|
||||||
t = max(t, it.y)
|
|
||||||
l = max(l, it.x)
|
|
||||||
}
|
|
||||||
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)?.center?.let {
|
|
||||||
t = max(t, it.y)
|
|
||||||
r = max(r, wWidth - it.x)
|
|
||||||
}
|
|
||||||
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT)?.center?.let {
|
|
||||||
b = max(b, wHeight - it.y)
|
|
||||||
l = max(l, it.x)
|
|
||||||
}
|
|
||||||
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT)?.center?.let {
|
|
||||||
b = max(b, wHeight - it.y)
|
|
||||||
r = max(r, wWidth - it.x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Corner insets are only applied along longer axis. Applying along both axis is unnecessary.
|
|
||||||
if (wWidth > wHeight)
|
|
||||||
return Insets.of(l, 0, r, 0)
|
|
||||||
else
|
|
||||||
return Insets.of(0, t, 0, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms given insets [insets] from Window's coordinates to [view]'s coordinates
|
|
||||||
*/
|
|
||||||
private fun windowInsetsToViewInsets(insets: Insets, view: View): Insets {
|
|
||||||
val viewLocation = intArrayOf(0, 0)
|
|
||||||
view.getLocationInWindow(viewLocation)
|
|
||||||
|
|
||||||
// View frame in window coordinates
|
|
||||||
val vl = viewLocation[0]
|
|
||||||
val vt = viewLocation[1]
|
|
||||||
val vr = vl + view.width
|
|
||||||
val vb = vt + view.height
|
|
||||||
|
|
||||||
// Insets applicable to given view
|
|
||||||
val l = max(0, insets.left - vl)
|
|
||||||
val t = max(0, insets.top - vt)
|
|
||||||
val r = max(0, insets.right - (window.decorView.width - vr))
|
|
||||||
val b = max(0, insets.bottom - (window.decorView.height - vb))
|
|
||||||
|
|
||||||
return Insets.of(l, t, r, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.ArrayMap
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
import com.example.tiny_computer.databinding.FragmentCredentialBinding
|
|
||||||
import com.gaurav.avnc.model.LoginInfo
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel.State.Companion.isConnected
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows user to enter login information.
|
|
||||||
*
|
|
||||||
* There are different types of login information ([LoginInfo.Type]),
|
|
||||||
* but all of them basically boils down to a username/password combo.
|
|
||||||
*
|
|
||||||
* User can choose to "remember" the information, in which case it will be
|
|
||||||
* saved in the profile.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class LoginFragment : DialogFragment() {
|
|
||||||
private lateinit var binding: FragmentCredentialBinding
|
|
||||||
private val viewModel by activityViewModels<VncViewModel>()
|
|
||||||
private val loginType by lazy { viewModel.loginInfoRequest.value!! }
|
|
||||||
private val loginInfo by lazy { getLoginInfoFromProfile(viewModel.profile) }
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
binding = FragmentCredentialBinding.inflate(layoutInflater, null, false)
|
|
||||||
|
|
||||||
binding.loginInfo = loginInfo
|
|
||||||
binding.usernameLayout.isVisible = loginInfo.username.isBlank() && loginType == LoginInfo.Type.VNC_CREDENTIAL
|
|
||||||
binding.passwordLayout.isVisible = loginInfo.password.isBlank()
|
|
||||||
binding.remember.isVisible = viewModel.profile.ID != 0L && loginType != LoginInfo.Type.SSH_KEY_PASSWORD
|
|
||||||
|
|
||||||
if (loginType == LoginInfo.Type.SSH_KEY_PASSWORD) {
|
|
||||||
binding.passwordLayout.setHint(R.string.hint_key_password)
|
|
||||||
binding.pkPasswordMsg.isVisible = viewModel.profile.sshPrivateKeyPassword.isNotBlank()
|
|
||||||
}
|
|
||||||
|
|
||||||
setupAutoComplete()
|
|
||||||
isCancelable = false
|
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(getTitle())
|
|
||||||
.setView(binding.root)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> onOk() }
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> onCancel() }
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getTitle() = when (loginType) {
|
|
||||||
LoginInfo.Type.VNC_PASSWORD,
|
|
||||||
LoginInfo.Type.VNC_CREDENTIAL -> R.string.title_vnc_login
|
|
||||||
LoginInfo.Type.SSH_PASSWORD -> R.string.title_ssh_login
|
|
||||||
LoginInfo.Type.SSH_KEY_PASSWORD -> R.string.title_unlock_private_key
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLoginInfoFromProfile(p: ServerProfile): LoginInfo {
|
|
||||||
return when (loginType) {
|
|
||||||
LoginInfo.Type.VNC_PASSWORD -> LoginInfo(p.name, p.host, "", p.password)
|
|
||||||
LoginInfo.Type.VNC_CREDENTIAL -> LoginInfo(p.name, p.host, p.username, p.password)
|
|
||||||
LoginInfo.Type.SSH_PASSWORD -> LoginInfo(p.name, p.sshHost, "", p.sshPassword)
|
|
||||||
LoginInfo.Type.SSH_KEY_PASSWORD -> LoginInfo(p.name, p.sshHost, "", "" /*p.sshPrivateKeyPassword*/)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setLoginInfoInProfile(p: ServerProfile, l: LoginInfo) {
|
|
||||||
when (loginType) {
|
|
||||||
LoginInfo.Type.VNC_PASSWORD -> p.password = l.password
|
|
||||||
LoginInfo.Type.VNC_CREDENTIAL -> {
|
|
||||||
p.username = l.username
|
|
||||||
p.password = l.password
|
|
||||||
}
|
|
||||||
LoginInfo.Type.SSH_PASSWORD -> p.sshPassword = l.password
|
|
||||||
LoginInfo.Type.SSH_KEY_PASSWORD -> p.sshPrivateKeyPassword = "" /* key password is not saved anymore */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onOk() {
|
|
||||||
loginInfo.password = getRealPassword(loginInfo.password)
|
|
||||||
viewModel.loginInfoRequest.offerResponse(loginInfo)
|
|
||||||
if (binding.remember.isChecked || binding.pkPasswordMsg.isVisible /* to forget saved password */)
|
|
||||||
saveLoginInfo(loginInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCancel() {
|
|
||||||
viewModel.loginInfoRequest.cancelRequest()
|
|
||||||
requireActivity().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If user has asked to remember credentials, we need to save them
|
|
||||||
* to database. But we don't want to save them immediately because
|
|
||||||
* user might have mistyped them. So, we wait until successful
|
|
||||||
* connection before saving them.
|
|
||||||
*/
|
|
||||||
private fun saveLoginInfo(loginInfo: LoginInfo) {
|
|
||||||
// Use activity as owner because this fragment will likely be destroyed before connecting
|
|
||||||
viewModel.state.observe(requireActivity(), object : Observer<VncViewModel.State> {
|
|
||||||
override fun onChanged(value: VncViewModel.State) {
|
|
||||||
if (value.isConnected) {
|
|
||||||
setLoginInfoInProfile(viewModel.profile, loginInfo)
|
|
||||||
viewModel.saveProfile()
|
|
||||||
viewModel.state.removeObserver(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hooks completion adapters
|
|
||||||
*
|
|
||||||
* This feature might not be that useful to end-users, but it saves a lot of time
|
|
||||||
* during development because I have to frequently install/uninstall app, test
|
|
||||||
* different servers running on different addresses/ports.
|
|
||||||
*/
|
|
||||||
private fun setupAutoComplete() {
|
|
||||||
|
|
||||||
viewModel.savedProfiles.observe(this) { profiles ->
|
|
||||||
val logins = profiles.map { getLoginInfoFromProfile(it) }
|
|
||||||
val usernames = logins.map { it.username }.filter { it.isNotEmpty() }.distinct()
|
|
||||||
val passwords = preparePasswordSuggestions(logins)
|
|
||||||
|
|
||||||
if (usernames.isNotEmpty()) {
|
|
||||||
val usernameAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, usernames)
|
|
||||||
binding.username.setAdapter(usernameAdapter)
|
|
||||||
binding.usernameLayout.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passwords.isNotEmpty()) {
|
|
||||||
val passwordAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, passwords)
|
|
||||||
binding.password.setAdapter(passwordAdapter)
|
|
||||||
binding.passwordLayout.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instead of showing plaintext passwords, we show server name & host in suggestion
|
|
||||||
* list. When user taps OK, we convert the suggestion back to real password.
|
|
||||||
*/
|
|
||||||
private val passwordMap = ArrayMap<String, String>()
|
|
||||||
|
|
||||||
private fun preparePasswordSuggestions(list: List<LoginInfo>): List<String> {
|
|
||||||
list.filter { it.password.isNotEmpty() }
|
|
||||||
.map { Pair("from: ${it.name} [${it.host}]", it.password) }
|
|
||||||
.distinct()
|
|
||||||
.toMap(passwordMap)
|
|
||||||
.removeAll(passwordMap.values) //Guard against (very unlikely) clash with real password
|
|
||||||
|
|
||||||
return passwordMap.keys.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRealPassword(typedPassword: String) = passwordMap[typedPassword] ?: typedPassword
|
|
||||||
}
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2024 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.GestureDetector
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.core.view.GravityCompat
|
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel.State
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel.State.Companion.isConnected
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Overview of toolbar layout:
|
|
||||||
*
|
|
||||||
* DrawerLayout
|
|
||||||
* +-------------------+--------------+
|
|
||||||
* | | |
|
|
||||||
* | Toolbar Drawer | |
|
|
||||||
* | [drawerView] | |
|
|
||||||
* | | |
|
|
||||||
* |+---+ | |
|
|
||||||
* || B | | |
|
|
||||||
* || t | | |
|
|
||||||
* || n |+------------+| Scrim |
|
|
||||||
* || s || Flyout || |
|
|
||||||
* |+---++------------+| |
|
|
||||||
* | | |
|
|
||||||
* | | |
|
|
||||||
* | | |
|
|
||||||
* | | |
|
|
||||||
* | | |
|
|
||||||
* +-------------------+--------------+
|
|
||||||
*
|
|
||||||
* User can align the toolbar to left or right edge.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class Toolbar(private val activity: VncActivity, private val dispatcher: Dispatcher) {
|
|
||||||
private val viewModel = activity.viewModel
|
|
||||||
private val binding = activity.binding.toolbar
|
|
||||||
private val drawerLayout = activity.binding.drawerLayout
|
|
||||||
private val drawerView = binding.root
|
|
||||||
private val openWithSwipe = viewModel.pref.viewer.toolbarOpenWithSwipe
|
|
||||||
|
|
||||||
fun initialize() {
|
|
||||||
binding.keyboardBtn.setOnClickListener { activity.showKeyboard(); close() }
|
|
||||||
binding.zoomOptions.setOnLongClickListener { resetZoomToDefault(); close(); true }
|
|
||||||
binding.zoomResetBtn.setOnClickListener { resetZoomToDefault(); close() }
|
|
||||||
binding.zoomResetBtn.setOnLongClickListener { resetZoom(); close(); true }
|
|
||||||
binding.zoomLockBtn.isChecked = viewModel.profile.fZoomLocked
|
|
||||||
binding.zoomLockBtn.setOnCheckedChangeListener { _, checked -> toggleZoomLock(checked); close() }
|
|
||||||
binding.zoomSaveBtn.setOnClickListener { saveZoom(); close() }
|
|
||||||
binding.virtualKeysBtn.setOnClickListener { activity.virtualKeys.show(); close() }
|
|
||||||
|
|
||||||
// Root view is transparent. Click on it should work just like a click in scrim area
|
|
||||||
drawerView.setOnClickListener { close() }
|
|
||||||
|
|
||||||
viewModel.state.observe(activity) { onStateChange(it) }
|
|
||||||
|
|
||||||
setupAlignment()
|
|
||||||
setupFlyoutClose()
|
|
||||||
setupGestureStyleSelection()
|
|
||||||
setupGestureExclusionRect()
|
|
||||||
setupDrawerCloseOnScrimSwipe()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun open() {
|
|
||||||
drawerLayout.openDrawer(drawerView)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
drawerLayout.closeDrawer(drawerView)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toast(@StringRes msgRes: Int) = Toast.makeText(activity, msgRes, Toast.LENGTH_SHORT).show()
|
|
||||||
|
|
||||||
private fun resetZoom() {
|
|
||||||
viewModel.resetZoom()
|
|
||||||
toast(R.string.msg_zoom_reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resetZoomToDefault() {
|
|
||||||
viewModel.resetZoomToDefault()
|
|
||||||
toast(R.string.msg_zoom_reset_default)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleZoomLock(enabled: Boolean) {
|
|
||||||
viewModel.toggleZoomLock(enabled)
|
|
||||||
toast(if (enabled) R.string.msg_zoom_locked else R.string.msg_zoom_unlocked)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveZoom() {
|
|
||||||
viewModel.saveZoom()
|
|
||||||
toast(R.string.msg_zoom_saved)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupGestureStyleSelection() {
|
|
||||||
val styleButtonMap = mapOf(
|
|
||||||
"auto" to R.id.gesture_style_auto,
|
|
||||||
"touchscreen" to R.id.gesture_style_touchscreen,
|
|
||||||
"touchpad" to R.id.gesture_style_touchpad
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.gestureStyleGroup.let { group ->
|
|
||||||
group.check(styleButtonMap[viewModel.profile.gestureStyle] ?: -1)
|
|
||||||
group.setOnCheckedChangeListener { _, id ->
|
|
||||||
for ((k, v) in styleButtonMap)
|
|
||||||
if (v == id) viewModel.profile.gestureStyle = k
|
|
||||||
viewModel.saveProfile()
|
|
||||||
dispatcher.onGestureStyleChanged()
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onStateChange(state: State) {
|
|
||||||
if (state.isConnected)
|
|
||||||
highlightForFirstTimeUser()
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 29)
|
|
||||||
updateGestureExclusionRect()
|
|
||||||
|
|
||||||
updateLockMode(state.isConnected)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the drawer for couple of seconds and then close it.
|
|
||||||
*/
|
|
||||||
private fun highlightForFirstTimeUser() {
|
|
||||||
if (!viewModel.pref.runInfo.hasConnectedSuccessfully) {
|
|
||||||
viewModel.pref.runInfo.hasConnectedSuccessfully = true
|
|
||||||
activity.lifecycleScope.launch {
|
|
||||||
open()
|
|
||||||
delay(2000)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateLockMode(isConnected: Boolean) {
|
|
||||||
if (isConnected && openWithSwipe)
|
|
||||||
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED)
|
|
||||||
else
|
|
||||||
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup gravity & layout direction
|
|
||||||
*/
|
|
||||||
@SuppressLint("RtlHardcoded")
|
|
||||||
private fun setupAlignment() {
|
|
||||||
val gravity = if (viewModel.pref.viewer.toolbarAlignment == "start") GravityCompat.START else GravityCompat.END
|
|
||||||
|
|
||||||
val lp = drawerView.layoutParams as DrawerLayout.LayoutParams
|
|
||||||
lp.gravity = gravity
|
|
||||||
drawerView.layoutParams = lp
|
|
||||||
|
|
||||||
// Before layout pass, layoutDirection should be retrieved from Activity config
|
|
||||||
val layoutDirection = activity.resources.configuration.layoutDirection
|
|
||||||
val isLeftAligned = Gravity.getAbsoluteGravity(gravity, layoutDirection) == Gravity.LEFT
|
|
||||||
|
|
||||||
// We need the layout direction based on alignment rather than language/locale
|
|
||||||
// so that flyouts and button icons are properly ordered.
|
|
||||||
drawerView.layoutDirection = if (isLeftAligned) View.LAYOUT_DIRECTION_LTR else View.LAYOUT_DIRECTION_RTL
|
|
||||||
|
|
||||||
// Let the gesture group have natural layout as it contains text elements
|
|
||||||
binding.gestureStyleGroup.layoutDirection = layoutDirection
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup gesture exclusion updates
|
|
||||||
*/
|
|
||||||
private fun setupGestureExclusionRect() {
|
|
||||||
if (Build.VERSION.SDK_INT >= 29) {
|
|
||||||
binding.primaryButtons.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
||||||
updateGestureExclusionRect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add System Gesture exclusion rects to allow toolbar opening when gesture navigation is active.
|
|
||||||
* Note: Some ROMs, e.g. MIUI, completely ignore whatever is set here.
|
|
||||||
*/
|
|
||||||
@RequiresApi(29)
|
|
||||||
private fun updateGestureExclusionRect() {
|
|
||||||
if (!openWithSwipe || !viewModel.state.value.isConnected) {
|
|
||||||
drawerLayout.systemGestureExclusionRects = listOf()
|
|
||||||
} else {
|
|
||||||
// Area covered by primaryButtons, in drawerLayout's coordinate space
|
|
||||||
val rect = Rect(drawerView.left, binding.primaryButtons.top, drawerView.right, binding.primaryButtons.bottom)
|
|
||||||
|
|
||||||
if (rect.left < 0) rect.offset(-rect.left, 0)
|
|
||||||
if (rect.right > drawerLayout.width) rect.offset(-(rect.right - drawerLayout.width), 0)
|
|
||||||
|
|
||||||
if (viewModel.pref.viewer.fullscreen) {
|
|
||||||
// For fullscreen activities, Android does not enforce the height limit of exclusion area.
|
|
||||||
// We could use the entire height for opening toolbar, but that will completely disable gestures.
|
|
||||||
// So we pad by one-third of available space in each direction
|
|
||||||
val padding = (drawerLayout.height - rect.height()) / 6
|
|
||||||
if (padding > 0) {
|
|
||||||
rect.top -= padding
|
|
||||||
rect.bottom += padding
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawerLayout.systemGestureExclusionRects = listOf(rect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close flyouts after drawer is closed.
|
|
||||||
*
|
|
||||||
* We can't do this in [close] because that will change toolbar width _while_ drawer
|
|
||||||
* is closing. This can conflict with close animation, and mess with internal calculations
|
|
||||||
* of DrawerLayout, resulting in failure of close operation.
|
|
||||||
*/
|
|
||||||
private fun setupFlyoutClose() {
|
|
||||||
drawerLayout.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {
|
|
||||||
override fun onDrawerClosed(closedView: View) {
|
|
||||||
if (closedView == drawerView) {
|
|
||||||
binding.zoomOptions.isChecked = false
|
|
||||||
binding.gestureStyleToggle.isChecked = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normally, drawers in [DrawerLayout] are closed by two gestures:
|
|
||||||
* 1. Swipe 'on' the drawer
|
|
||||||
* 2. Tap inside Scrim (dimmed region outside of drawer)
|
|
||||||
*
|
|
||||||
* Notably, swiping inside scrim area does NOT hide the drawer. This can be jarring
|
|
||||||
* to users if drawer is relatively small & most of the layout area acts as scrim.
|
|
||||||
* The toolbar drawer is affected by this issue.
|
|
||||||
*
|
|
||||||
* This function attempts to detect these swipe gestures and close the drawer
|
|
||||||
* when they happen.
|
|
||||||
*
|
|
||||||
* Note: It will set a custom TouchListener on [drawerLayout].
|
|
||||||
*/
|
|
||||||
@SuppressLint("ClickableViewAccessibility", "RtlHardcoded")
|
|
||||||
private fun setupDrawerCloseOnScrimSwipe() {
|
|
||||||
drawerLayout.setOnTouchListener(object : View.OnTouchListener {
|
|
||||||
var drawerOpen = false
|
|
||||||
var drawerGravity = Gravity.LEFT
|
|
||||||
|
|
||||||
val detector = GestureDetector(drawerLayout.context, object : GestureDetector.SimpleOnGestureListener() {
|
|
||||||
|
|
||||||
override fun onFling(e1: MotionEvent?, e2: MotionEvent, vX: Float, vY: Float): Boolean {
|
|
||||||
if ((drawerGravity == Gravity.LEFT && vX < 0) || (drawerGravity == Gravity.RIGHT && vX > 0)) {
|
|
||||||
close()
|
|
||||||
drawerOpen = false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
|
||||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
|
||||||
drawerOpen = drawerLayout.isDrawerOpen(drawerView)
|
|
||||||
drawerGravity = Gravity.getAbsoluteGravity(
|
|
||||||
(drawerView.layoutParams as DrawerLayout.LayoutParams).gravity,
|
|
||||||
drawerLayout.layoutDirection) and Gravity.HORIZONTAL_GRAVITY_MASK
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drawerOpen)
|
|
||||||
detector.onTouchEvent(event)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,543 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.PointF
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.GestureDetector
|
|
||||||
import android.view.GestureDetector.SimpleOnGestureListener
|
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import android.view.InputDevice
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.ScaleGestureDetector
|
|
||||||
import android.view.ViewConfiguration
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
|
||||||
import com.gaurav.avnc.vnc.PointerButton
|
|
||||||
import kotlin.math.PI
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.atan2
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for touch events. It detects various gestures and notifies [dispatcher].
|
|
||||||
*/
|
|
||||||
class TouchHandler(private val viewModel: VncViewModel, private val dispatcher: Dispatcher)
|
|
||||||
: ScaleGestureDetector.OnScaleGestureListener, SimpleOnGestureListener() {
|
|
||||||
|
|
||||||
//Extension to easily access touch position
|
|
||||||
private fun MotionEvent.point() = PointF(x, y)
|
|
||||||
|
|
||||||
/****************************************************************************************
|
|
||||||
* Touch Event receivers
|
|
||||||
*
|
|
||||||
* Note: On some devices, 'Source' property for Stylus events is set to both
|
|
||||||
* [InputDevice.SOURCE_STYLUS] & [InputDevice.SOURCE_MOUSE]. Hence, we should
|
|
||||||
* pass the events to [handleStylusEvent] before [handleMouseEvent].
|
|
||||||
****************************************************************************************/
|
|
||||||
|
|
||||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
val handled = handleStylusEvent(event) || handleMouseEvent(event) || handleGestureEvent(event)
|
|
||||||
handleGestureStartStop(event)
|
|
||||||
return handled
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onGenericMotionEvent(event: MotionEvent): Boolean {
|
|
||||||
return onHoverEvent(event) || handleStylusEvent(event) || handleMouseEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onHoverEvent(event: MotionEvent): Boolean {
|
|
||||||
if (event.actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
|
|
||||||
lastHoverPoint = event.point()
|
|
||||||
dispatcher.onMouseMove(lastHoverPoint)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleGestureStartStop(event: MotionEvent) {
|
|
||||||
when (event.actionMasked) {
|
|
||||||
MotionEvent.ACTION_DOWN -> dispatcher.onGestureStart()
|
|
||||||
MotionEvent.ACTION_UP,
|
|
||||||
MotionEvent.ACTION_CANCEL -> dispatcher.onGestureStop(event.point())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for back-press interception
|
|
||||||
private var lastHoverPoint = PointF()
|
|
||||||
fun onMouseBack() {
|
|
||||||
dispatcher.onMouseBack(lastHoverPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************************************
|
|
||||||
* Mouse
|
|
||||||
****************************************************************************************/
|
|
||||||
private val mousePassthrough = viewModel.pref.input.mousePassthrough
|
|
||||||
|
|
||||||
private fun handleMouseEvent(e: MotionEvent): Boolean {
|
|
||||||
if (Build.VERSION.SDK_INT < 23 || !mousePassthrough || !e.isFromSource(InputDevice.SOURCE_MOUSE))
|
|
||||||
return false
|
|
||||||
|
|
||||||
val p = e.point()
|
|
||||||
|
|
||||||
when (e.actionMasked) {
|
|
||||||
MotionEvent.ACTION_BUTTON_PRESS -> dispatcher.onMouseButtonDown(convertButton(e.actionButton), p)
|
|
||||||
MotionEvent.ACTION_BUTTON_RELEASE -> dispatcher.onMouseButtonUp(convertButton(e.actionButton), p)
|
|
||||||
MotionEvent.ACTION_MOVE -> dispatcher.onMouseMove(p)
|
|
||||||
|
|
||||||
MotionEvent.ACTION_SCROLL -> {
|
|
||||||
val hs = e.getAxisValue(MotionEvent.AXIS_HSCROLL)
|
|
||||||
val vs = e.getAxisValue(MotionEvent.AXIS_VSCROLL)
|
|
||||||
dispatcher.onMouseScroll(p, hs, vs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow touchpad gestures to be passed on to GestureDetector
|
|
||||||
return !(e.buttonState == 0 && e.getToolType(0) != MotionEvent.TOOL_TYPE_MOUSE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert from [MotionEvent] button to [PointerButton]
|
|
||||||
*/
|
|
||||||
private fun convertButton(button: Int) = when (button) {
|
|
||||||
MotionEvent.BUTTON_PRIMARY -> PointerButton.Left
|
|
||||||
MotionEvent.BUTTON_SECONDARY -> PointerButton.Right
|
|
||||||
MotionEvent.BUTTON_TERTIARY -> PointerButton.Middle
|
|
||||||
else -> PointerButton.None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/****************************************************************************************
|
|
||||||
* Stylus
|
|
||||||
****************************************************************************************/
|
|
||||||
private val stylusGestureDetector = GestureDetector(viewModel.app, StylusGestureListener())
|
|
||||||
|
|
||||||
private fun handleStylusEvent(event: MotionEvent): Boolean {
|
|
||||||
if (event.isFromSource(InputDevice.SOURCE_STYLUS) &&
|
|
||||||
event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
|
|
||||||
stylusGestureDetector.onTouchEvent(event)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class StylusGestureListener : SimpleOnGestureListener() {
|
|
||||||
private var scrolling = false
|
|
||||||
|
|
||||||
override fun onDown(e: MotionEvent): Boolean {
|
|
||||||
scrolling = false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
|
||||||
dispatcher.onStylusTap(e.point())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
|
||||||
dispatcher.onStylusDoubleTap(e.point())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongPress(e: MotionEvent) {
|
|
||||||
viewModel.frameViewRef.get()?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
dispatcher.onStylusLongPress(e.point())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
|
||||||
// Scrolling with stylus button pressed is currently used for scale gesture
|
|
||||||
if (e1 != null && e2.buttonState and MotionEvent.BUTTON_STYLUS_PRIMARY == 0) {
|
|
||||||
|
|
||||||
// When scrolling starts, we need to send the first event at initial touch-point.
|
|
||||||
// Otherwise, we will loose the small distance (touch-slope) required by onScroll().
|
|
||||||
if (!scrolling) {
|
|
||||||
scrolling = true
|
|
||||||
dispatcher.onStylusScroll(e1.point())
|
|
||||||
}
|
|
||||||
dispatcher.onStylusScroll(e2.point())
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/****************************************************************************************
|
|
||||||
* Finger Gestures (and everything else beside mouse & stylus)
|
|
||||||
****************************************************************************************/
|
|
||||||
private val scaleDetector = ScaleGestureDetector(viewModel.app, this).apply { isQuickScaleEnabled = false }
|
|
||||||
private val gestureDetector = GestureDetectorEx(viewModel.app, FingerGestureListener())
|
|
||||||
private val swipeVsScale = SwipeVsScale()
|
|
||||||
private val longPressSwipeEnabled = viewModel.pref.input.gesture.longPressSwipeEnabled
|
|
||||||
private val swipeSensitivity = viewModel.pref.input.gesture.swipeSensitivity
|
|
||||||
|
|
||||||
|
|
||||||
private fun handleGestureEvent(event: MotionEvent): Boolean {
|
|
||||||
swipeVsScale.onTouchEvent(event)
|
|
||||||
scaleDetector.onTouchEvent(event)
|
|
||||||
return gestureDetector.onTouchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScaleBegin(detector: ScaleGestureDetector) = true
|
|
||||||
override fun onScaleEnd(detector: ScaleGestureDetector) {}
|
|
||||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
|
||||||
if (swipeVsScale.shouldScale())
|
|
||||||
dispatcher.onScale(detector.scaleFactor, detector.focusX, detector.focusY)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class FingerGestureListener : GestureDetectorEx.GestureListenerEx {
|
|
||||||
|
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent) = dispatcher.onTap1(e.point())
|
|
||||||
override fun onDoubleTapConfirmed(e: MotionEvent) = dispatcher.onDoubleTap(e.point())
|
|
||||||
|
|
||||||
override fun onMultiFingerTap(e: MotionEvent, fingerCount: Int) {
|
|
||||||
when (fingerCount) {
|
|
||||||
2 -> dispatcher.onTap2(e.point())
|
|
||||||
// Taps by 3+ fingers are not exposed yet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongPress(e: MotionEvent) {
|
|
||||||
viewModel.frameViewRef.get()?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
|
|
||||||
// If long-press-swipe is disabled, we can dispatch long-press immediately
|
|
||||||
if (!longPressSwipeEnabled) dispatcher.onLongPress(e.point())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongPressConfirmed(e: MotionEvent) {
|
|
||||||
if (longPressSwipeEnabled) dispatcher.onLongPress(e.point())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onScroll(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float) {
|
|
||||||
val startPoint = e1.point()
|
|
||||||
val currentPoint = e2.point()
|
|
||||||
val normalizedDx = dx * swipeSensitivity
|
|
||||||
val normalizedDy = dy * swipeSensitivity
|
|
||||||
|
|
||||||
when (e2.pointerCount) {
|
|
||||||
1 -> dispatcher.onSwipe1(startPoint, currentPoint, normalizedDx, normalizedDy)
|
|
||||||
2 -> if (swipeVsScale.shouldSwipe())
|
|
||||||
dispatcher.onSwipe2(startPoint, currentPoint, normalizedDx, normalizedDy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScrollAfterLongPress(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float) {
|
|
||||||
dispatcher.onLongPressSwipe(e1.point(), e2.point(), dx, dy)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScrollAfterDoubleTap(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float) {
|
|
||||||
dispatcher.onDoubleTapSwipe(e1.point(), e2.point(), dx, dy)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFling(velocityX: Float, velocityY: Float) {
|
|
||||||
dispatcher.onFling(velocityX, velocityY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stock [GestureDetector] only detects the most common gestures. But we need to
|
|
||||||
* detect some more gestures to provide maximum flexibility to the user.
|
|
||||||
*
|
|
||||||
* [GestureDetectorEx] is used to for this purpose. It internally uses stock
|
|
||||||
* [GestureDetector], and some custom event processing to detect more gestures.
|
|
||||||
*/
|
|
||||||
private class GestureDetectorEx(context: Context, val listener: GestureListenerEx) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detected gestures. Some of these come directly from stock [GestureDetector],
|
|
||||||
* while the following are custom detected:
|
|
||||||
*
|
|
||||||
* [onDoubleTapConfirmed]
|
|
||||||
* To support double-tap-swipe gesture, double-tap is not immediately triggered on
|
|
||||||
* ACTION_DOWN of second tap. Instead, [doubleTapDetected] flag is set, and when
|
|
||||||
* final ACTION_UP is received (within timeout), [onDoubleTapConfirmed] is called.
|
|
||||||
*
|
|
||||||
* [onMultiFingerTap]
|
|
||||||
* Maximum number of fingers that went down is tracked in [maxFingerDown]. If
|
|
||||||
* ACTION_UP is received within a timeout, and more than one finger went down
|
|
||||||
* without any scrolling, [onMultiFingerTap] is called.
|
|
||||||
*
|
|
||||||
* [onLongPressConfirmed]
|
|
||||||
* Similar to [onDoubleTapConfirmed], to support long-press-swipe, we wait for
|
|
||||||
* ACTION_UP to confirm long-press.
|
|
||||||
* Note: [onLongPress] is always called immediately. It enables haptic feedback
|
|
||||||
* and supports cases where waiting for [onLongPressConfirmed] is not necessary.
|
|
||||||
*
|
|
||||||
* [onScrollAfterDoubleTap]
|
|
||||||
* This is the double-tap-swipe gesture. If scrolling after [doubleTapDetected] flag
|
|
||||||
* is set, [onScrollAfterDoubleTap] is called.
|
|
||||||
*
|
|
||||||
* [onScrollAfterLongPress]
|
|
||||||
* This is the long-press-swipe gesture. If scrolling after [longPressDetected] flag
|
|
||||||
* is set, [onScrollAfterLongPress] is called.
|
|
||||||
*/
|
|
||||||
interface GestureListenerEx {
|
|
||||||
fun onSingleTapConfirmed(e: MotionEvent)
|
|
||||||
fun onDoubleTapConfirmed(e: MotionEvent)
|
|
||||||
fun onMultiFingerTap(e: MotionEvent, fingerCount: Int)
|
|
||||||
|
|
||||||
fun onLongPress(e: MotionEvent)
|
|
||||||
fun onLongPressConfirmed(e: MotionEvent)
|
|
||||||
|
|
||||||
fun onScroll(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float)
|
|
||||||
fun onScrollAfterLongPress(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float)
|
|
||||||
fun onScrollAfterDoubleTap(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float)
|
|
||||||
|
|
||||||
fun onFling(velocityX: Float, velocityY: Float)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stock [GestureDetector] has two unwanted behaviours:
|
|
||||||
* - If long-press or double-tap is detected, scroll events will not be reported anymore.
|
|
||||||
* - If you don't lift the finger after double-tap, a long-press will be triggered.
|
|
||||||
*
|
|
||||||
* Fortunately, [GestureDetector] lets us disable long-press detection, which allows us
|
|
||||||
* to use a combination of multiple [GestureDetector]s to overcome the restrictions:
|
|
||||||
*
|
|
||||||
* - +------------------+
|
|
||||||
* - +->| [innerDetector1] |
|
|
||||||
* - | +------------------+
|
|
||||||
* - | (tap, long-press)
|
|
||||||
* - +----------------+ event |
|
|
||||||
* - | [onTouchEvent] |---------+
|
|
||||||
* - +----------------+ |
|
|
||||||
* - |
|
|
||||||
* - | +------------------+ double-tap event +------------------+
|
|
||||||
* - +->| [innerDetector2] |-------------------->| [innerDetector3] |
|
|
||||||
* - +------------------+ +------------------+
|
|
||||||
* - (double-tap) (double-tap-swipe)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private val innerDetector1 = GestureDetector(context, InnerListener1())
|
|
||||||
private val innerDetector2 = GestureDetector(context, InnerListener2()).apply { setIsLongpressEnabled(false) }
|
|
||||||
private val innerDetector3 = GestureDetector(context, InnerListener3()).apply { setIsLongpressEnabled(false) }
|
|
||||||
|
|
||||||
private var longPressDetected = false
|
|
||||||
private var doubleTapDetected = false
|
|
||||||
private var scrolling = false
|
|
||||||
private var maxFingerDown = 0
|
|
||||||
private var currentDownEvent: MotionEvent? = null
|
|
||||||
|
|
||||||
|
|
||||||
private inner class InnerListener1 : SimpleOnGestureListener() {
|
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
|
||||||
listener.onSingleTapConfirmed(e)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongPress(e: MotionEvent) {
|
|
||||||
if (doubleTapDetected)
|
|
||||||
return // Ignore long-press triggered during double-tap-swipe
|
|
||||||
|
|
||||||
longPressDetected = true
|
|
||||||
listener.onLongPress(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
|
|
||||||
listener.onFling(velocityX, velocityY)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class InnerListener2 : SimpleOnGestureListener() {
|
|
||||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
|
||||||
doubleTapDetected = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDoubleTapEvent(e: MotionEvent) = innerDetector3.onTouchEvent(e)
|
|
||||||
|
|
||||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float) = handleScroll(e1, e2, dx, dy)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class InnerListener3 : SimpleOnGestureListener() {
|
|
||||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float) = handleScroll(e1, e2, dx, dy)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float): Boolean {
|
|
||||||
e1 ?: return false
|
|
||||||
if (!scrolling) {
|
|
||||||
scrolling = true
|
|
||||||
// Send first scroll event on initial touch-down point, because GestureDetector
|
|
||||||
// requires certain amount of finger movement before scroll is triggered, and
|
|
||||||
// we don't want to 'loose' that small movement.
|
|
||||||
callOnScroll(e1, e1, 0f, 0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
callOnScroll(e1, e2, -dx, -dy)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun callOnScroll(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float) {
|
|
||||||
if (doubleTapDetected)
|
|
||||||
listener.onScrollAfterDoubleTap(e1, e2, dx, dy)
|
|
||||||
else if (longPressDetected)
|
|
||||||
listener.onScrollAfterLongPress(e1, e2, dx, dy)
|
|
||||||
else
|
|
||||||
listener.onScroll(e1, e2, dx, dy)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event receiver
|
|
||||||
*/
|
|
||||||
fun onTouchEvent(e: MotionEvent): Boolean {
|
|
||||||
innerDetector1.onTouchEvent(e)
|
|
||||||
innerDetector2.onTouchEvent(e)
|
|
||||||
|
|
||||||
when (e.actionMasked) {
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
|
||||||
maxFingerDown = 1
|
|
||||||
currentDownEvent = MotionEvent.obtain(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
|
||||||
maxFingerDown = max(maxFingerDown, e.pointerCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_UP -> {
|
|
||||||
currentDownEvent?.let { downEvent ->
|
|
||||||
if (longPressDetected && !doubleTapDetected && !scrolling && maxFingerDown <= 1)
|
|
||||||
listener.onLongPressConfirmed(downEvent)
|
|
||||||
|
|
||||||
if (doubleTapDetected && !longPressDetected && !scrolling && maxFingerDown <= 1)
|
|
||||||
listener.onDoubleTapConfirmed(downEvent)
|
|
||||||
|
|
||||||
val gestureDuration = (e.eventTime - downEvent.eventTime)
|
|
||||||
if (maxFingerDown > 1 && !scrolling && gestureDuration < ViewConfiguration.getDoubleTapTimeout())
|
|
||||||
listener.onMultiFingerTap(downEvent, maxFingerDown)
|
|
||||||
}
|
|
||||||
|
|
||||||
reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_CANCEL -> reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reset() {
|
|
||||||
longPressDetected = false
|
|
||||||
doubleTapDetected = false
|
|
||||||
scrolling = false
|
|
||||||
maxFingerDown = 0
|
|
||||||
currentDownEvent?.recycle()
|
|
||||||
currentDownEvent = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swipe vs Scale detector.
|
|
||||||
*
|
|
||||||
* Many two-finger gestures are detected as both swipe & scale gestures because
|
|
||||||
* [GestureDetector] & [ScaleGestureDetector] work independently. This works
|
|
||||||
* very well when two-finger swipe pref is set to 'pan' (default value).
|
|
||||||
* But when the pref is set to 'remote-scroll', this independent detection
|
|
||||||
* becomes an issue. When user tries to scale, it frequently triggers remote
|
|
||||||
* scrolling. And when user tries to scroll, it triggers scaling.
|
|
||||||
*
|
|
||||||
* This class tries to clearly differentiate between these two gestures.
|
|
||||||
* It works by tracking the fingers, and calculating the angle between two paths.
|
|
||||||
* Then [decide] between two gestures by comparing the angle to some thresholds.
|
|
||||||
*
|
|
||||||
* This class can mis-detect some gestures because fingers don't always move
|
|
||||||
* perfectly, but it does provide huge improvement over existing situation.
|
|
||||||
*/
|
|
||||||
private inner class SwipeVsScale {
|
|
||||||
private val enabled = viewModel.pref.input.gesture.swipe2 == "remote-scroll"
|
|
||||||
private var detecting = false
|
|
||||||
private var scaleDetected = false
|
|
||||||
private var swipeDetected = false
|
|
||||||
|
|
||||||
private var f1Id = 0 // Finger 1
|
|
||||||
private var f2Id = 0 // Finger 2
|
|
||||||
private val f1Start = PointF()
|
|
||||||
private val f2Start = PointF()
|
|
||||||
private val f1Current = PointF()
|
|
||||||
private val f2Current = PointF()
|
|
||||||
|
|
||||||
|
|
||||||
fun onTouchEvent(e: MotionEvent) {
|
|
||||||
if (!enabled)
|
|
||||||
return
|
|
||||||
|
|
||||||
when (e.actionMasked) {
|
|
||||||
MotionEvent.ACTION_DOWN,
|
|
||||||
MotionEvent.ACTION_CANCEL,
|
|
||||||
MotionEvent.ACTION_UP,
|
|
||||||
MotionEvent.ACTION_POINTER_UP -> {
|
|
||||||
detecting = false
|
|
||||||
scaleDetected = false
|
|
||||||
swipeDetected = false
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
|
||||||
detecting = e.pointerCount == 2
|
|
||||||
if (detecting) {
|
|
||||||
f1Id = e.getPointerId(0)
|
|
||||||
f2Id = e.getPointerId(1)
|
|
||||||
f1Start.set(e.getX(0), e.getY(0))
|
|
||||||
f2Start.set(e.getX(1), e.getY(1))
|
|
||||||
f1Current.set(f1Start)
|
|
||||||
f2Current.set(f2Start)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
|
||||||
if (detecting) {
|
|
||||||
val i1 = e.findPointerIndex(f1Id)
|
|
||||||
val i2 = e.findPointerIndex(f2Id)
|
|
||||||
if (i1 != -1 && i2 != -1) {
|
|
||||||
f1Current.set(e.getX(i1), e.getY(i1))
|
|
||||||
f2Current.set(e.getX(i2), e.getY(i2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun shouldScale(): Boolean {
|
|
||||||
decide()
|
|
||||||
return !enabled || (detecting && scaleDetected)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun shouldSwipe(): Boolean {
|
|
||||||
decide()
|
|
||||||
return !enabled || (detecting && swipeDetected)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decides if gesture can be considered a swipe/scale
|
|
||||||
*/
|
|
||||||
private fun decide() {
|
|
||||||
if (!detecting)
|
|
||||||
return
|
|
||||||
|
|
||||||
val t1 = theta(f1Start, f1Current)
|
|
||||||
val t2 = theta(f2Start, f2Current)
|
|
||||||
val diff = abs(t1 - t2)
|
|
||||||
|
|
||||||
scaleDetected = diff > 45
|
|
||||||
swipeDetected = diff < 30
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the angle made by line [p1]->[p2] with the positive x-axis.
|
|
||||||
* Returned angle will be in range [0, 360]
|
|
||||||
*/
|
|
||||||
private fun theta(p1: PointF, p2: PointF): Double {
|
|
||||||
val theta = atan2(p2.y - p1.y, p2.x - p1.x)
|
|
||||||
val degree = (theta / PI) * 180
|
|
||||||
return (degree + 360) % 360 // Map [-180, 180] to [0, 360]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewConfiguration
|
|
||||||
import androidx.databinding.BindingAdapter
|
|
||||||
import com.example.tiny_computer.databinding.VirtualKeysBinding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Virtual keys allow the user to input keys which are not normally found on
|
|
||||||
* keyboards but can be useful for controlling remote server.
|
|
||||||
*
|
|
||||||
* This class manages the inflation & visibility of virtual keys.
|
|
||||||
*/
|
|
||||||
class VirtualKeys(activity: VncActivity) {
|
|
||||||
|
|
||||||
private val pref = activity.viewModel.pref.input
|
|
||||||
private val keyHandler = activity.keyHandler
|
|
||||||
private val stub = activity.binding.virtualKeysStub
|
|
||||||
private var openedWithKb = false
|
|
||||||
|
|
||||||
val container: View? get() = stub.root
|
|
||||||
|
|
||||||
fun show() {
|
|
||||||
init()
|
|
||||||
container?.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hide() {
|
|
||||||
container?.visibility = View.GONE
|
|
||||||
openedWithKb = false //Reset flag
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onKeyboardOpen() {
|
|
||||||
if (pref.vkOpenWithKeyboard && container?.visibility != View.VISIBLE) {
|
|
||||||
show()
|
|
||||||
openedWithKb = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onKeyboardClose() {
|
|
||||||
if (openedWithKb) {
|
|
||||||
hide()
|
|
||||||
openedWithKb = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun releaseMetaKeys() {
|
|
||||||
val binding = stub.binding as? VirtualKeysBinding
|
|
||||||
binding?.apply {
|
|
||||||
superBtn.isChecked = false
|
|
||||||
shiftBtn.isChecked = false
|
|
||||||
ctrlBtn.isChecked = false
|
|
||||||
altBtn.isChecked = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun init() {
|
|
||||||
if (stub.isInflated)
|
|
||||||
return
|
|
||||||
|
|
||||||
stub.viewStub?.inflate()
|
|
||||||
val binding = stub.binding as VirtualKeysBinding
|
|
||||||
binding.h = keyHandler
|
|
||||||
binding.showAll = pref.vkShowAll
|
|
||||||
binding.hideBtn.setOnClickListener { hide() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a View is touched, we schedule a callback to to simulate a click.
|
|
||||||
* As long as finger stays on the view, we keep repeating this callback.
|
|
||||||
*
|
|
||||||
* Another option here is to send VNC KeyEvent(down) on [MotionEvent.ACTION_DOWN]
|
|
||||||
* and then send VNC KeyEvent(up) on [MotionEvent.ACTION_UP].
|
|
||||||
*/
|
|
||||||
@BindingAdapter("isRepeatable")
|
|
||||||
fun repeatableKeyBinding(keyView: View, repeatable: Boolean) {
|
|
||||||
if (!repeatable)
|
|
||||||
return
|
|
||||||
|
|
||||||
keyView.setOnTouchListener(object : View.OnTouchListener {
|
|
||||||
private var doRepeat = false
|
|
||||||
|
|
||||||
private fun repeat(v: View) {
|
|
||||||
if (doRepeat) {
|
|
||||||
v.performClick()
|
|
||||||
v.postDelayed({ repeat(v) }, ViewConfiguration.getKeyRepeatDelay().toLong())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
|
||||||
when (event.actionMasked) {
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
|
||||||
doRepeat = true
|
|
||||||
v.postDelayed({ repeat(v) }, ViewConfiguration.getKeyRepeatTimeout().toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_POINTER_DOWN,
|
|
||||||
MotionEvent.ACTION_UP,
|
|
||||||
MotionEvent.ACTION_CANCEL -> {
|
|
||||||
doRepeat = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PictureInPictureParams
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.os.SystemClock
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.Rational
|
|
||||||
import android.view.InputDevice
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.WindowManager
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.os.BundleCompat
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.databinding.DataBindingUtil
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
|
||||||
import com.example.tiny_computer.R
|
|
||||||
import com.example.tiny_computer.databinding.ActivityVncBinding
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
import com.gaurav.avnc.util.DeviceAuthPrompt
|
|
||||||
import com.gaurav.avnc.util.SamsungDex
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel.State.Companion.isConnected
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel.State.Companion.isDisconnected
|
|
||||||
import com.gaurav.avnc.vnc.VncUri
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
/********** [VncActivity] startup helpers *********************************/
|
|
||||||
|
|
||||||
private const val PROFILE_KEY = "com.gaurav.avnc.server_profile"
|
|
||||||
private const val FRAME_STATE_KEY = "com.gaurav.avnc.frame_state"
|
|
||||||
|
|
||||||
fun createVncIntent(context: Context, profile: ServerProfile): Intent {
|
|
||||||
return Intent(context, VncActivity::class.java).apply {
|
|
||||||
putExtra(PROFILE_KEY, profile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startVncActivity(source: Activity, profile: ServerProfile) {
|
|
||||||
source.startActivity(createVncIntent(source, profile))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startVncActivity(source: Activity, uri: VncUri) {
|
|
||||||
startVncActivity(source, uri.toServerProfile())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
private data class SavedFrameState(val frameX: Float, val frameY: Float, val zoom1: Float, val zoom2: Float) : Parcelable
|
|
||||||
|
|
||||||
private fun startVncActivity(source: Activity, profile: ServerProfile, frameState: SavedFrameState) {
|
|
||||||
source.startActivity(createVncIntent(source, profile).also { it.putExtra(FRAME_STATE_KEY, frameState) })
|
|
||||||
}
|
|
||||||
/**************************************************************************/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This activity handles the connection to a VNC server.
|
|
||||||
*/
|
|
||||||
class VncActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
lateinit var viewModel: VncViewModel
|
|
||||||
lateinit var binding: ActivityVncBinding
|
|
||||||
private val dispatcher by lazy { Dispatcher(this) }
|
|
||||||
val touchHandler by lazy { TouchHandler(viewModel, dispatcher) }
|
|
||||||
val keyHandler by lazy { KeyHandler(dispatcher, viewModel.profile.fLegacyKeySym, viewModel.pref) }
|
|
||||||
val virtualKeys by lazy { VirtualKeys(this) }
|
|
||||||
private val serverUnlockPrompt = DeviceAuthPrompt(this)
|
|
||||||
private val layoutManager by lazy { LayoutManager(this) }
|
|
||||||
private val toolbar by lazy { Toolbar(this, dispatcher) }
|
|
||||||
private var restoredFromBundle = false
|
|
||||||
private var wasConnectedWhenStopped = false
|
|
||||||
private var onStartTime = 0L
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
DeviceAuthPrompt.applyFingerprintDialogFix(supportFragmentManager)
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
if (!loadViewModel(savedInstanceState)) {
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.initConnection()
|
|
||||||
|
|
||||||
//Main UI
|
|
||||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_vnc)
|
|
||||||
binding.viewModel = viewModel
|
|
||||||
binding.lifecycleOwner = this
|
|
||||||
binding.frameView.initialize(this)
|
|
||||||
viewModel.frameViewRef = WeakReference(binding.frameView)
|
|
||||||
toolbar.initialize()
|
|
||||||
|
|
||||||
setupLayout()
|
|
||||||
setupServerUnlock()
|
|
||||||
|
|
||||||
//Observers
|
|
||||||
binding.reconnectBtn.setOnClickListener { retryConnection() }
|
|
||||||
viewModel.loginInfoRequest.observe(this) { showLoginDialog() }
|
|
||||||
viewModel.sshHostKeyVerifyRequest.observe(this) { showHostKeyDialog() }
|
|
||||||
viewModel.state.observe(this) { onClientStateChanged(it) }
|
|
||||||
|
|
||||||
savedInstanceState?.let {
|
|
||||||
restoredFromBundle = true
|
|
||||||
wasConnectedWhenStopped = it.getBoolean("wasConnectedWhenStopped")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
binding.frameView.onResume()
|
|
||||||
viewModel.resumeFrameBufferUpdates()
|
|
||||||
onStartTime = SystemClock.uptimeMillis()
|
|
||||||
|
|
||||||
// Refresh framebuffer on activity restart:
|
|
||||||
// - It forces read/write on the socket. This allows us to verify the socket, which might have
|
|
||||||
// been closed by the server while app process was frozen in background
|
|
||||||
// - It also attempts to fix some unusual cases of old updates requests being lost while AVNC
|
|
||||||
// was frozen by the system
|
|
||||||
if (wasConnectedWhenStopped) viewModel.refreshFrameBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
virtualKeys.releaseMetaKeys()
|
|
||||||
binding.frameView.onPause()
|
|
||||||
viewModel.pauseFrameBufferUpdates()
|
|
||||||
wasConnectedWhenStopped = viewModel.state.value.isConnected
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putParcelable(PROFILE_KEY, viewModel.profile)
|
|
||||||
outState.putBoolean("wasConnectedWhenStopped", wasConnectedWhenStopped || viewModel.state.value.isConnected)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadViewModel(savedState: Bundle?): Boolean {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val profile = savedState?.getParcelable(PROFILE_KEY)
|
|
||||||
?: intent.getParcelableExtra<ServerProfile?>(PROFILE_KEY)
|
|
||||||
|
|
||||||
if (profile == null) {
|
|
||||||
Toast.makeText(this, "Error: Missing Server Info", Toast.LENGTH_LONG).show()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val factory = viewModelFactory { initializer { VncViewModel(profile, application) } }
|
|
||||||
viewModel = viewModels<VncViewModel> { factory }.value
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun retryConnection(seamless: Boolean = false) {
|
|
||||||
//We simply create a new activity to force creation of new ViewModel
|
|
||||||
//which effectively restarts the connection.
|
|
||||||
if (!isFinishing) {
|
|
||||||
val savedFrameState = viewModel.frameState.let {
|
|
||||||
SavedFrameState(frameX = it.frameX, frameY = it.frameY, zoom1 = it.zoomScale1, zoom2 = it.zoomScale2)
|
|
||||||
}
|
|
||||||
|
|
||||||
startVncActivity(this, viewModel.profile, savedFrameState)
|
|
||||||
|
|
||||||
if (seamless) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
overridePendingTransition(0, 0)
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupServerUnlock() {
|
|
||||||
serverUnlockPrompt.init(
|
|
||||||
onSuccess = { viewModel.serverUnlockRequest.offerResponse(true) },
|
|
||||||
onFail = { viewModel.serverUnlockRequest.offerResponse(false) }
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.serverUnlockRequest.observe(this) {
|
|
||||||
if (serverUnlockPrompt.canLaunch())
|
|
||||||
serverUnlockPrompt.launch(getString(R.string.title_unlock_dialog))
|
|
||||||
else
|
|
||||||
viewModel.serverUnlockRequest.offerResponse(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLoginDialog() {
|
|
||||||
LoginFragment().show(supportFragmentManager, "LoginDialog")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showHostKeyDialog() {
|
|
||||||
HostKeyFragment().show(supportFragmentManager, "HostKeyFragment")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showKeyboard() {
|
|
||||||
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
||||||
|
|
||||||
binding.frameView.requestFocus()
|
|
||||||
imm.showSoftInput(binding.frameView, 0)
|
|
||||||
|
|
||||||
virtualKeys.onKeyboardOpen()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onClientStateChanged(newState: VncViewModel.State) {
|
|
||||||
val isConnected = newState.isConnected
|
|
||||||
|
|
||||||
binding.frameView.isVisible = isConnected
|
|
||||||
binding.frameView.keepScreenOn = isConnected && viewModel.pref.viewer.keepScreenOn
|
|
||||||
SamsungDex.setMetaKeyCapture(this, isConnected)
|
|
||||||
layoutManager.onConnectionStateChanged()
|
|
||||||
updateStatusContainerVisibility(isConnected)
|
|
||||||
autoReconnect(newState)
|
|
||||||
|
|
||||||
if (isConnected && !restoredFromBundle) {
|
|
||||||
incrementUseCount()
|
|
||||||
restoreFrameState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun incrementUseCount() {
|
|
||||||
viewModel.profile.useCount += 1
|
|
||||||
viewModel.saveProfile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateStatusContainerVisibility(isConnected: Boolean) {
|
|
||||||
binding.statusContainer.isVisible = true
|
|
||||||
binding.statusContainer
|
|
||||||
.animate()
|
|
||||||
.alpha(if (isConnected) 0f else 1f)
|
|
||||||
.withEndAction { binding.statusContainer.isVisible = !isConnected }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreFrameState() {
|
|
||||||
intent.extras?.let { extras ->
|
|
||||||
BundleCompat.getParcelable(extras, FRAME_STATE_KEY, SavedFrameState::class.java)?.let {
|
|
||||||
viewModel.setZoom(it.zoom1, it.zoom2)
|
|
||||||
viewModel.panFrame(it.frameX, it.frameY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var autoReconnecting = false
|
|
||||||
private fun autoReconnect(state: VncViewModel.State) {
|
|
||||||
if (!state.isDisconnected)
|
|
||||||
return
|
|
||||||
|
|
||||||
// If disconnected when coming back from background, try to reconnect immediately
|
|
||||||
if (wasConnectedWhenStopped && (SystemClock.uptimeMillis() - onStartTime) in 0..2000) {
|
|
||||||
Log.d(javaClass.simpleName, "Disconnected while in background, reconnecting ...")
|
|
||||||
retryConnection(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoReconnecting || !viewModel.pref.server.autoReconnect)
|
|
||||||
return
|
|
||||||
|
|
||||||
autoReconnecting = true
|
|
||||||
lifecycleScope.launch {
|
|
||||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
val timeout = 5 //seconds, must be >1
|
|
||||||
repeat(timeout) {
|
|
||||||
binding.autoReconnectProgress.setProgressCompat((100 * it) / (timeout - 1), true)
|
|
||||||
delay(1000)
|
|
||||||
if (it >= (timeout - 1))
|
|
||||||
retryConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************************************************************************
|
|
||||||
* Layout handling.
|
|
||||||
************************************************************************************/
|
|
||||||
private fun setupLayout() {
|
|
||||||
setupOrientation()
|
|
||||||
layoutManager.initialize()
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 28 && viewModel.pref.viewer.drawBehindCutout) {
|
|
||||||
window.attributes = window.attributes.apply {
|
|
||||||
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupOrientation() {
|
|
||||||
val choice = viewModel.profile.screenOrientation.let {
|
|
||||||
if (it != "auto") it else viewModel.pref.viewer.orientation
|
|
||||||
}
|
|
||||||
|
|
||||||
requestedOrientation = when (choice) {
|
|
||||||
"portrait" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
|
||||||
"landscape" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
|
||||||
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
|
||||||
super.onWindowFocusChanged(hasFocus)
|
|
||||||
layoutManager.onWindowFocusChanged(hasFocus)
|
|
||||||
if (hasFocus) viewModel.sendClipboardText()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/************************************************************************************
|
|
||||||
* Picture-in-Picture support
|
|
||||||
************************************************************************************/
|
|
||||||
|
|
||||||
override fun onUserLeaveHint() {
|
|
||||||
super.onUserLeaveHint()
|
|
||||||
enterPiPMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(26)
|
|
||||||
override fun onPictureInPictureModeChanged(inPiP: Boolean, newConfig: Configuration) {
|
|
||||||
super.onPictureInPictureModeChanged(inPiP, newConfig)
|
|
||||||
if (inPiP) {
|
|
||||||
toolbar.close()
|
|
||||||
viewModel.resetZoom()
|
|
||||||
virtualKeys.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enterPiPMode() {
|
|
||||||
val canEnter = viewModel.pref.viewer.pipEnabled && viewModel.client.connected
|
|
||||||
|
|
||||||
if (canEnter && Build.VERSION.SDK_INT >= 26) {
|
|
||||||
|
|
||||||
var w = viewModel.frameState.fbWidth
|
|
||||||
var h = viewModel.frameState.fbHeight
|
|
||||||
if (w <= 0 || h <= 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
// Android require aspect ratio to be less than 2.39
|
|
||||||
w = w.coerceIn(1f, 2.3f * h)
|
|
||||||
h = h.coerceIn(1f, 2.3f * w)
|
|
||||||
|
|
||||||
val aspectRatio = Rational(w.toInt(), h.toInt())
|
|
||||||
val param = PictureInPictureParams.Builder().setAspectRatio(aspectRatio).build()
|
|
||||||
|
|
||||||
try {
|
|
||||||
enterPictureInPictureMode(param)
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
Log.w(javaClass.simpleName, "Cannot enter PiP mode", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************************************************************************
|
|
||||||
* Input
|
|
||||||
************************************************************************************/
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
|
||||||
return keyHandler.onKeyEvent(event) || workarounds(event) || super.onKeyDown(keyCode, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
|
||||||
return keyHandler.onKeyEvent(event) || workarounds(event) || super.onKeyUp(keyCode, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onKeyMultiple(keyCode: Int, repeatCount: Int, event: KeyEvent): Boolean {
|
|
||||||
return keyHandler.onKeyEvent(event) || super.onKeyMultiple(keyCode, repeatCount, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun workarounds(keyEvent: KeyEvent): Boolean {
|
|
||||||
|
|
||||||
//It seems that some device manufacturers are hell-bent on making developers'
|
|
||||||
//life miserable. In their infinite wisdom, they decided that Android apps don't
|
|
||||||
//need Mouse right-click events. It is hardcoded to act as back-press, without
|
|
||||||
//giving apps a chance to handle it. For better or worse, they set the 'source'
|
|
||||||
//for such key events to Mouse, enabling the following workarounds.
|
|
||||||
if (keyEvent.keyCode == KeyEvent.KEYCODE_BACK &&
|
|
||||||
InputDevice.getDevice(keyEvent.deviceId)?.supportsSource(InputDevice.SOURCE_MOUSE) == true &&
|
|
||||||
viewModel.pref.input.interceptMouseBack) {
|
|
||||||
if (keyEvent.action == KeyEvent.ACTION_DOWN)
|
|
||||||
touchHandler.onMouseBack()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc.gl
|
|
||||||
|
|
||||||
import android.opengl.GLES20.GL_FLOAT
|
|
||||||
import android.opengl.GLES20.GL_TRIANGLES
|
|
||||||
import android.opengl.GLES20.glDrawArrays
|
|
||||||
import android.opengl.GLES20.glEnableVertexAttribArray
|
|
||||||
import android.opengl.GLES20.glVertexAttribPointer
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.nio.FloatBuffer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Frame is represented as two triangles:
|
|
||||||
*
|
|
||||||
* [0, fbHeight] +-----------+ [fbWidth, fbHeight]
|
|
||||||
* | /|
|
|
||||||
* | / |
|
|
||||||
* | / |
|
|
||||||
* | / |
|
|
||||||
* [0, 0] +-----------+ [fbWidth, 0]
|
|
||||||
*
|
|
||||||
* Frame texture is mapped onto these triangles.
|
|
||||||
*/
|
|
||||||
class Frame {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val FLOAT_SIZE = 4
|
|
||||||
const val TRIANGLE_COMPONENT = 2 //[x,y]
|
|
||||||
const val TEXTURE_COMPONENT = 2 //[x,y]
|
|
||||||
const val STRIDE = (TEXTURE_COMPONENT + TEXTURE_COMPONENT) * FLOAT_SIZE
|
|
||||||
}
|
|
||||||
|
|
||||||
private var fbWidth = 0F
|
|
||||||
private var fbHeight = 0F
|
|
||||||
private var vertexData: FloatArray
|
|
||||||
private var vertexBuffer: FloatBuffer
|
|
||||||
|
|
||||||
init {
|
|
||||||
vertexData = generateVertexData()
|
|
||||||
vertexBuffer = ByteBuffer.allocateDirect(vertexData.size * 4)
|
|
||||||
.order(ByteOrder.nativeOrder())
|
|
||||||
.asFloatBuffer()
|
|
||||||
.put(vertexData)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates vertex data for frame.
|
|
||||||
*/
|
|
||||||
private fun generateVertexData(): FloatArray {
|
|
||||||
|
|
||||||
//Note: Textures have their own coordinate system. [0,0] represents bottom-left
|
|
||||||
// and [1,1] represents upper-right corner.
|
|
||||||
|
|
||||||
return floatArrayOf(
|
|
||||||
//@formatter:off
|
|
||||||
//Triangle coordinates //Texture coordinates
|
|
||||||
0F, 0F, 0F, 0F,
|
|
||||||
fbWidth, 0F, 1F, 0F,
|
|
||||||
fbWidth, fbHeight, 1F, 1F,
|
|
||||||
|
|
||||||
0F, 0F, 0F, 0F,
|
|
||||||
fbWidth, fbHeight, 1F, 1F,
|
|
||||||
0F, fbHeight, 0F, 1F
|
|
||||||
//@formatter:on
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun bind(program: FrameProgram) {
|
|
||||||
setVertexAttributePointer(0, program.aPositionLocation, TRIANGLE_COMPONENT, STRIDE)
|
|
||||||
setVertexAttributePointer(TRIANGLE_COMPONENT, program.aTextureCoordinatesLocation, TEXTURE_COMPONENT, STRIDE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setVertexAttributePointer(dataOffset: Int, attributeLocation: Int, componentCount: Int, stride: Int) {
|
|
||||||
vertexBuffer.position(dataOffset)
|
|
||||||
glVertexAttribPointer(attributeLocation, componentCount, GL_FLOAT, false, stride, vertexBuffer)
|
|
||||||
glEnableVertexAttribArray(attributeLocation)
|
|
||||||
vertexBuffer.position(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should be called whenever the size of framebuffer is changed.
|
|
||||||
* This size will be used to calculate frame vertices.
|
|
||||||
*/
|
|
||||||
fun updateFbSize(width: Float, height: Float) {
|
|
||||||
if (width == fbWidth && height == fbHeight)
|
|
||||||
return //Nothing to do
|
|
||||||
|
|
||||||
fbWidth = width
|
|
||||||
fbHeight = height
|
|
||||||
|
|
||||||
vertexData = generateVertexData()
|
|
||||||
vertexBuffer.position(0)
|
|
||||||
vertexBuffer.put(vertexData)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun draw() {
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc.gl
|
|
||||||
|
|
||||||
import android.opengl.GLES20.GL_CLAMP_TO_EDGE
|
|
||||||
import android.opengl.GLES20.GL_LINEAR
|
|
||||||
import android.opengl.GLES20.GL_TEXTURE0
|
|
||||||
import android.opengl.GLES20.GL_TEXTURE_2D
|
|
||||||
import android.opengl.GLES20.GL_TEXTURE_MAG_FILTER
|
|
||||||
import android.opengl.GLES20.GL_TEXTURE_MIN_FILTER
|
|
||||||
import android.opengl.GLES20.GL_TEXTURE_WRAP_S
|
|
||||||
import android.opengl.GLES20.GL_TEXTURE_WRAP_T
|
|
||||||
import android.opengl.GLES20.glActiveTexture
|
|
||||||
import android.opengl.GLES20.glBindTexture
|
|
||||||
import android.opengl.GLES20.glGenTextures
|
|
||||||
import android.opengl.GLES20.glGetAttribLocation
|
|
||||||
import android.opengl.GLES20.glGetUniformLocation
|
|
||||||
import android.opengl.GLES20.glTexParameteri
|
|
||||||
import android.opengl.GLES20.glUniform1i
|
|
||||||
import android.opengl.GLES20.glUniformMatrix4fv
|
|
||||||
import android.opengl.GLES20.glUseProgram
|
|
||||||
import android.util.Log
|
|
||||||
import com.example.tiny_computer.BuildConfig
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the GL program used for Frame rendering.
|
|
||||||
*
|
|
||||||
* NOTE: It must be instantiated in an OpenGL context.
|
|
||||||
*/
|
|
||||||
class FrameProgram {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// Attribute constants
|
|
||||||
const val A_POSITION = "a_Position"
|
|
||||||
const val A_TEXTURE_COORDINATES = "a_TextureCoordinates"
|
|
||||||
|
|
||||||
// Uniform constants
|
|
||||||
const val U_PROJECTION = "u_Projection"
|
|
||||||
const val U_TEXTURE_UNIT = "u_TextureUnit"
|
|
||||||
}
|
|
||||||
|
|
||||||
val program = ShaderCompiler.buildProgram(Shaders.VERTEX_SHADER, Shaders.FRAGMENT_SHADER)
|
|
||||||
val aPositionLocation = glGetAttribLocation(program, A_POSITION)
|
|
||||||
val aTextureCoordinatesLocation = glGetAttribLocation(program, A_TEXTURE_COORDINATES)
|
|
||||||
val uProjectionLocation = glGetUniformLocation(program, U_PROJECTION)
|
|
||||||
val uTexUnitLocation = glGetUniformLocation(program, U_TEXTURE_UNIT)
|
|
||||||
val textureId = createTexture()
|
|
||||||
var validated = false
|
|
||||||
|
|
||||||
|
|
||||||
fun setUniforms(projectionMatrix: FloatArray) {
|
|
||||||
glUniformMatrix4fv(uProjectionLocation, 1, false, projectionMatrix, 0)
|
|
||||||
glActiveTexture(GL_TEXTURE0)
|
|
||||||
glBindTexture(GL_TEXTURE_2D, textureId)
|
|
||||||
glUniform1i(uTexUnitLocation, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createTexture(): Int {
|
|
||||||
val texturesObjects = intArrayOf(0)
|
|
||||||
glGenTextures(1, texturesObjects, 0)
|
|
||||||
if (texturesObjects[0] == 0) {
|
|
||||||
Log.e("Texture", "Could not generate texture.")
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texturesObjects[0])
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0)
|
|
||||||
return texturesObjects[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun validate() {
|
|
||||||
if (BuildConfig.DEBUG && !validated) {
|
|
||||||
ShaderCompiler.validateProgram(program)
|
|
||||||
validated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun useProgram() {
|
|
||||||
glUseProgram(program)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc.gl
|
|
||||||
|
|
||||||
import android.opengl.GLES20.GL_COLOR_BUFFER_BIT
|
|
||||||
import android.opengl.GLES20.glClear
|
|
||||||
import android.opengl.GLES20.glClearColor
|
|
||||||
import android.opengl.GLES20.glViewport
|
|
||||||
import android.opengl.GLSurfaceView
|
|
||||||
import android.opengl.Matrix
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
|
||||||
import javax.microedition.khronos.egl.EGLConfig
|
|
||||||
import javax.microedition.khronos.opengles.GL10
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Frame renderer.
|
|
||||||
*/
|
|
||||||
class Renderer(val viewModel: VncViewModel) : GLSurfaceView.Renderer {
|
|
||||||
|
|
||||||
private val projectionMatrix = FloatArray(16)
|
|
||||||
private val hideCursor = viewModel.pref.input.hideRemoteCursor
|
|
||||||
private lateinit var program: FrameProgram
|
|
||||||
private lateinit var frame: Frame
|
|
||||||
|
|
||||||
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
|
|
||||||
glClearColor(0f, 0f, 0f, 1f)
|
|
||||||
|
|
||||||
frame = Frame()
|
|
||||||
program = FrameProgram()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
|
|
||||||
glViewport(0, 0, width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws frame on screen according to current state.
|
|
||||||
*
|
|
||||||
* Y-axis of coordinate system used by OpenGL is in opposite direction (upwards)
|
|
||||||
* relative to Y-axis in screen coordinates (downwards).
|
|
||||||
*
|
|
||||||
* To compensate for this, we invert the Y-coordinates of drawn frame. This is
|
|
||||||
* achieved by:
|
|
||||||
* 1. Using clipping region [-height, 0] instead of [0, height] for Y-axis
|
|
||||||
* 2. Inverting sign of Y-axis position
|
|
||||||
* 3. Inverting sign of Y-axis scale (to flip the frame)
|
|
||||||
*
|
|
||||||
* So the frame is drawn as follows in OpenGL:
|
|
||||||
*
|
|
||||||
* +Y ^
|
|
||||||
* |
|
|
||||||
* |[0,0] [width,0]
|
|
||||||
* -X <-----|---------------------+-------------> +X
|
|
||||||
* | |
|
|
||||||
* | |
|
|
||||||
* | Frame |
|
|
||||||
* | |
|
|
||||||
* | |
|
|
||||||
* +---------------------+
|
|
||||||
* |[0,-height] [width,-height]
|
|
||||||
* |
|
|
||||||
* -Y v
|
|
||||||
*
|
|
||||||
* Doing this inversion here allows rest of the code to not worry about difference
|
|
||||||
* in Y-axis direction.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
override fun onDrawFrame(gl: GL10?) {
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT)
|
|
||||||
|
|
||||||
if (!viewModel.client.connected)
|
|
||||||
return
|
|
||||||
|
|
||||||
val state = viewModel.frameState.getSnapshot()
|
|
||||||
if (state.vpWidth == 0f || state.vpHeight == 0f)
|
|
||||||
return
|
|
||||||
|
|
||||||
Matrix.setIdentityM(projectionMatrix, 0)
|
|
||||||
Matrix.orthoM(projectionMatrix, 0, 0f, state.vpWidth, -state.vpHeight, 0f, -1f, 1f)
|
|
||||||
Matrix.translateM(projectionMatrix, 0, state.frameX, -state.frameY, 0f)
|
|
||||||
Matrix.scaleM(projectionMatrix, 0, state.scale, -state.scale, 1f)
|
|
||||||
|
|
||||||
program.useProgram()
|
|
||||||
program.setUniforms(projectionMatrix)
|
|
||||||
|
|
||||||
viewModel.client.uploadFrameTexture()
|
|
||||||
if (!hideCursor) viewModel.client.uploadCursor()
|
|
||||||
|
|
||||||
frame.updateFbSize(state.fbWidth, state.fbHeight)
|
|
||||||
frame.bind(program)
|
|
||||||
|
|
||||||
program.validate()
|
|
||||||
frame.draw()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc.gl
|
|
||||||
|
|
||||||
import android.opengl.GLES20.GL_COMPILE_STATUS
|
|
||||||
import android.opengl.GLES20.GL_FRAGMENT_SHADER
|
|
||||||
import android.opengl.GLES20.GL_LINK_STATUS
|
|
||||||
import android.opengl.GLES20.GL_VALIDATE_STATUS
|
|
||||||
import android.opengl.GLES20.GL_VERTEX_SHADER
|
|
||||||
import android.opengl.GLES20.glAttachShader
|
|
||||||
import android.opengl.GLES20.glCompileShader
|
|
||||||
import android.opengl.GLES20.glCreateProgram
|
|
||||||
import android.opengl.GLES20.glCreateShader
|
|
||||||
import android.opengl.GLES20.glDeleteProgram
|
|
||||||
import android.opengl.GLES20.glDeleteShader
|
|
||||||
import android.opengl.GLES20.glGetProgramInfoLog
|
|
||||||
import android.opengl.GLES20.glGetProgramiv
|
|
||||||
import android.opengl.GLES20.glGetShaderInfoLog
|
|
||||||
import android.opengl.GLES20.glGetShaderiv
|
|
||||||
import android.opengl.GLES20.glLinkProgram
|
|
||||||
import android.opengl.GLES20.glShaderSource
|
|
||||||
import android.opengl.GLES20.glValidateProgram
|
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
object ShaderCompiler {
|
|
||||||
|
|
||||||
private const val TAG = "ShaderCompiler"
|
|
||||||
|
|
||||||
fun compileShader(type: Int, shaderText: String): Int {
|
|
||||||
val shaderObjectId = glCreateShader(type)
|
|
||||||
if (shaderObjectId == 0) {
|
|
||||||
Log.e(TAG, "Could not create shader object")
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
glShaderSource(shaderObjectId, shaderText)
|
|
||||||
glCompileShader(shaderObjectId)
|
|
||||||
Log.d(TAG, glGetShaderInfoLog(shaderObjectId))
|
|
||||||
|
|
||||||
val status = intArrayOf(0)
|
|
||||||
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, status, 0)
|
|
||||||
if (status[0] == 0) {
|
|
||||||
Log.e(TAG, "Shader compilation failed")
|
|
||||||
glDeleteShader(shaderObjectId)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return shaderObjectId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun linkProgram(vertexShaderId: Int, fragmentShaderId: Int): Int {
|
|
||||||
val programId = glCreateProgram()
|
|
||||||
if (programId == 0) {
|
|
||||||
Log.e(TAG, "Could not create program object")
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
glAttachShader(programId, vertexShaderId)
|
|
||||||
glAttachShader(programId, fragmentShaderId)
|
|
||||||
glLinkProgram(programId)
|
|
||||||
Log.d(TAG, glGetProgramInfoLog(programId))
|
|
||||||
|
|
||||||
val status = intArrayOf(0)
|
|
||||||
glGetProgramiv(programId, GL_LINK_STATUS, status, 0)
|
|
||||||
if (status[0] == 0) {
|
|
||||||
Log.e(TAG, "Program linking failed")
|
|
||||||
glDeleteProgram(programId)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return programId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun validateProgram(programId: Int): Boolean {
|
|
||||||
glValidateProgram(programId)
|
|
||||||
val status = intArrayOf(0)
|
|
||||||
glGetProgramiv(programId, GL_VALIDATE_STATUS, status, 0)
|
|
||||||
Log.d(TAG, "Program [" + programId + "] validation result: " + status[0])
|
|
||||||
Log.d(TAG, "Program [" + programId + "] validation log: " + glGetProgramInfoLog(programId))
|
|
||||||
return status[0] != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildProgram(vertexShaderSource: String, fragmentShaderSource: String): Int {
|
|
||||||
val vertexShaderId = compileShader(GL_VERTEX_SHADER, vertexShaderSource)
|
|
||||||
val fragmentShaderId = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource)
|
|
||||||
|
|
||||||
if (vertexShaderId == 0 || fragmentShaderId == 0)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
return linkProgram(vertexShaderId, fragmentShaderId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.vnc.gl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shaders used for rendering framebuffer
|
|
||||||
*/
|
|
||||||
object Shaders {
|
|
||||||
//language=GLSL
|
|
||||||
const val VERTEX_SHADER = """
|
|
||||||
uniform mat4 u_Projection;
|
|
||||||
attribute vec2 a_Position;
|
|
||||||
attribute vec2 a_TextureCoordinates;
|
|
||||||
varying vec2 v_TextureCoordinates;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
v_TextureCoordinates = a_TextureCoordinates;
|
|
||||||
gl_Position = u_Projection * vec4(a_Position, 0, 1);
|
|
||||||
}"""
|
|
||||||
|
|
||||||
//language=GLSL
|
|
||||||
const val FRAGMENT_SHADER = """
|
|
||||||
precision mediump float;
|
|
||||||
uniform sampler2D u_TextureUnit;
|
|
||||||
varying vec2 v_TextureCoordinates;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates);
|
|
||||||
}"""
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for accessing app preferences
|
|
||||||
*/
|
|
||||||
class AppPreferences(context: Context) {
|
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
inner class UI {
|
|
||||||
val theme = LivePref("theme", "system")
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Viewer {
|
|
||||||
val orientation; get() = prefs.getString("viewer_orientation", "landscape")
|
|
||||||
val fullscreen; get() = prefs.getBoolean("fullscreen_display", true)
|
|
||||||
val pipEnabled; get() = prefs.getBoolean("pip_enabled", false)
|
|
||||||
val drawBehindCutout; get() = prefs.getBoolean("viewer_draw_behind_cutout", false)
|
|
||||||
val keepScreenOn; get() = prefs.getBoolean("keep_screen_on", true)
|
|
||||||
val toolbarAlignment; get() = prefs.getString("toolbar_alignment", "start")
|
|
||||||
val toolbarOpenWithSwipe; get() = prefs.getBoolean("toolbar_open_with_swipe", true)
|
|
||||||
val zoomMax; get() = prefs.getInt("zoom_max", 500) / 100F
|
|
||||||
val zoomMin; get() = prefs.getInt("zoom_min", 50) / 100F
|
|
||||||
val perOrientationZoom; get() = prefs.getBoolean("per_orientation_zoom", true)
|
|
||||||
val toolbarShowGestureStyleToggle; get() = prefs.getBoolean("toolbar_show_gesture_style_toggle", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Gesture {
|
|
||||||
val style; get() = prefs.getString("gesture_style", "touchscreen")!!
|
|
||||||
val tap1 = "left-click" //Preference UI was removed
|
|
||||||
val tap2; get() = prefs.getString("gesture_tap2", "open-keyboard")!!
|
|
||||||
val doubleTap; get() = prefs.getString("gesture_double_tap", "double-click")!!
|
|
||||||
val longPress; get() = prefs.getString("gesture_long_press", "right-click")!!
|
|
||||||
val swipe1; get() = prefs.getString("gesture_swipe1", "pan")!!
|
|
||||||
val swipe2; get() = prefs.getString("gesture_swipe2", "remote-scroll")!!
|
|
||||||
val doubleTapSwipe; get() = prefs.getString("gesture_double_tap_swipe", "remote-drag")!!
|
|
||||||
val longPressSwipe; get() = prefs.getString("gesture_long_press_swipe", "none")!!
|
|
||||||
val longPressSwipeEnabled; get() = (longPressSwipe != "none")
|
|
||||||
val swipeSensitivity; get() = prefs.getInt("gesture_swipe_sensitivity", 10) / 10f
|
|
||||||
val invertVerticalScrolling; get() = prefs.getBoolean("invert_vertical_scrolling", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Input {
|
|
||||||
val gesture = Gesture()
|
|
||||||
|
|
||||||
val vkOpenWithKeyboard; get() = prefs.getBoolean("vk_open_with_keyboard", false)
|
|
||||||
val vkShowAll; get() = prefs.getBoolean("vk_show_all", true)
|
|
||||||
|
|
||||||
val mousePassthrough; get() = prefs.getBoolean("mouse_passthrough", true)
|
|
||||||
val hideLocalCursor; get() = prefs.getBoolean("hide_local_cursor", true)
|
|
||||||
val hideRemoteCursor; get() = prefs.getBoolean("hide_remote_cursor", false)
|
|
||||||
val mouseBack; get() = prefs.getString("mouse_back", "right-click")!!
|
|
||||||
val interceptMouseBack; get() = mouseBack != "default"
|
|
||||||
|
|
||||||
val kmLanguageSwitchToSuper; get() = prefs.getBoolean("km_language_switch_to_super", false)
|
|
||||||
val kmRightAltToSuper; get() = prefs.getBoolean("km_right_alt_to_super", false)
|
|
||||||
val kmBackToEscape; get() = prefs.getBoolean("km_back_to_escape", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Server {
|
|
||||||
val clipboardSync; get() = prefs.getBoolean("clipboard_sync", true)
|
|
||||||
val autoReconnect; get() = prefs.getBoolean("auto_reconnect", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* These are used for one-time features/tips.
|
|
||||||
* These are not exposed to user.
|
|
||||||
*/
|
|
||||||
inner class RunInfo {
|
|
||||||
var hasConnectedSuccessfully: Boolean
|
|
||||||
get() = prefs.getBoolean("run_info_has_connected_successfully", false)
|
|
||||||
set(value) = prefs.edit { putBoolean("run_info_has_connected_successfully", value) }
|
|
||||||
|
|
||||||
var hasShownV2WelcomeMsg
|
|
||||||
get() = prefs.getBoolean("run_info_has_shown_v2_welcome_msg", false)
|
|
||||||
set(value) = prefs.edit { putBoolean("run_info_has_shown_v2_welcome_msg", value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val ui = UI()
|
|
||||||
val viewer = Viewer()
|
|
||||||
val input = Input()
|
|
||||||
val server = Server()
|
|
||||||
val runInfo = RunInfo()
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For some preference changes we want to provide live feedback to user.
|
|
||||||
* This class is used for such scenarios. Based on [LiveData], it notifies
|
|
||||||
* the observers whenever the value of given preference is changed.
|
|
||||||
*
|
|
||||||
* For now, each [LivePref] creates a separate change listener, but if
|
|
||||||
* number of [LivePref]s grow, we can optimize by sharing a single listener.
|
|
||||||
*/
|
|
||||||
inner class LivePref<T>(val key: String, private val defValue: T) : LiveData<T>() {
|
|
||||||
private val prefChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey ->
|
|
||||||
if (key == changedKey)
|
|
||||||
updateValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var initialized = false
|
|
||||||
|
|
||||||
override fun onActive() {
|
|
||||||
if (!initialized) {
|
|
||||||
initialized = true
|
|
||||||
updateValue()
|
|
||||||
prefs.registerOnSharedPreferenceChangeListener(prefChangeListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateValue() {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
when (defValue) {
|
|
||||||
is Boolean -> value = prefs.getBoolean(key, defValue) as T
|
|
||||||
is String -> value = prefs.getString(key, defValue) as T
|
|
||||||
is Int -> value = prefs.getInt(key, defValue) as T
|
|
||||||
is Long -> value = prefs.getLong(key, defValue) as T
|
|
||||||
is Float -> value = prefs.getFloat(key, defValue) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/****************************** Migrations *******************************/
|
|
||||||
init {
|
|
||||||
if (!prefs.getBoolean("gesture_direct_touch", true)) prefs.edit {
|
|
||||||
remove("gesture_direct_touch")
|
|
||||||
putString("gesture_style", "touchpad")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prefs.getBoolean("natural_scrolling", true)) prefs.edit {
|
|
||||||
remove("natural_scrolling")
|
|
||||||
putBoolean("invert_vertical_scrolling", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.getString("gesture_drag", null)?.let {
|
|
||||||
prefs.edit {
|
|
||||||
remove("gesture_drag")
|
|
||||||
putString("gesture_long_press_swipe", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.SimpleAdapter
|
|
||||||
import android.widget.Spinner
|
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.databinding.BindingAdapter
|
|
||||||
import androidx.databinding.InverseBindingAdapter
|
|
||||||
import androidx.databinding.InverseBindingListener
|
|
||||||
|
|
||||||
|
|
||||||
@BindingAdapter("isVisible")
|
|
||||||
fun visibilityAdapter(view: View, isVisible: Boolean) {
|
|
||||||
view.isVisible = isVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
@BindingAdapter("isInvisible")
|
|
||||||
fun invisibilityAdapter(view: View, isInvisible: Boolean) {
|
|
||||||
view.isInvisible = isInvisible
|
|
||||||
}
|
|
||||||
|
|
||||||
/**************************************************************************************************
|
|
||||||
* Spinner value binding
|
|
||||||
* These allows the Spinner to be populated using data-binding in XMl layouts.
|
|
||||||
* There are 4 attributes used for this:
|
|
||||||
*
|
|
||||||
* app:value => Reference to the backing field which actually stores the selected value.
|
|
||||||
* This can be used with two-way binding to update the backing field whenever
|
|
||||||
* selection changes in Spinner.
|
|
||||||
* For now, Only String & Int types are supported for backing field.
|
|
||||||
*
|
|
||||||
* app:values => String Array, holds possible values of app:value
|
|
||||||
*
|
|
||||||
* app:valueLabels => String Array, Optional. If provided, these labels will be shown in the UI
|
|
||||||
* instead of raw app:values
|
|
||||||
*
|
|
||||||
* app:valueDescriptions => String Array, Optional. Short descriptions of values, shown in Spinner
|
|
||||||
* Popup below each label.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*************************************************************************************************/
|
|
||||||
private fun prepareEntryMap(labels: Array<String>, descriptions: Array<String>?) = mutableListOf<Map<String, *>>().apply {
|
|
||||||
for (i in labels.indices)
|
|
||||||
add(mapOf("label" to labels[i], "description" to descriptions?.getOrNull(i)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SpinnerAdapter(context: Context, labels: Array<String>, descriptions: Array<String>?, val values: Array<String>)
|
|
||||||
: SimpleAdapter(context, prepareEntryMap(labels, descriptions), android.R.layout.simple_list_item_1,
|
|
||||||
arrayOf("label", "description"), intArrayOf(android.R.id.text1, android.R.id.text2))
|
|
||||||
|
|
||||||
@BindingAdapter("valueLabels", "valueDescriptions", "values", "value", requireAll = false)
|
|
||||||
fun spinnerValueAdapter(spinner: Spinner, labels: Array<String>?, descriptions: Array<String>?, values: Array<String>?, value: Any?) {
|
|
||||||
if (spinner.adapter == null && values != null) {
|
|
||||||
check(labels == null || labels.size == values.size)
|
|
||||||
check(descriptions == null || descriptions.size == values.size)
|
|
||||||
|
|
||||||
val adapter = SpinnerAdapter(spinner.context, labels ?: values, descriptions, values)
|
|
||||||
if (descriptions != null)
|
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_list_item_2)
|
|
||||||
|
|
||||||
spinner.adapter = adapter
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spinner.adapter != null && value != null) {
|
|
||||||
val adapter = spinner.adapter as SpinnerAdapter
|
|
||||||
val index = adapter.values.indexOf(value.toString())
|
|
||||||
if (index != -1)
|
|
||||||
spinner.setSelection(index)
|
|
||||||
else
|
|
||||||
Log.e("SpinnerValue", "value: $value not found in adapter values: ${adapter.values}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@BindingAdapter("valueAttrChanged")
|
|
||||||
fun spinnerValueChangedAdapter(spinner: Spinner, valueChange: InverseBindingListener) {
|
|
||||||
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(p: AdapterView<*>, v: View?, pos: Int, id: Long) = valueChange.onChange()
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@InverseBindingAdapter(attribute = "value")
|
|
||||||
fun spinnerValueInverseAdapter(spinner: Spinner): String {
|
|
||||||
return (spinner.adapter as SpinnerAdapter).values[spinner.selectedItemPosition]
|
|
||||||
}
|
|
||||||
|
|
||||||
@InverseBindingAdapter(attribute = "value")
|
|
||||||
fun spinnerValueInverseAdapterInt(spinner: Spinner) = spinnerValueInverseAdapter(spinner).toInt()
|
|
||||||
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2024 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clipboard access is slightly more complex in AVNC, because clip data can be
|
|
||||||
* unusually large. It includes stuff coming from server, and app logs.
|
|
||||||
* As clipboard access involves binder IPC, it can lead to ANR issues. So we
|
|
||||||
* use a background thread for accessing clipboard.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts given text on the clipboard.
|
|
||||||
*/
|
|
||||||
suspend fun setClipboardText(context: Context, text: String): Boolean {
|
|
||||||
var success = false
|
|
||||||
try {
|
|
||||||
getClipboardManager(context)?.let {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
it.setPrimaryClip(ClipData.newPlainText(null, text))
|
|
||||||
success = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.e("ClipboardUtil", "Could not copy text to clipboard.", t)
|
|
||||||
}
|
|
||||||
return success
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns current clipboard text.
|
|
||||||
*/
|
|
||||||
suspend fun getClipboardText(context: Context): String? {
|
|
||||||
var result: String? = null
|
|
||||||
try {
|
|
||||||
getClipboardManager(context)?.let {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
result = it.primaryClip?.getItemAt(0)?.text?.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.e("ClipboardUtil", "Could not retrieve text from clipboard.", t)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [ClipboardManager] has to be created on a thread where Looper has been initialized.
|
|
||||||
*/
|
|
||||||
private suspend fun getClipboardManager(context: Context) = withContext(Dispatchers.Main.immediate) {
|
|
||||||
ContextCompat.getSystemService(context, ClipboardManager::class.java)
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import com.example.tiny_computer.BuildConfig
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utilities to aid in debugging
|
|
||||||
*/
|
|
||||||
object Debugging {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns logcat output.
|
|
||||||
* Should not be called from main thread.
|
|
||||||
*/
|
|
||||||
fun logcat(): String {
|
|
||||||
try {
|
|
||||||
return ProcessBuilder("logcat", "-d", "*")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
.inputStream
|
|
||||||
.reader()
|
|
||||||
.readText()
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
return "Error getting logs: ${t.message}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearLogs() {
|
|
||||||
try {
|
|
||||||
ProcessBuilder("logcat", "-c").start()
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
//Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a query parameter string which can be used with GitHub issue url.
|
|
||||||
* Currently, only `body` parameter is generated
|
|
||||||
*/
|
|
||||||
fun bugReportUrlParams(): String {
|
|
||||||
val body = """
|
|
||||||
|**Description**
|
|
||||||
|
|
|
||||||
|
|
|
||||||
|
|
|
||||||
|
|
|
||||||
|**Additional Info**
|
|
||||||
|- App Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})
|
|
||||||
|- Android Version: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT})
|
|
||||||
""".trimMargin()
|
|
||||||
|
|
||||||
return "?body=${Uri.encode(body)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps given logs into a <details> element.
|
|
||||||
* This is useful for GitHub comments.
|
|
||||||
*/
|
|
||||||
fun wrapLogs(title: String, logs: String): String {
|
|
||||||
return """
|
|
||||||
<details>
|
|
||||||
<summary>$title</summary>
|
|
||||||
<p>
|
|
||||||
|
|
||||||
```python
|
|
||||||
{logs}
|
|
||||||
```
|
|
||||||
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
""".trimIndent().replace("{logs}", logs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2022 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.biometric.FingerprintDialogFragment
|
|
||||||
import androidx.biometric.auth.AuthPromptCallback
|
|
||||||
import androidx.biometric.auth.startClass2BiometricOrCredentialAuthentication
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.fragment.app.FragmentFactory
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper around AndroidX Biometrics library.
|
|
||||||
*
|
|
||||||
* - Allow checking for auth availability
|
|
||||||
* - Provide simplified Kotlin-style callback setup
|
|
||||||
* - Provide consistent handling for activity restarts
|
|
||||||
* - Apply workarounds for library bugs
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* 1. Call [init] to setup callbacks
|
|
||||||
* 2. Call [launch] to start auth session
|
|
||||||
*/
|
|
||||||
class DeviceAuthPrompt(private val activity: FragmentActivity) {
|
|
||||||
|
|
||||||
class PromptViewModel : ViewModel() {
|
|
||||||
var isPromptShown = false
|
|
||||||
var promptTitle = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
private val viewModel by activity.viewModels<PromptViewModel>()
|
|
||||||
private var onAuthSuccess: (() -> Unit)? = null
|
|
||||||
private var onAuthFail: ((String) -> Unit)? = null
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup auth callbacks.
|
|
||||||
* Should be called from onCreate() of the host activity/fragment.
|
|
||||||
* If an auth session is active, it will be updated with given callbacks.
|
|
||||||
*/
|
|
||||||
fun init(onSuccess: () -> Unit, onFail: (String) -> Unit) {
|
|
||||||
onAuthSuccess = onSuccess
|
|
||||||
onAuthFail = onFail
|
|
||||||
|
|
||||||
if (viewModel.isPromptShown)
|
|
||||||
launch(viewModel.promptTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether user can be authenticated using Biometric or Device credentials (e.g. PIN, Password)
|
|
||||||
*/
|
|
||||||
fun canLaunch(): Boolean {
|
|
||||||
val types = BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
|
||||||
return BiometricManager.from(activity).canAuthenticate(types) == BiometricManager.BIOMETRIC_SUCCESS
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch auth prompt.
|
|
||||||
*/
|
|
||||||
fun launch(title: String) {
|
|
||||||
check(onAuthSuccess != null)
|
|
||||||
check(onAuthFail != null)
|
|
||||||
|
|
||||||
activity.startClass2BiometricOrCredentialAuthentication(
|
|
||||||
title = title,
|
|
||||||
confirmationRequired = false,
|
|
||||||
callback = PromptCallback()
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.isPromptShown = true
|
|
||||||
viewModel.promptTitle = title
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onAuthFinished() {
|
|
||||||
viewModel.isPromptShown = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class PromptCallback : AuthPromptCallback() {
|
|
||||||
override fun onAuthenticationSucceeded(activity: FragmentActivity?, result: BiometricPrompt.AuthenticationResult) {
|
|
||||||
onAuthSuccess?.invoke()
|
|
||||||
onAuthFinished()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationError(activity: FragmentActivity?, errorCode: Int, errString: CharSequence) {
|
|
||||||
Log.e(javaClass.simpleName, "Authentication error: $errString [$errorCode] ")
|
|
||||||
onAuthFail?.invoke(errString.toString())
|
|
||||||
onAuthFinished()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* The constructor of [FingerprintDialogFragment] is currently marked private.
|
|
||||||
* When fragment manager tries to re-instantiate it after activity restart,
|
|
||||||
* it will fail and crash the app. So we install a custom [FragmentFactoryWrapper]
|
|
||||||
* which instantiates [FingerprintDialogFragment] via reflection.
|
|
||||||
*
|
|
||||||
* Issue: https://issuetracker.google.com/issues/181805603
|
|
||||||
*
|
|
||||||
* TODO: Remove this after issue is fixed in library.
|
|
||||||
*/
|
|
||||||
fun applyFingerprintDialogFix(fm: FragmentManager) {
|
|
||||||
fm.fragmentFactory = FragmentFactoryWrapper(fm.fragmentFactory)
|
|
||||||
}
|
|
||||||
|
|
||||||
class FragmentFactoryWrapper(private val realFactory: FragmentFactory) : FragmentFactory() {
|
|
||||||
private val fpClassName = FingerprintDialogFragment::class.java.name
|
|
||||||
|
|
||||||
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
|
|
||||||
if (className == fpClassName) {
|
|
||||||
return FingerprintDialogFragment::class.java.getDeclaredConstructor().let {
|
|
||||||
it.isAccessible = true
|
|
||||||
it.newInstance()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return realFactory.instantiate(classLoader, className)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2024 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class implements single-shot observable events.
|
|
||||||
* It is based on [LiveData] which does the heavy lifting for us.
|
|
||||||
*
|
|
||||||
* Single-shot
|
|
||||||
* ===========
|
|
||||||
* When this event is fired, it will notify exactly one active observer.
|
|
||||||
* If there is no active observer, it will wait for one so that the event
|
|
||||||
* is not "lost".
|
|
||||||
*
|
|
||||||
* This is the main difference between this class & [LiveData]. [LiveData] will
|
|
||||||
* notify the future observers to bring them up-to date. This can happen during
|
|
||||||
* Activity restarts where old observers are detached and new ones are attached.
|
|
||||||
*
|
|
||||||
* This class is used for events which should be handled only once.
|
|
||||||
* E.g. starting a fragment.
|
|
||||||
*/
|
|
||||||
open class LiveEvent<T> {
|
|
||||||
|
|
||||||
private class WrappedData<T>(val data: T, var consumed: Boolean = false)
|
|
||||||
private class WrappedObserver<T>(private val observer: Observer<T>) : Observer<WrappedData<T>> {
|
|
||||||
override fun onChanged(value: WrappedData<T>) {
|
|
||||||
if (!value.consumed) {
|
|
||||||
value.consumed = true
|
|
||||||
observer.onChanged(value.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val liveData = MutableLiveData<WrappedData<T>>()
|
|
||||||
private val wrappedObservers = mutableMapOf<Observer<T>, WrappedObserver<T>>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Peek current value of this event, irrespective of whether any observer has been notified.
|
|
||||||
*/
|
|
||||||
val value get() = liveData.value?.data
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fire this event with given data.
|
|
||||||
* Must be called from main thread.
|
|
||||||
*/
|
|
||||||
@MainThread
|
|
||||||
fun fire(data: T) {
|
|
||||||
liveData.value = WrappedData(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous version of [fire].
|
|
||||||
* Can be called from any thread.
|
|
||||||
*/
|
|
||||||
fun fireAsync(data: T) {
|
|
||||||
liveData.postValue(WrappedData(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun observe(owner: LifecycleOwner, observer: Observer<T>) {
|
|
||||||
val wrapped = WrappedObserver(observer)
|
|
||||||
wrappedObservers[observer] = wrapped
|
|
||||||
liveData.observe(owner, wrapped)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun observeForever(observer: Observer<T>) {
|
|
||||||
val wrapped = WrappedObserver(observer)
|
|
||||||
wrappedObservers[observer] = wrapped
|
|
||||||
liveData.observeForever(wrapped)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun removeObserver(observer: Observer<T>) {
|
|
||||||
wrappedObservers.remove(observer)?.let { liveData.removeObserver(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2024 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension of [LiveEvent] to facilitate awaiting some response from observers.
|
|
||||||
*
|
|
||||||
* This simplifies the cases where a background thread needs some value from the user (i.e. UI thread),
|
|
||||||
* and we want the background thread to block until that value is available.
|
|
||||||
*
|
|
||||||
* If a request is canceled then [requestResponse] will return [cancellationValue].
|
|
||||||
*
|
|
||||||
* @param scope can be specified to auto-cancel this request on scope cancellation.
|
|
||||||
*/
|
|
||||||
class LiveRequest<RequestType, ResponseType>(private val cancellationValue: ResponseType, scope: CoroutineScope?)
|
|
||||||
: LiveEvent<RequestType>() {
|
|
||||||
|
|
||||||
private val responses = LinkedBlockingQueue<ResponseType>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
scope?.launch { awaitCancellation() }?.invokeOnCompletion { cancelRequest() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires this request with given value and returns the response.
|
|
||||||
* Will block until any response is available.
|
|
||||||
* Can be called from any threads.
|
|
||||||
*/
|
|
||||||
fun requestResponse(value: RequestType): ResponseType {
|
|
||||||
responses.clear()
|
|
||||||
fireAsync(value)
|
|
||||||
return responses.take() //Blocking call
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets response for current request.
|
|
||||||
*/
|
|
||||||
fun offerResponse(response: ResponseType) = responses.offer(response)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels any pending request.
|
|
||||||
*/
|
|
||||||
fun cancelRequest() = responses.offer(cancellationValue)
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
|
|
||||||
|
|
||||||
object MsgDialog {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a dialog with given title & message,
|
|
||||||
*/
|
|
||||||
fun show(manager: FragmentManager, title: CharSequence, msg: CharSequence) {
|
|
||||||
val fragment = MsgDialogFragment()
|
|
||||||
val args = Bundle(2)
|
|
||||||
|
|
||||||
args.putCharSequence("title", title)
|
|
||||||
args.putCharSequence("msg", msg)
|
|
||||||
fragment.arguments = args
|
|
||||||
|
|
||||||
fragment.show(manager, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
class MsgDialogFragment : DialogFragment() {
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(requireArguments().getCharSequence("title"))
|
|
||||||
.setMessage(requireArguments().getCharSequence("msg"))
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> /* Let it dismiss */ }
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contract for openable documents.
|
|
||||||
* This is needed because OpenDocument doesn't specify [Intent.CATEGORY_OPENABLE]
|
|
||||||
*/
|
|
||||||
class OpenableDocument : ActivityResultContracts.OpenDocument() {
|
|
||||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
|
||||||
return super.createIntent(context, input).addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utilities related to Samsung DeX
|
|
||||||
*/
|
|
||||||
object SamsungDex {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true, if DeX mode is enabled.
|
|
||||||
*/
|
|
||||||
private fun isInDexMode(context: Context) = runCatching {
|
|
||||||
val config = context.resources.configuration
|
|
||||||
val configClass = config.javaClass
|
|
||||||
|
|
||||||
val flag = configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass)
|
|
||||||
val value = configClass.getField("semDesktopModeEnabled").getInt(config)
|
|
||||||
|
|
||||||
value == flag
|
|
||||||
}.getOrDefault(false)
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables/disables meta-key event capturing.
|
|
||||||
*/
|
|
||||||
fun setMetaKeyCapture(activity: Activity, isEnabled: Boolean) {
|
|
||||||
if (!isInDexMode(activity))
|
|
||||||
return
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
val managerClass = Class.forName("com.samsung.android.view.SemWindowManager")
|
|
||||||
val instanceMethod = managerClass.getMethod("getInstance")
|
|
||||||
val manager = instanceMethod.invoke(null)
|
|
||||||
|
|
||||||
val requestMethod = managerClass.getDeclaredMethod("requestMetaKeyEvent",
|
|
||||||
ComponentName::class.java,
|
|
||||||
Boolean::class.java)
|
|
||||||
requestMethod.invoke(manager, activity.componentName, isEnabled)
|
|
||||||
}.onFailure { Log.d("DeX Support", "Meta key capture error", it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.util.TypedValue
|
|
||||||
import androidx.appcompat.widget.AppCompatSpinner
|
|
||||||
import com.google.android.material.elevation.ElevationOverlayProvider
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class extends spinner to handle some quirks and add some utility features.
|
|
||||||
*/
|
|
||||||
class SpinnerEx(context: Context, attrs: AttributeSet? = null) : AppCompatSpinner(context, attrs) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
setupElevationOverlay()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Popup window of the Spinner does not support elevation overlay
|
|
||||||
* which makes it hard to differentiate between popup & rest of the controls in dark theme.
|
|
||||||
*
|
|
||||||
* So we manually apply the overlay to popup background.
|
|
||||||
*/
|
|
||||||
private fun setupElevationOverlay() {
|
|
||||||
// Elevation is hardcoded to 16dp because we don't have access to popup
|
|
||||||
val popupElevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
16F,
|
|
||||||
resources.displayMetrics)
|
|
||||||
|
|
||||||
val overlay = ElevationOverlayProvider(context)
|
|
||||||
.compositeOverlayWithThemeSurfaceColorIfNeeded(popupElevation)
|
|
||||||
|
|
||||||
val background = popupBackground
|
|
||||||
if (background is GradientDrawable)
|
|
||||||
background.setColor(overlay)
|
|
||||||
else
|
|
||||||
background.setTint(overlay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.viewmodel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.gaurav.avnc.model.db.MainDb
|
|
||||||
import com.gaurav.avnc.util.AppPreferences
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base view model.
|
|
||||||
*/
|
|
||||||
open class BaseViewModel(val app: Application) : AndroidViewModel(app) {
|
|
||||||
|
|
||||||
protected val db by lazy { MainDb.getInstance(app) }
|
|
||||||
|
|
||||||
protected val serverProfileDao by lazy { db.serverProfileDao }
|
|
||||||
|
|
||||||
val pref by lazy { AppPreferences(app) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launches a new coroutine using [viewModelScope], and executes [block] in that coroutine.
|
|
||||||
*/
|
|
||||||
protected fun launch(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> Unit): Job {
|
|
||||||
return viewModelScope.launch(context) { this.block() }
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun launchMain(block: suspend CoroutineScope.() -> Unit) = launch(Dispatchers.Main, block)
|
|
||||||
protected fun launchIO(block: suspend CoroutineScope.() -> Unit) = launch(Dispatchers.IO, block)
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2024 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.viewmodel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ViewModel for profile editor
|
|
||||||
*/
|
|
||||||
class EditorViewModel(app: Application, state: SavedStateHandle, initialProfile: ServerProfile) : BaseViewModel(app) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Profile being edited
|
|
||||||
*/
|
|
||||||
val profile = state["profile"] ?: initialProfile.copy()
|
|
||||||
|
|
||||||
init {
|
|
||||||
state["profile"] = profile
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* While most fields of [profile] are straightforward to edit, some require
|
|
||||||
* more complex handling, and live feedback in UI.
|
|
||||||
* For these, we have to use dedicated LiveData fields.
|
|
||||||
*/
|
|
||||||
val useRepeater = state.getLiveData("useRepeater", profile.useRepeater)
|
|
||||||
val idOnRepeater = state.getLiveData("idOnRepeater", if (profile.useRepeater) profile.idOnRepeater.toString() else "")
|
|
||||||
val useRawEncoding = state.getLiveData("useRawEncoding", profile.useRawEncoding)
|
|
||||||
val useSshTunnel = state.getLiveData("useSshTunnel", profile.channelType == ServerProfile.CHANNEL_SSH_TUNNEL)
|
|
||||||
val sshUsePassword = state.getLiveData("sshUsePassword", profile.sshAuthType == ServerProfile.SSH_AUTH_PASSWORD)
|
|
||||||
val sshUsePrivateKey = state.getLiveData("sshUsePrivateKey", profile.sshAuthType == ServerProfile.SSH_AUTH_KEY)
|
|
||||||
val hasSshPrivateKey = state.getLiveData("hasSshPrivateKey", profile.sshPrivateKey.isNotBlank())
|
|
||||||
|
|
||||||
|
|
||||||
fun prepareProfileForSave(): ServerProfile {
|
|
||||||
profile.useRepeater = useRepeater.value ?: false
|
|
||||||
profile.idOnRepeater = idOnRepeater.value?.toIntOrNull() ?: 0
|
|
||||||
profile.useRawEncoding = useRawEncoding.value ?: false
|
|
||||||
profile.channelType = if (useSshTunnel.value == true) ServerProfile.CHANNEL_SSH_TUNNEL else ServerProfile.CHANNEL_TCP
|
|
||||||
profile.sshAuthType = if (sshUsePassword.value == true) ServerProfile.SSH_AUTH_PASSWORD else ServerProfile.SSH_AUTH_KEY
|
|
||||||
return profile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.viewmodel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.room.withTransaction
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
import com.gaurav.avnc.util.LiveEvent
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Viewmodel for preferences activity.
|
|
||||||
*/
|
|
||||||
class PrefsViewModel(app: Application) : BaseViewModel(app) {
|
|
||||||
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Import/Export
|
|
||||||
*
|
|
||||||
* Currently, we are only exporting server profiles but preferences can be
|
|
||||||
* exported in the future.
|
|
||||||
*
|
|
||||||
* Importing/Exporting is done on a background thread.
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class Container(
|
|
||||||
val version: Int = 1,
|
|
||||||
val profiles: List<ServerProfile>
|
|
||||||
)
|
|
||||||
|
|
||||||
private val serializer = Json {
|
|
||||||
encodeDefaults = false
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
prettyPrint = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val importFinishedEvent = LiveEvent<Boolean>()
|
|
||||||
val exportFinishedEvent = LiveEvent<Boolean>()
|
|
||||||
var importExportError = MutableLiveData<String>()
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exports data to given [uri].
|
|
||||||
*/
|
|
||||||
fun export(uri: Uri) {
|
|
||||||
launchIO {
|
|
||||||
runCatching {
|
|
||||||
// Serialize
|
|
||||||
val profiles = serverProfileDao.getList()
|
|
||||||
val data = Container(profiles = profiles)
|
|
||||||
val json = serializer.encodeToString(data)
|
|
||||||
|
|
||||||
// Write out
|
|
||||||
app.contentResolver.openOutputStream(uri)?.use { stream ->
|
|
||||||
stream.writer().use { it.write(json) }
|
|
||||||
} ?: throw IOException("Unable to write the file.")
|
|
||||||
|
|
||||||
}.let {
|
|
||||||
importExportError.postValue(it.exceptionOrNull()?.message)
|
|
||||||
exportFinishedEvent.fireAsync(it.isSuccess)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Imports data from given [uri].
|
|
||||||
*/
|
|
||||||
fun import(uri: Uri, deleteCurrentServers: Boolean) {
|
|
||||||
launchIO {
|
|
||||||
runCatching {
|
|
||||||
|
|
||||||
val json = app.contentResolver.openInputStream(uri)?.use { stream ->
|
|
||||||
stream.reader().use { it.readText() }
|
|
||||||
} ?: throw IOException("Unable to read the file.")
|
|
||||||
|
|
||||||
// Deserialize
|
|
||||||
val data = serializer.decodeFromString<Container>(json)
|
|
||||||
|
|
||||||
//This is where migrations would be applied (if required in future)
|
|
||||||
|
|
||||||
//Update database
|
|
||||||
if (deleteCurrentServers) {
|
|
||||||
db.withTransaction {
|
|
||||||
serverProfileDao.deleteAll()
|
|
||||||
serverProfileDao.insert(data.profiles)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//Reset IDs so that they don't conflict with saved profiles
|
|
||||||
data.profiles.forEach { it.ID = 0 }
|
|
||||||
serverProfileDao.insert(data.profiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
}.let {
|
|
||||||
importExportError.postValue(it.exceptionOrNull()?.message)
|
|
||||||
importFinishedEvent.fireAsync(it.isSuccess)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2023 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.viewmodel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.switchMap
|
|
||||||
|
|
||||||
class UrlBarViewModel(app: Application) : BaseViewModel(app) {
|
|
||||||
|
|
||||||
val query = MutableLiveData("")
|
|
||||||
val filteredServers = query.switchMap {
|
|
||||||
if (it.isNotBlank()) serverProfileDao.search("%$it%")
|
|
||||||
else MutableLiveData(listOf())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.viewmodel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.graphics.RectF
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.gaurav.avnc.model.LoginInfo
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
import com.gaurav.avnc.ui.vnc.FrameScroller
|
|
||||||
import com.gaurav.avnc.ui.vnc.FrameState
|
|
||||||
import com.gaurav.avnc.ui.vnc.FrameView
|
|
||||||
import com.gaurav.avnc.util.LiveRequest
|
|
||||||
import com.gaurav.avnc.util.getClipboardText
|
|
||||||
import com.gaurav.avnc.util.setClipboardText
|
|
||||||
import com.gaurav.avnc.viewmodel.service.HostKey
|
|
||||||
import com.gaurav.avnc.viewmodel.service.SshTunnel
|
|
||||||
import com.gaurav.avnc.vnc.Messenger
|
|
||||||
import com.gaurav.avnc.vnc.UserCredential
|
|
||||||
import com.gaurav.avnc.vnc.VncClient
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.IOException
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ViewModel for VncActivity
|
|
||||||
*
|
|
||||||
* Connection
|
|
||||||
* ==========
|
|
||||||
*
|
|
||||||
* At construction, we instantiate a [VncClient] referenced by [client]. Then
|
|
||||||
* activity starts the connection by calling [initConnection] which starts a coroutine to
|
|
||||||
* handle connection setup.
|
|
||||||
*
|
|
||||||
* After successful connection, we continue to operate normally until the remote
|
|
||||||
* server closes the connection, or an error occurs. Once disconnected, we
|
|
||||||
* wait for the activity to finish and then cleanup any acquired resources.
|
|
||||||
*
|
|
||||||
* Currently, lifecycle of [client] is tied to this view model. So one [VncViewModel]
|
|
||||||
* manages only one [VncClient].
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Threading
|
|
||||||
* =========
|
|
||||||
*
|
|
||||||
* Receiver thread :- This thread is started (as a coroutine) in [launchConnection].
|
|
||||||
* It handles the protocol initialization, and after that processes incoming messages.
|
|
||||||
* Most of the callbacks of [VncClient.Observer] are invoked on this thread. In most
|
|
||||||
* cases it is stopped when activity is finished and this view model is cleaned up.
|
|
||||||
*
|
|
||||||
* Sender thread :- This thread is created (as an executor) by [messenger]. It is
|
|
||||||
* used to send messages to remote server. We use this dedicated thread instead
|
|
||||||
* of coroutines to preserve the order of sent messages.
|
|
||||||
*
|
|
||||||
* UI thread :- Main thread of the app. Used for updating UI and controlling other
|
|
||||||
* Threads. This is where [frameState] is updated.
|
|
||||||
*
|
|
||||||
* Renderer thread :- This is managed by [FrameView] and used for rendering frame
|
|
||||||
* via OpenGL ES. [frameState] is read from this thread to decide how/where frame
|
|
||||||
* should be drawn.
|
|
||||||
*/
|
|
||||||
class VncViewModel(val profile: ServerProfile, app: Application) : BaseViewModel(app), VncClient.Observer {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connection lifecycle:
|
|
||||||
*
|
|
||||||
* Created
|
|
||||||
* |
|
|
||||||
* v
|
|
||||||
* Connecting ----------+
|
|
||||||
* | |
|
|
||||||
* v |
|
|
||||||
* Connected |
|
|
||||||
* | |
|
|
||||||
* v |
|
|
||||||
* Disconnected <-------+
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
enum class State {
|
|
||||||
Created,
|
|
||||||
Connecting,
|
|
||||||
Connected,
|
|
||||||
Disconnected;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val State?.isConnected get() = (this == Connected)
|
|
||||||
val State?.isDisconnected get() = (this == Disconnected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val client = VncClient(this)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We have two places for connection state (both are synced):
|
|
||||||
*
|
|
||||||
* [VncClient.connected] - Simple boolean state, used most of the time
|
|
||||||
* [state] - More granular, used by observers & data binding
|
|
||||||
*/
|
|
||||||
val state = MutableLiveData(State.Created)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reason for disconnecting.
|
|
||||||
*/
|
|
||||||
val disconnectReason = MutableLiveData("")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired when we need some credentials from user.
|
|
||||||
* It will trigger the Login dialog.
|
|
||||||
*/
|
|
||||||
val loginInfoRequest = LiveRequest<LoginInfo.Type, LoginInfo>(LoginInfo(), viewModelScope)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired to unlock saved servers.
|
|
||||||
*/
|
|
||||||
val serverUnlockRequest = LiveRequest<Any?, Boolean>(false, viewModelScope)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of saved profiles.
|
|
||||||
* Used by login-autocompletion.
|
|
||||||
*/
|
|
||||||
val savedProfiles by lazy { serverProfileDao.getLiveList() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds a weak reference to [FrameView] instance.
|
|
||||||
*
|
|
||||||
* This is used to tell [FrameView] to re-render its content when VncClient's
|
|
||||||
* framebuffer is updated. Instead of using LiveData/LiveEvent, we keep a
|
|
||||||
* weak reference because:
|
|
||||||
*
|
|
||||||
* 1. It avoids a context-switch to UI thread. Rendering request to
|
|
||||||
* a GlSurfaceView can be sent from any thread.
|
|
||||||
*
|
|
||||||
* 2. We don't have to invoke the whole ViewModel machinery just for
|
|
||||||
* a single call to FrameView.
|
|
||||||
*/
|
|
||||||
var frameViewRef = WeakReference<FrameView>(null)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds information about scaling, translation etc.
|
|
||||||
*/
|
|
||||||
val frameState = with(pref.viewer) { FrameState(zoomMin, zoomMax, perOrientationZoom) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used for scrolling/animating the frame.
|
|
||||||
*/
|
|
||||||
val frameScroller = FrameScroller(this)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used for sending events to remote server.
|
|
||||||
*/
|
|
||||||
val messenger = Messenger(client)
|
|
||||||
|
|
||||||
private val sshTunnel = SshTunnel(this)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to confirm unknown hosts.
|
|
||||||
*/
|
|
||||||
val sshHostKeyVerifyRequest = LiveRequest<HostKey, Boolean>(false, viewModelScope)
|
|
||||||
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Connection management
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize VNC connection.
|
|
||||||
* It can be called multiple times due to activity restarts.
|
|
||||||
*/
|
|
||||||
fun initConnection() {
|
|
||||||
if (state.value == State.Created) {
|
|
||||||
state.value = State.Connecting
|
|
||||||
frameState.setZoom(profile.zoom1, profile.zoom2)
|
|
||||||
launchConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchConnection() {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
|
|
||||||
preConnect()
|
|
||||||
connect()
|
|
||||||
processMessages()
|
|
||||||
|
|
||||||
}.onFailure {
|
|
||||||
if (it is IOException) disconnectReason.postValue(it.message)
|
|
||||||
Log.e("ReceiverCoroutine", "Connection failed", it)
|
|
||||||
}
|
|
||||||
|
|
||||||
state.postValue(State.Disconnected)
|
|
||||||
|
|
||||||
//Wait until activity is finished and viewmodel is cleaned up.
|
|
||||||
runCatching { awaitCancellation() }
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun preConnect() {
|
|
||||||
if (profile.ID != 0L)
|
|
||||||
if (!serverUnlockRequest.requestResponse(null))
|
|
||||||
throw IOException("Could not unlock server")
|
|
||||||
|
|
||||||
client.configure(profile.viewOnly, profile.securityType, true /* Hardcoded to true */,
|
|
||||||
profile.imageQuality, profile.useRawEncoding)
|
|
||||||
|
|
||||||
if (profile.useRepeater)
|
|
||||||
client.setupRepeater(profile.idOnRepeater)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun connect() {
|
|
||||||
when (profile.channelType) {
|
|
||||||
ServerProfile.CHANNEL_TCP ->
|
|
||||||
client.connect(profile.host, profile.port)
|
|
||||||
|
|
||||||
ServerProfile.CHANNEL_SSH_TUNNEL ->
|
|
||||||
sshTunnel.open().use {
|
|
||||||
client.connect(it.host, it.port)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> throw IOException("Unknown Channel: ${profile.channelType}")
|
|
||||||
}
|
|
||||||
|
|
||||||
state.postValue(State.Connected)
|
|
||||||
|
|
||||||
// Initial sync, slightly delayed to allow extended clipboard negotiations
|
|
||||||
launchIO { delay(1000L); sendClipboardText() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processMessages() {
|
|
||||||
while (viewModelScope.isActive)
|
|
||||||
client.processServerMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cleanup() {
|
|
||||||
messenger.cleanup()
|
|
||||||
client.cleanup()
|
|
||||||
sshTunnel.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can be used to persist any changes made to [profile]
|
|
||||||
*/
|
|
||||||
fun saveProfile() {
|
|
||||||
if (profile.ID != 0L)
|
|
||||||
launch { serverProfileDao.update(profile) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Frame management
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
fun updateZoom(scaleFactor: Float, fx: Float, fy: Float) {
|
|
||||||
if (profile.fZoomLocked) return
|
|
||||||
|
|
||||||
val appliedScaleFactor = frameState.updateZoom(scaleFactor)
|
|
||||||
|
|
||||||
//Calculate how much the focus would shift after scaling
|
|
||||||
val dfx = (fx - frameState.frameX) * (appliedScaleFactor - 1)
|
|
||||||
val dfy = (fy - frameState.frameY) * (appliedScaleFactor - 1)
|
|
||||||
|
|
||||||
//Translate in opposite direction to keep focus fixed
|
|
||||||
frameState.pan(-dfx, -dfy)
|
|
||||||
|
|
||||||
frameViewRef.get()?.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetZoom() {
|
|
||||||
frameState.setZoom(1f, 1f)
|
|
||||||
frameViewRef.get()?.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetZoomToDefault() {
|
|
||||||
frameState.setZoom(profile.zoom1, profile.zoom2)
|
|
||||||
frameViewRef.get()?.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setZoom(zoom1: Float, zoom2: Float) {
|
|
||||||
frameState.setZoom(zoom1, zoom2)
|
|
||||||
frameViewRef.get()?.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun panFrame(deltaX: Float, deltaY: Float) {
|
|
||||||
frameState.pan(deltaX, deltaY)
|
|
||||||
frameViewRef.get()?.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun moveFrameTo(x: Float, y: Float) {
|
|
||||||
frameState.moveTo(x, y)
|
|
||||||
frameViewRef.get()?.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleZoomLock(enabled: Boolean) {
|
|
||||||
profile.fZoomLocked = enabled
|
|
||||||
saveProfile()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveZoom() {
|
|
||||||
profile.zoom1 = frameState.zoomScale1
|
|
||||||
profile.zoom2 = frameState.zoomScale2
|
|
||||||
saveProfile()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSafeArea(safeArea: RectF) {
|
|
||||||
frameState.setSafeArea(safeArea)
|
|
||||||
frameViewRef.get()?.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Miscellaneous
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
fun sendClipboardText() {
|
|
||||||
if (pref.server.clipboardSync && client.connected) launchIO {
|
|
||||||
getClipboardText(app)?.let { messenger.sendClipboardText(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var clipReceiverJob: Job? = null
|
|
||||||
private fun receiveClipboardText(text: String) {
|
|
||||||
if (!pref.server.clipboardSync)
|
|
||||||
return
|
|
||||||
|
|
||||||
// This is a protective measure against servers which send every 'selection' made on the server.
|
|
||||||
// Setting clip text involves IPC, so these events can exhaust Binder resources, leading to ANRs.
|
|
||||||
if (clipReceiverJob?.isActive == true) {
|
|
||||||
Log.w(javaClass.simpleName, "Dropping clip text received from server, previous text is still pending")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clipReceiverJob = launchIO {
|
|
||||||
setClipboardText(app, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLoginInfo(type: LoginInfo.Type): LoginInfo {
|
|
||||||
val vu = profile.username
|
|
||||||
val vp = profile.password
|
|
||||||
val sp = profile.sshPassword
|
|
||||||
|
|
||||||
if (type == LoginInfo.Type.VNC_PASSWORD && vp.isNotBlank())
|
|
||||||
return LoginInfo(password = vp)
|
|
||||||
|
|
||||||
if (type == LoginInfo.Type.VNC_CREDENTIAL && vu.isNotBlank() && vp.isNotBlank())
|
|
||||||
return LoginInfo(username = vu, password = vp)
|
|
||||||
|
|
||||||
if (type == LoginInfo.Type.SSH_PASSWORD && sp.isNotBlank())
|
|
||||||
return LoginInfo(password = sp)
|
|
||||||
|
|
||||||
// Something is missing, so we have to ask the user
|
|
||||||
return loginInfoRequest.requestResponse(type) // Blocking call
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resize remote desktop to match with local window size (if requested by user).
|
|
||||||
* In portrait mode, safe area is used instead of window to exclude the keyboard.
|
|
||||||
*/
|
|
||||||
fun resizeRemoteDesktop() {
|
|
||||||
if (profile.resizeRemoteDesktop) frameState.let {
|
|
||||||
if (it.windowWidth > it.windowHeight)
|
|
||||||
messenger.setDesktopSize(it.windowWidth.toInt(), it.windowHeight.toInt())
|
|
||||||
else
|
|
||||||
messenger.setDesktopSize(it.safeArea.width().toInt(), it.safeArea.height().toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pauseFrameBufferUpdates() {
|
|
||||||
//client.setAutomaticFrameBufferUpdates(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resumeFrameBufferUpdates() {
|
|
||||||
//client.setAutomaticFrameBufferUpdates(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshFrameBuffer() {
|
|
||||||
messenger.refreshFrameBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* [VncClient.Observer] Implementation
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
override fun onPasswordRequired(): String {
|
|
||||||
return getLoginInfo(LoginInfo.Type.VNC_PASSWORD).password
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCredentialRequired(): UserCredential {
|
|
||||||
return getLoginInfo(LoginInfo.Type.VNC_CREDENTIAL).let { UserCredential(it.username, it.password) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFramebufferUpdated() {
|
|
||||||
frameViewRef.get()?.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGotXCutText(text: String) {
|
|
||||||
receiveClipboardText(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFramebufferSizeChanged(width: Int, height: Int) {
|
|
||||||
launchMain {
|
|
||||||
frameState.setFramebufferSize(width.toFloat(), height.toFloat())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPointerMoved(x: Int, y: Int) {
|
|
||||||
frameViewRef.get()?.requestRender()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2024 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.viewmodel.service
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.nsd.NsdManager
|
|
||||||
import android.net.nsd.NsdServiceInfo
|
|
||||||
import android.net.wifi.WifiManager
|
|
||||||
import android.net.wifi.WifiManager.MulticastLock
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discovers VNC servers advertising themselves on the network.
|
|
||||||
*/
|
|
||||||
object Discovery {
|
|
||||||
private const val TAG = "VncServiceDiscovery"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of servers found by Discovery.
|
|
||||||
*/
|
|
||||||
val servers = MutableLiveData<List<ServerProfile>>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status of discovery.
|
|
||||||
*/
|
|
||||||
val isRunning = MutableLiveData(false)
|
|
||||||
|
|
||||||
private val impl by lazy { Impl() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts discovery.
|
|
||||||
*/
|
|
||||||
fun start(context: Context) = impl.start(context)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops discovery.
|
|
||||||
*/
|
|
||||||
fun stop() = impl.stop()
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Due to Android API limitations, service discovery is more complicated than necessary:
|
|
||||||
*
|
|
||||||
* - [NsdManager] is asynchronous, which means every command's result is communicated later
|
|
||||||
* on a separate thread. Also, [NsdManager] throws up if more than one request is made by same
|
|
||||||
* listener. You can't call [NsdManager.discoverServices] even if discovery is already started
|
|
||||||
* So to avoid race conditions (and keep my sanity, because its one of those APIs in Android
|
|
||||||
* where I wish some day the API designers are forced to use this crap themselves), all callbacks
|
|
||||||
* of [Impl] are run on a dedicated [executor], and [startRequested] is used to track pending start.
|
|
||||||
*
|
|
||||||
* - Only one service can resolved at a time via [NsdManager.resolveService]. To handle this,
|
|
||||||
* newly found service is first added to [pendingResolves]. When resolution finishes for a
|
|
||||||
* service, we remove that service form [pendingResolves], and start resolution for the next.
|
|
||||||
*
|
|
||||||
* - Android can filter/drop multicast WiFi packets to save power. Devices, like Pixel phone, enable
|
|
||||||
* this feature. This can be turned off by acquiring a multicast lock, but [NsdManager] doesn't
|
|
||||||
* doesn't do this automatically. So we have to acquire it manually.
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION") // Yeah, f**k you too Google
|
|
||||||
private class Impl {
|
|
||||||
private val serviceType = "_rfb._tcp"
|
|
||||||
private var wifiManager: WifiManager? = null
|
|
||||||
private var multicastLock: MulticastLock? = null
|
|
||||||
private var nsdManager: NsdManager? = null
|
|
||||||
private val listener = DiscoveryListener()
|
|
||||||
private val executor = Executors.newSingleThreadExecutor()
|
|
||||||
|
|
||||||
private var started = false
|
|
||||||
private var startRequested = false
|
|
||||||
private val pendingResolves = mutableMapOf<ResolveListener, NsdServiceInfo>()
|
|
||||||
private val resolvedProfiles = mutableSetOf<ServerProfile>()
|
|
||||||
|
|
||||||
private fun execute(action: Runnable) {
|
|
||||||
runCatching { executor.execute(action) }.onFailure { Log.e(TAG, "Cannot execute action", it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun postResolvedProfiles() {
|
|
||||||
servers.postValue(resolvedProfiles.toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start(context: Context) = execute {
|
|
||||||
if (startRequested || started)
|
|
||||||
return@execute
|
|
||||||
|
|
||||||
val appContext = context.applicationContext // Need app context to avoid possibility of WiFiManager leaks
|
|
||||||
wifiManager = wifiManager ?: ContextCompat.getSystemService(appContext, WifiManager::class.java)
|
|
||||||
nsdManager = nsdManager ?: ContextCompat.getSystemService(appContext, NsdManager::class.java)
|
|
||||||
nsdManager!!.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
|
|
||||||
startRequested = true
|
|
||||||
|
|
||||||
// Forget old profiles
|
|
||||||
resolvedProfiles.clear()
|
|
||||||
postResolvedProfiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() = execute {
|
|
||||||
if (started)
|
|
||||||
nsdManager?.stopServiceDiscovery(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onStarted() = execute {
|
|
||||||
started = true
|
|
||||||
startRequested = false
|
|
||||||
isRunning.postValue(true)
|
|
||||||
|
|
||||||
multicastLock = wifiManager?.createMulticastLock(TAG)
|
|
||||||
multicastLock?.acquire()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onStopped() = execute {
|
|
||||||
started = false
|
|
||||||
isRunning.postValue(false)
|
|
||||||
multicastLock?.release()
|
|
||||||
multicastLock = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onStartFailed() = execute {
|
|
||||||
startRequested = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onServiceFound(serviceInfo: NsdServiceInfo) = execute {
|
|
||||||
val listener = ResolveListener()
|
|
||||||
pendingResolves[listener] = serviceInfo
|
|
||||||
if (pendingResolves.size == 1) // Kick-start the resolution chain
|
|
||||||
nsdManager?.resolveService(serviceInfo, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onServiceLost(serviceInfo: NsdServiceInfo) = execute {
|
|
||||||
resolvedProfiles.removeAll { it.name == serviceInfo.serviceName }
|
|
||||||
postResolvedProfiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onResolved(si: NsdServiceInfo) = execute {
|
|
||||||
resolvedProfiles.add(ServerProfile(name = si.serviceName, host = si.host.hostAddress!!, port = si.port))
|
|
||||||
postResolvedProfiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onResolveFinished(finishedResolve: ResolveListener) = execute {
|
|
||||||
pendingResolves.remove(finishedResolve)
|
|
||||||
pendingResolves.keys.firstOrNull()?.let { nsdManager?.resolveService(pendingResolves[it], it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for discovery process.
|
|
||||||
*/
|
|
||||||
private class DiscoveryListener : NsdManager.DiscoveryListener {
|
|
||||||
override fun onDiscoveryStarted(serviceType: String?) = impl.onStarted()
|
|
||||||
override fun onDiscoveryStopped(serviceType: String?) = impl.onStopped()
|
|
||||||
override fun onServiceFound(serviceInfo: NsdServiceInfo) = impl.onServiceFound(serviceInfo)
|
|
||||||
override fun onServiceLost(serviceInfo: NsdServiceInfo) = impl.onServiceLost(serviceInfo)
|
|
||||||
|
|
||||||
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
|
||||||
Log.e(TAG, "Service discovery failed to start [E: $errorCode ]")
|
|
||||||
impl.onStartFailed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
|
||||||
Log.w(TAG, "Service discovery failed to stop [E: $errorCode ]")
|
|
||||||
// From our perspective, this is same as onDiscoveryStopped().
|
|
||||||
// We can't retry stopping it because NsdManager will clear the listener
|
|
||||||
// before invoking this callback.
|
|
||||||
impl.onStopped()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for service resolution result.
|
|
||||||
*/
|
|
||||||
private class ResolveListener : NsdManager.ResolveListener {
|
|
||||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
|
||||||
impl.onResolved(serviceInfo)
|
|
||||||
impl.onResolveFinished(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
|
||||||
Log.e(TAG, "Service resolution failed for '${serviceInfo}' [E: $errorCode]")
|
|
||||||
impl.onResolveFinished(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2024 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.viewmodel.service
|
|
||||||
|
|
||||||
import android.util.Base64
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.gaurav.avnc.model.LoginInfo
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
|
||||||
import com.trilead.ssh2.Connection
|
|
||||||
import com.trilead.ssh2.KnownHosts
|
|
||||||
import com.trilead.ssh2.LocalPortForwarder
|
|
||||||
import com.trilead.ssh2.ServerHostKeyVerifier
|
|
||||||
import com.trilead.ssh2.crypto.PEMDecoder
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.InetAddress
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.NoRouteToHostException
|
|
||||||
import java.net.ServerSocket
|
|
||||||
import java.security.KeyPair
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Container for SSH Host Key
|
|
||||||
*/
|
|
||||||
class HostKey(
|
|
||||||
val host: String,
|
|
||||||
val isKnownHost: Boolean,
|
|
||||||
val algo: String,
|
|
||||||
val key: ByteArray,
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Returns SHA-256 fingerprint of the [key].
|
|
||||||
*/
|
|
||||||
fun getFingerprint(): String {
|
|
||||||
val sha256 = MessageDigest.getInstance("SHA-256").digest(key)
|
|
||||||
val base64 = Base64.encodeToString(sha256, Base64.NO_PADDING)
|
|
||||||
return "SHA256:$base64"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements Host Key verification.
|
|
||||||
*
|
|
||||||
* Known hosts & keys are stored in a file inside app's private storage.
|
|
||||||
* For unknown host, user is prompted to confirm the key.
|
|
||||||
*/
|
|
||||||
class HostKeyVerifier(private val viewModel: VncViewModel) : ServerHostKeyVerifier {
|
|
||||||
|
|
||||||
private val knownHostsFile = File(viewModel.app.filesDir, "known-hosts")
|
|
||||||
|
|
||||||
private val knownHosts = KnownHosts(knownHostsFile)
|
|
||||||
|
|
||||||
override fun verifyServerHostKey(hostname: String, port: Int, keyAlgorithm: String, key: ByteArray): Boolean {
|
|
||||||
val verification = knownHosts.verifyHostkey(hostname, keyAlgorithm, key)
|
|
||||||
|
|
||||||
if (verification == KnownHosts.HOSTKEY_IS_OK)
|
|
||||||
return true
|
|
||||||
|
|
||||||
val isKnownHost = (verification == KnownHosts.HOSTKEY_HAS_CHANGED)
|
|
||||||
val hostKey = HostKey(hostname, isKnownHost, keyAlgorithm, key)
|
|
||||||
|
|
||||||
if (viewModel.sshHostKeyVerifyRequest.requestResponse(hostKey)) {
|
|
||||||
//User has confirmed the key, so remember it.
|
|
||||||
KnownHosts.addHostkeyToFile(knownHostsFile, arrayOf(hostname), keyAlgorithm, key)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Small wrapper around [LocalPortForwarder].
|
|
||||||
*
|
|
||||||
* Once connection has been successfully established via [host] & [port],
|
|
||||||
* this gate should be closed to stop new connections.
|
|
||||||
*/
|
|
||||||
class TunnelGate(val host: String, val port: Int, private val forwarder: LocalPortForwarder) : Closeable {
|
|
||||||
override fun close() {
|
|
||||||
forwarder.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manager for SSH Tunnel
|
|
||||||
*/
|
|
||||||
class SshTunnel(private val viewModel: VncViewModel) {
|
|
||||||
|
|
||||||
private var connection: Connection? = null
|
|
||||||
private val localHost = "127.0.0.1"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the tunnel according to current profile in [viewModel].
|
|
||||||
*/
|
|
||||||
fun open(): TunnelGate {
|
|
||||||
val profile = viewModel.profile
|
|
||||||
val connection = connect(profile)
|
|
||||||
this.connection = connection
|
|
||||||
|
|
||||||
authenticate(connection, profile)
|
|
||||||
|
|
||||||
if (!connection.isAuthenticationComplete)
|
|
||||||
throw IOException("SSH authentication failed")
|
|
||||||
|
|
||||||
|
|
||||||
// SSHLib does not expose internal ServerSocket used for local port forwarder.
|
|
||||||
// Hence, if we pass 0 as local port to let the system pick a port for us, we have no way
|
|
||||||
// to know the port system picked.
|
|
||||||
// So we create a temporary ServerSocket, close it immediately and try to use its port.
|
|
||||||
// But between the close-reuse, that port can be assigned to someone else, so we try again.
|
|
||||||
for (i in 1..50) {
|
|
||||||
val attemptedPort = ServerSocket(0).use { it.localPort }
|
|
||||||
val address = InetSocketAddress(localHost, attemptedPort)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val forwarder = connection.createLocalPortForwarder(address, profile.host, profile.port)
|
|
||||||
return TunnelGate(localHost, attemptedPort, forwarder)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
//Retry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw IOException("Cannot find a local port for SSH Tunnel")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It is possible for a host to have multiple IP addresses.
|
|
||||||
* If connection failed due to [NoRouteToHostException], we try the next address (if available).
|
|
||||||
*/
|
|
||||||
private fun connect(profile: ServerProfile): Connection {
|
|
||||||
for (address in InetAddress.getAllByName(profile.sshHost)) {
|
|
||||||
try {
|
|
||||||
return Connection(address.hostAddress, profile.sshPort).apply { connect(HostKeyVerifier(viewModel)) }
|
|
||||||
} catch (e: IOException) {
|
|
||||||
if (e.cause is NoRouteToHostException) continue
|
|
||||||
else throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We will reach here only if every address throws NoRouteToHostException
|
|
||||||
throw NoRouteToHostException("Unreachable SSH host: ${profile.sshHost}")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun authenticate(connection: Connection, profile: ServerProfile) {
|
|
||||||
when (profile.sshAuthType) {
|
|
||||||
ServerProfile.SSH_AUTH_PASSWORD -> {
|
|
||||||
val password = viewModel.getLoginInfo(LoginInfo.Type.SSH_PASSWORD).password //Possibly blocking call
|
|
||||||
connection.authenticateWithPassword(profile.sshUsername, password)
|
|
||||||
}
|
|
||||||
ServerProfile.SSH_AUTH_KEY -> {
|
|
||||||
val pk = profile.sshPrivateKey
|
|
||||||
val cached = KeyCache.get(pk)
|
|
||||||
if (cached != null) {
|
|
||||||
connection.authenticateWithPublicKey(profile.sshUsername, cached)
|
|
||||||
} else {
|
|
||||||
val pem = PEMDecoder.parsePEM(pk.toCharArray())
|
|
||||||
var password = ""
|
|
||||||
if (PEMDecoder.isPEMEncrypted(pem)) {
|
|
||||||
password = viewModel.getLoginInfo(LoginInfo.Type.SSH_KEY_PASSWORD).password //Blocking call
|
|
||||||
}
|
|
||||||
val keyPair = PEMDecoder.decode(pem, password)
|
|
||||||
connection.authenticateWithPublicKey(profile.sshUsername, keyPair)
|
|
||||||
KeyCache.put(pk, keyPair)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> throw IOException("Unknown SSH auth type: ${profile.sshAuthType}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
connection?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A very simple key cache to keep unlocked/decoded keys in memory
|
|
||||||
* Strategy:
|
|
||||||
* 1. Keep keys in memory as long as app is in foreground
|
|
||||||
* 2. Clear cache if app goes in background for more than 15 minutes
|
|
||||||
*/
|
|
||||||
private object KeyCache {
|
|
||||||
private val cache = mutableMapOf<String, KeyPair>()
|
|
||||||
private var lifecycleObserver: DefaultLifecycleObserver? = null
|
|
||||||
|
|
||||||
fun get(pk: String): KeyPair? {
|
|
||||||
synchronized(cache) {
|
|
||||||
return cache[pk]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun put(pk: String, keyPair: KeyPair) {
|
|
||||||
synchronized(cache) {
|
|
||||||
cache[pk] = keyPair
|
|
||||||
addLifecycleObserver()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addLifecycleObserver() {
|
|
||||||
if (lifecycleObserver != null)
|
|
||||||
return // Already added
|
|
||||||
|
|
||||||
lifecycleObserver = object : DefaultLifecycleObserver {
|
|
||||||
var cleanupJob: Job? = null
|
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
|
||||||
cleanupJob?.let { if (it.isActive) it.cancel() }
|
|
||||||
cleanupJob = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
|
||||||
cleanupJob = owner.lifecycleScope.launch {
|
|
||||||
delay(15 * 60 * 1000)
|
|
||||||
synchronized(cache) {
|
|
||||||
cache.values.forEach { it.private.destroy() }
|
|
||||||
cache.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().let {
|
|
||||||
// Observer needs to be set on main thread,
|
|
||||||
// and lifecycleScope is already bound to main thread
|
|
||||||
it.lifecycleScope.launch {
|
|
||||||
it.lifecycle.addObserver(lifecycleObserver!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.vnc
|
|
||||||
|
|
||||||
import android.graphics.PointF
|
|
||||||
import android.util.Log
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows sending different types of messages to remote server.
|
|
||||||
*/
|
|
||||||
class Messenger(private val client: VncClient) {
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Sender thread
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
private val sender = Executors.newSingleThreadExecutor()
|
|
||||||
private val senderLock = Any()
|
|
||||||
|
|
||||||
private fun execute(action: Runnable) {
|
|
||||||
synchronized(senderLock) {
|
|
||||||
if (!sender.isShutdown)
|
|
||||||
sender.execute(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cleanup() {
|
|
||||||
synchronized(senderLock) { sender.shutdown() }
|
|
||||||
runCatching { sender.awaitTermination(60, TimeUnit.SECONDS) }
|
|
||||||
if (!sender.isTerminated) Log.w(javaClass.simpleName, "Unable to fully stop Sender thread!")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Input events
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keeps track of current pointer button state.
|
|
||||||
*/
|
|
||||||
private var pointerButtonMask: Int = 0
|
|
||||||
|
|
||||||
private fun sendPointerEvent(mask: Int, p: PointF) {
|
|
||||||
val x = p.x.toInt()
|
|
||||||
val y = p.y.toInt()
|
|
||||||
client.moveClientPointer(x, y)
|
|
||||||
execute { client.sendPointerEvent(x, y, mask) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendPointerButtonDown(button: PointerButton, p: PointF) {
|
|
||||||
pointerButtonMask = pointerButtonMask or button.bitMask
|
|
||||||
sendPointerEvent(pointerButtonMask, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendPointerButtonUp(button: PointerButton, p: PointF) {
|
|
||||||
pointerButtonMask = pointerButtonMask and button.bitMask.inv()
|
|
||||||
sendPointerEvent(pointerButtonMask, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendPointerButtonRelease(p: PointF) {
|
|
||||||
if (pointerButtonMask != 0) {
|
|
||||||
pointerButtonMask = 0
|
|
||||||
sendPointerEvent(pointerButtonMask, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendKey(keySym: Int, xtCode: Int, isDown: Boolean): Boolean {
|
|
||||||
if (!client.connected)
|
|
||||||
return false
|
|
||||||
|
|
||||||
execute { client.sendKeyEvent(keySym, xtCode, isDown) }
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun insertButtonUpDelay() {
|
|
||||||
execute { runCatching { Thread.sleep(200) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**************************************************************************
|
|
||||||
* Misc
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
fun sendClipboardText(text: String) {
|
|
||||||
execute { client.sendCutText(text) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDesktopSize(width: Int, height: Int) {
|
|
||||||
execute { client.setDesktopSize(width, height) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshFrameBuffer() {
|
|
||||||
execute { client.refreshFrameBuffer() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.vnc
|
|
||||||
|
|
||||||
enum class PointerButton(val bitMask: Int) {
|
|
||||||
None(0),
|
|
||||||
Left(1),
|
|
||||||
Middle(2),
|
|
||||||
Right(4),
|
|
||||||
WheelUp(8),
|
|
||||||
WheelDown(16),
|
|
||||||
WheelLeft(32),
|
|
||||||
WheelRight(64)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.vnc
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is used for returning user credentials from callbacks.
|
|
||||||
*/
|
|
||||||
data class UserCredential(
|
|
||||||
@JvmField val username: String = "",
|
|
||||||
@JvmField val password: String = ""
|
|
||||||
)
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
package com.gaurav.avnc.vnc
|
|
||||||
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import androidx.annotation.Keep
|
|
||||||
import java.io.IOException
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a thin wrapper around native client.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* - +------------+ +----------+
|
|
||||||
* - | Public API | | Observer |
|
|
||||||
* - +------------+ +-----A----+
|
|
||||||
* - | |
|
|
||||||
* - | |
|
|
||||||
* - JNI -------|------------------------------------------------|-----------
|
|
||||||
* - | |
|
|
||||||
* - | |
|
|
||||||
* - +-------v--------+ +--------------+ +--------v---------+
|
|
||||||
* - | Native Methods |------>| LibVNCClient |<----->| Native Callbacks |
|
|
||||||
* - +----------------+ +--------------+ +------------------+
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* For every new instance of [VncClient], we create a native 'rfbClient' and
|
|
||||||
* store its pointer in [nativePtr]. Parameters for connection can be setup using
|
|
||||||
* [configure]. Connection is then started using [connect]. Then incoming
|
|
||||||
* messages are handled by [processServerMessage].
|
|
||||||
*
|
|
||||||
* To release the resources you must call [cleanup] after you are done with
|
|
||||||
* this instance.
|
|
||||||
*/
|
|
||||||
class VncClient(private val observer: Observer) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for event observer.
|
|
||||||
* DO NOT throw exceptions from these methods.
|
|
||||||
* There is NO guarantee about which thread will invoke [Observer] methods.
|
|
||||||
*/
|
|
||||||
interface Observer {
|
|
||||||
fun onPasswordRequired(): String
|
|
||||||
fun onCredentialRequired(): UserCredential
|
|
||||||
fun onGotXCutText(text: String)
|
|
||||||
fun onFramebufferUpdated()
|
|
||||||
fun onFramebufferSizeChanged(width: Int, height: Int)
|
|
||||||
fun onPointerMoved(x: Int, y: Int)
|
|
||||||
|
|
||||||
//fun onBell()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Value of the pointer to native 'rfbClient'. This is passed to all native methods.
|
|
||||||
*/
|
|
||||||
private val nativePtr: Long
|
|
||||||
|
|
||||||
init {
|
|
||||||
nativePtr = nativeClientCreate()
|
|
||||||
if (nativePtr == 0L)
|
|
||||||
throw RuntimeException("Could not create native rfbClient!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
var connected = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of remote desktop
|
|
||||||
*/
|
|
||||||
val desktopName; get() = nativeGetDesktopName(nativePtr)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether connection is encrypted
|
|
||||||
*/
|
|
||||||
val isEncrypted; get() = nativeIsEncrypted(nativePtr)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In 'View-only' mode input to remote server is disabled
|
|
||||||
*/
|
|
||||||
var viewOnlyMode = false; private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Latest pointer position. See [moveClientPointer].
|
|
||||||
*/
|
|
||||||
var pointerX = 0; private set
|
|
||||||
var pointerY = 0; private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client-side cursor rendering creates a synchronization issue.
|
|
||||||
* Suppose if pointer is moved to (50,10) by client. A PointerEvent is sent
|
|
||||||
* to the server and cursor is immediately rendered on (50,10).
|
|
||||||
* Some servers (e.g. Vino) will send back a PointerPosition event for (50, 10).
|
|
||||||
* But, by the time that event is received from server, pointer on client
|
|
||||||
* might have already moved to (60,20) (this is almost guaranteed to happen
|
|
||||||
* with touchpad/relative action mode). So the cursor will probably 'jump back'
|
|
||||||
* depending on the order of these events.
|
|
||||||
*
|
|
||||||
* This flags works around the issue by temporarily ignoring serer-side updates.
|
|
||||||
*/
|
|
||||||
@Volatile
|
|
||||||
var ignorePointerMovesByServer = false
|
|
||||||
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var autoFBRequestsQueued = true
|
|
||||||
private var autoFBRequests = autoFBRequestsQueued
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Value of the most recent cut text sent/received from server
|
|
||||||
*/
|
|
||||||
@Volatile
|
|
||||||
private var lastCutText: String? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup different properties for this client.
|
|
||||||
*
|
|
||||||
* @param securityType RFB security type to use.
|
|
||||||
*/
|
|
||||||
fun configure(viewOnly: Boolean, securityType: Int, useLocalCursor: Boolean, imageQuality: Int, useRawEncoding: Boolean) {
|
|
||||||
viewOnlyMode = viewOnly
|
|
||||||
nativeConfigure(nativePtr, securityType, useLocalCursor, imageQuality, useRawEncoding)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setupRepeater(serverId: Int) {
|
|
||||||
nativeSetDest(nativePtr, "ID", serverId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes VNC connection.
|
|
||||||
*/
|
|
||||||
fun connect(host: String, port: Int) {
|
|
||||||
connected = nativeInit(nativePtr, host, port)
|
|
||||||
if (!connected) throw IOException(nativeGetLastErrorStr())
|
|
||||||
applyCompatQuirks()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Waits for incoming server message, parses it and then invokes appropriate callbacks.
|
|
||||||
*
|
|
||||||
* @param uSecTimeout Timeout in microseconds.
|
|
||||||
*/
|
|
||||||
fun processServerMessage(uSecTimeout: Int = 1000000) {
|
|
||||||
if (!connected)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (!nativeProcessServerMessage(nativePtr, uSecTimeout)) {
|
|
||||||
connected = false
|
|
||||||
throw IOException(nativeGetLastErrorStr())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoFBRequests != autoFBRequestsQueued) {
|
|
||||||
autoFBRequests = autoFBRequestsQueued
|
|
||||||
nativeSetAutomaticFramebufferUpdates(nativePtr, autoFBRequests)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends Key event to remote server.
|
|
||||||
*
|
|
||||||
* @param keySym Key symbol
|
|
||||||
* @param xtCode Key code from [XTKeyCode]
|
|
||||||
* @param isDown true for key down, false for key up
|
|
||||||
*/
|
|
||||||
fun sendKeyEvent(keySym: Int, xtCode: Int, isDown: Boolean) = ifConnectedAndInteractive {
|
|
||||||
nativeSendKeyEvent(nativePtr, keySym, xtCode, isDown)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends pointer event to remote server.
|
|
||||||
*
|
|
||||||
* @param x Horizontal pointer coordinate
|
|
||||||
* @param y Vertical pointer coordinate
|
|
||||||
* @param mask Button mask to identify which button was pressed.
|
|
||||||
*/
|
|
||||||
fun sendPointerEvent(x: Int, y: Int, mask: Int) = ifConnectedAndInteractive {
|
|
||||||
nativeSendPointerEvent(nativePtr, x, y, mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates client-side pointer position.
|
|
||||||
* No event is sent to server.
|
|
||||||
*
|
|
||||||
* Primary use-case is to update pointer position during gestures.
|
|
||||||
* This way we can immediately render the cursor on new position without
|
|
||||||
* waiting for Network IO.
|
|
||||||
*
|
|
||||||
* It also helps with servers which don't send pointer-position updates
|
|
||||||
* if pointer was moved by the client.
|
|
||||||
*
|
|
||||||
* @param x Horizontal pointer coordinate
|
|
||||||
* @param y Vertical pointer coordinate
|
|
||||||
*/
|
|
||||||
fun moveClientPointer(x: Int, y: Int) {
|
|
||||||
pointerX = x
|
|
||||||
pointerY = y
|
|
||||||
observer.onPointerMoved(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends text to remote desktop's clipboard.
|
|
||||||
*/
|
|
||||||
fun sendCutText(text: String) = ifConnectedAndInteractive {
|
|
||||||
if (text != lastCutText) {
|
|
||||||
val sent = if (nativeIsUTF8CutTextSupported(nativePtr))
|
|
||||||
nativeSendCutText(nativePtr, text.toByteArray(StandardCharsets.UTF_8), true)
|
|
||||||
else
|
|
||||||
nativeSendCutText(nativePtr, text.toByteArray(StandardCharsets.ISO_8859_1), false)
|
|
||||||
if (sent)
|
|
||||||
lastCutText = text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set remote desktop size to given dimensions.
|
|
||||||
* This needs server support to actually work.
|
|
||||||
* Non-positive [width] & [height] are ignored.
|
|
||||||
*/
|
|
||||||
fun setDesktopSize(width: Int, height: Int) = ifConnected {
|
|
||||||
if (width > 0 && height > 0)
|
|
||||||
nativeSetDesktopSize(nativePtr, width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends frame buffer update request to remote server.
|
|
||||||
*/
|
|
||||||
fun refreshFrameBuffer() = ifConnected {
|
|
||||||
nativeRefreshFrameBuffer(nativePtr)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls whether framebuffer update requests are sent automatically.
|
|
||||||
* It takes effect after the next call to [processServerMessage].
|
|
||||||
*/
|
|
||||||
/*fun setAutomaticFrameBufferUpdates(enabled: Boolean) = ifConnected {
|
|
||||||
//autoFBRequestsQueued = enabled
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts framebuffer contents in currently active OpenGL texture.
|
|
||||||
* Must be called from an OpenGL ES context (i.e. from renderer thread).
|
|
||||||
*/
|
|
||||||
fun uploadFrameTexture() = nativeUploadFrameTexture(nativePtr)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload cursor shape into framebuffer texture.
|
|
||||||
*/
|
|
||||||
fun uploadCursor() = nativeUploadCursor(nativePtr, pointerX, pointerY)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release all resources allocated by the client.
|
|
||||||
* DO NOT use this client after [cleanup].
|
|
||||||
*/
|
|
||||||
fun cleanup() {
|
|
||||||
connected = false
|
|
||||||
nativeCleanup(nativePtr)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun ifConnected(block: () -> Unit) {
|
|
||||||
if (connected)
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun ifConnectedAndInteractive(block: () -> Unit) = ifConnected {
|
|
||||||
if (!viewOnlyMode)
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applyCompatQuirks() {
|
|
||||||
if (nativeIsServerMacOS(nativePtr)) {
|
|
||||||
XKeySymAndroid.updateKeyMap(KeyEvent.KEYCODE_ALT_LEFT, XKeySym.XK_Meta_L)
|
|
||||||
XKeySymAndroid.updateKeyMap(KeyEvent.KEYCODE_ALT_RIGHT, XKeySym.XK_Meta_R)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun nativeClientCreate(): Long
|
|
||||||
private external fun nativeConfigure(clientPtr: Long, securityType: Int, useLocalCursor: Boolean, imageQuality: Int, useRawEncoding: Boolean)
|
|
||||||
private external fun nativeInit(clientPtr: Long, host: String, port: Int): Boolean
|
|
||||||
private external fun nativeSetDest(clientPtr: Long, host: String, port: Int)
|
|
||||||
private external fun nativeProcessServerMessage(clientPtr: Long, uSecTimeout: Int): Boolean
|
|
||||||
private external fun nativeSendKeyEvent(clientPtr: Long, keySym: Int, xtCode: Int, isDown: Boolean): Boolean
|
|
||||||
private external fun nativeSendPointerEvent(clientPtr: Long, x: Int, y: Int, mask: Int): Boolean
|
|
||||||
private external fun nativeSendCutText(clientPtr: Long, bytes: ByteArray, isUTF8: Boolean): Boolean
|
|
||||||
private external fun nativeIsUTF8CutTextSupported(clientPtr: Long): Boolean
|
|
||||||
private external fun nativeSetDesktopSize(clientPtr: Long, width: Int, height: Int): Boolean
|
|
||||||
private external fun nativeRefreshFrameBuffer(clientPtr: Long): Boolean
|
|
||||||
private external fun nativeSetAutomaticFramebufferUpdates(clientPtr: Long, enabled: Boolean)
|
|
||||||
private external fun nativeGetDesktopName(clientPtr: Long): String
|
|
||||||
private external fun nativeGetWidth(clientPtr: Long): Int
|
|
||||||
private external fun nativeGetHeight(clientPtr: Long): Int
|
|
||||||
private external fun nativeIsEncrypted(clientPtr: Long): Boolean
|
|
||||||
private external fun nativeUploadFrameTexture(clientPtr: Long)
|
|
||||||
private external fun nativeUploadCursor(clientPtr: Long, px: Int, py: Int)
|
|
||||||
private external fun nativeGetLastErrorStr(): String
|
|
||||||
private external fun nativeIsServerMacOS(clientPtr: Long): Boolean
|
|
||||||
private external fun nativeCleanup(clientPtr: Long)
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
private fun cbGetPassword() = observer.onPasswordRequired()
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
private fun cbGetCredential() = observer.onCredentialRequired()
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
private fun cbGotXCutText(bytes: ByteArray, isUTF8: Boolean) {
|
|
||||||
(if (isUTF8) StandardCharsets.UTF_8 else StandardCharsets.ISO_8859_1).let {
|
|
||||||
val cutText = it.decode(ByteBuffer.wrap(bytes)).toString()
|
|
||||||
if (cutText != lastCutText) {
|
|
||||||
lastCutText = cutText
|
|
||||||
observer.onGotXCutText(cutText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
private fun cbFinishedFrameBufferUpdate() = observer.onFramebufferUpdated()
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
private fun cbFramebufferSizeChanged(w: Int, h: Int) = observer.onFramebufferSizeChanged(w, h)
|
|
||||||
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
private fun cbBell() = Unit // observer.onBell()
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
private fun cbHandleCursorPos(x: Int, y: Int) {
|
|
||||||
if (!ignorePointerMovesByServer)
|
|
||||||
moveClientPointer(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Native library initialization
|
|
||||||
*/
|
|
||||||
companion object {
|
|
||||||
fun loadLibrary() {
|
|
||||||
System.loadLibrary("native-vnc")
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
private external fun initLibrary()
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadLibrary()
|
|
||||||
initLibrary()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.vnc
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class implements the `vnc` URI scheme.
|
|
||||||
* Reference: https://tools.ietf.org/html/rfc7869
|
|
||||||
*
|
|
||||||
* If host in given URI string is an IPv6 address, it MUST be wrapped in square brackets.
|
|
||||||
* (This requirement come from using Java [URI] internally.)
|
|
||||||
*
|
|
||||||
* If given URI doesn't start with 'vnc://' scheme, it will be automatically added.
|
|
||||||
*/
|
|
||||||
class VncUri(str: String) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add scheme if missing.
|
|
||||||
* It is also common for users to accidentally type 'vnc:host' instead of 'vnc://host',
|
|
||||||
* so we gracefully handle that case too.
|
|
||||||
*/
|
|
||||||
private val uriString = str.replaceFirst(Regex("^(vnc:/?/?)?", RegexOption.IGNORE_CASE), "vnc://")
|
|
||||||
|
|
||||||
private val uri = Uri.parse(uriString)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Older versions of Android [Uri] does not support IPv6, so we need to use Java [URI] for host & port.
|
|
||||||
* It also serves as a validation step because [URI] verifies that address is well-formed.
|
|
||||||
*/
|
|
||||||
private val javaUri = runCatching { URI(uriString) }.getOrNull()
|
|
||||||
|
|
||||||
|
|
||||||
val host = javaUri?.host?.trim('[', ']') ?: ""
|
|
||||||
val port = if (javaUri?.port == -1) 5900 else javaUri?.port ?: 5900
|
|
||||||
val connectionName = uri.getQueryParameter("ConnectionName") ?: ""
|
|
||||||
val username = uri.getQueryParameter("VncUsername") ?: ""
|
|
||||||
val password = uri.getQueryParameter("VncPassword") ?: ""
|
|
||||||
val securityType = uri.getQueryParameter("SecurityType")?.toIntOrNull() ?: 0
|
|
||||||
val channelType = uri.getQueryParameter("ChannelType")?.toIntOrNull() ?: ServerProfile.CHANNEL_TCP
|
|
||||||
val colorLevel = uri.getQueryParameter("ColorLevel")?.toIntOrNull() ?: 7
|
|
||||||
val viewOnly = uri.getBooleanQueryParameter("ViewOnly", false)
|
|
||||||
val saveConnection = uri.getBooleanQueryParameter("SaveConnection", false)
|
|
||||||
val sshHost = uri.getQueryParameter("SshHost") ?: host
|
|
||||||
val sshPort = uri.getQueryParameter("SshPort")?.toIntOrNull() ?: 22
|
|
||||||
val sshUsername = uri.getQueryParameter("SshUsername") ?: ""
|
|
||||||
val sshPassword = uri.getQueryParameter("SshPassword") ?: ""
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a [ServerProfile] using this instance.
|
|
||||||
*/
|
|
||||||
fun toServerProfile() = ServerProfile(
|
|
||||||
name = connectionName,
|
|
||||||
host = host,
|
|
||||||
port = port,
|
|
||||||
username = username,
|
|
||||||
password = password,
|
|
||||||
securityType = securityType,
|
|
||||||
channelType = channelType,
|
|
||||||
colorLevel = colorLevel,
|
|
||||||
viewOnly = viewOnly,
|
|
||||||
sshHost = sshHost,
|
|
||||||
sshPort = sshPort,
|
|
||||||
sshUsername = sshUsername,
|
|
||||||
sshAuthType = ServerProfile.SSH_AUTH_PASSWORD,
|
|
||||||
sshPassword = sshPassword
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun toString() = uriString
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,337 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.vnc
|
|
||||||
|
|
||||||
import android.view.KeyEvent
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements mapping between [KeyEvent] key codes & X KeySyms.
|
|
||||||
*/
|
|
||||||
object XKeySymAndroid {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns X KeySym for given [keyCode].
|
|
||||||
* Returns 0 if no mapping is found.
|
|
||||||
*/
|
|
||||||
fun getKeySymForAndroidKeyCode(keyCode: Int): Int {
|
|
||||||
if (keyCode >= 0 && keyCode < AndroidKeyCodeToXKeySym.size)
|
|
||||||
return AndroidKeyCodeToXKeySym[keyCode]
|
|
||||||
else
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateKeyMap(keyCode: Int, xKeySym: Int) {
|
|
||||||
if (keyCode >= 0 && keyCode < AndroidKeyCodeToXKeySym.size)
|
|
||||||
AndroidKeyCodeToXKeySym[keyCode] = xKeySym
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lookup table for X KeySym.
|
|
||||||
*
|
|
||||||
* Each index represents a keycode from [KeyEvent] and
|
|
||||||
* value at that index represents the corresponding X KeySym.
|
|
||||||
*/
|
|
||||||
private val AndroidKeyCodeToXKeySym = intArrayOf(
|
|
||||||
0, // KEYCODE_UNKNOWN = 0
|
|
||||||
0, // KEYCODE_SOFT_LEFT = 1
|
|
||||||
0, // KEYCODE_SOFT_RIGHT = 2
|
|
||||||
0, // KEYCODE_HOME = 3
|
|
||||||
0, // KEYCODE_BACK = 4
|
|
||||||
0, // KEYCODE_CALL = 5
|
|
||||||
0, // KEYCODE_ENDCALL = 6
|
|
||||||
XKeySym.XK_0, // KEYCODE_0 = 7
|
|
||||||
XKeySym.XK_1, // KEYCODE_1 = 8
|
|
||||||
XKeySym.XK_2, // KEYCODE_2 = 9
|
|
||||||
XKeySym.XK_3, // KEYCODE_3 = 10
|
|
||||||
XKeySym.XK_4, // KEYCODE_4 = 11
|
|
||||||
XKeySym.XK_5, // KEYCODE_5 = 12
|
|
||||||
XKeySym.XK_6, // KEYCODE_6 = 13
|
|
||||||
XKeySym.XK_7, // KEYCODE_7 = 14
|
|
||||||
XKeySym.XK_8, // KEYCODE_8 = 15
|
|
||||||
XKeySym.XK_9, // KEYCODE_9 = 16
|
|
||||||
XKeySym.XK_asterisk, // KEYCODE_STAR = 17
|
|
||||||
XKeySym.XK_numbersign, // KEYCODE_POUND = 18
|
|
||||||
XKeySym.XK_Up, // KEYCODE_DPAD_UP = 19
|
|
||||||
XKeySym.XK_Down, // KEYCODE_DPAD_DOWN = 20
|
|
||||||
XKeySym.XK_Left, // KEYCODE_DPAD_LEFT = 21
|
|
||||||
XKeySym.XK_Right, // KEYCODE_DPAD_RIGHT = 22
|
|
||||||
0, // KEYCODE_DPAD_CENTER = 23
|
|
||||||
XKeySym.XF86XK_AudioRaiseVolume, // KEYCODE_VOLUME_UP = 24
|
|
||||||
XKeySym.XF86XK_AudioLowerVolume, // KEYCODE_VOLUME_DOWN = 25
|
|
||||||
0, // KEYCODE_POWER = 26
|
|
||||||
0, // KEYCODE_CAMERA = 27
|
|
||||||
0, // KEYCODE_CLEAR = 28
|
|
||||||
XKeySym.XK_a, // KEYCODE_A = 29
|
|
||||||
XKeySym.XK_b, // KEYCODE_B = 30
|
|
||||||
XKeySym.XK_c, // KEYCODE_C = 31
|
|
||||||
XKeySym.XK_d, // KEYCODE_D = 32
|
|
||||||
XKeySym.XK_e, // KEYCODE_E = 33
|
|
||||||
XKeySym.XK_f, // KEYCODE_F = 34
|
|
||||||
XKeySym.XK_g, // KEYCODE_G = 35
|
|
||||||
XKeySym.XK_h, // KEYCODE_H = 36
|
|
||||||
XKeySym.XK_i, // KEYCODE_I = 37
|
|
||||||
XKeySym.XK_j, // KEYCODE_J = 38
|
|
||||||
XKeySym.XK_k, // KEYCODE_K = 39
|
|
||||||
XKeySym.XK_l, // KEYCODE_L = 40
|
|
||||||
XKeySym.XK_m, // KEYCODE_M = 41
|
|
||||||
XKeySym.XK_n, // KEYCODE_N = 42
|
|
||||||
XKeySym.XK_o, // KEYCODE_O = 43
|
|
||||||
XKeySym.XK_p, // KEYCODE_P = 44
|
|
||||||
XKeySym.XK_q, // KEYCODE_Q = 45
|
|
||||||
XKeySym.XK_r, // KEYCODE_R = 46
|
|
||||||
XKeySym.XK_s, // KEYCODE_S = 47
|
|
||||||
XKeySym.XK_t, // KEYCODE_T = 48
|
|
||||||
XKeySym.XK_u, // KEYCODE_U = 49
|
|
||||||
XKeySym.XK_v, // KEYCODE_V = 50
|
|
||||||
XKeySym.XK_w, // KEYCODE_W = 51
|
|
||||||
XKeySym.XK_x, // KEYCODE_X = 52
|
|
||||||
XKeySym.XK_y, // KEYCODE_Y = 53
|
|
||||||
XKeySym.XK_z, // KEYCODE_Z = 54
|
|
||||||
XKeySym.XK_comma, // KEYCODE_COMMA = 55
|
|
||||||
XKeySym.XK_period, // KEYCODE_PERIOD = 56
|
|
||||||
XKeySym.XK_Alt_L, // KEYCODE_ALT_LEFT = 57
|
|
||||||
XKeySym.XK_Alt_R, // KEYCODE_ALT_RIGHT = 58
|
|
||||||
XKeySym.XK_Shift_L, // KEYCODE_SHIFT_LEFT = 59
|
|
||||||
XKeySym.XK_Shift_R, // KEYCODE_SHIFT_RIGHT = 60
|
|
||||||
XKeySym.XK_Tab, // KEYCODE_TAB = 61
|
|
||||||
XKeySym.XK_space, // KEYCODE_SPACE = 62
|
|
||||||
0, // KEYCODE_SYM = 63
|
|
||||||
0, // KEYCODE_EXPLORER = 64
|
|
||||||
0, // KEYCODE_ENVELOPE = 65
|
|
||||||
XKeySym.XK_Return, // KEYCODE_ENTER = 66
|
|
||||||
XKeySym.XK_BackSpace, // KEYCODE_DEL = 67
|
|
||||||
XKeySym.XK_grave, // KEYCODE_GRAVE = 68
|
|
||||||
XKeySym.XK_minus, // KEYCODE_MINUS = 69
|
|
||||||
XKeySym.XK_equal, // KEYCODE_EQUALS = 70
|
|
||||||
XKeySym.XK_bracketleft, // KEYCODE_LEFT_BRACKET = 71
|
|
||||||
XKeySym.XK_bracketright, // KEYCODE_RIGHT_BRACKET = 72
|
|
||||||
XKeySym.XK_backslash, // KEYCODE_BACKSLASH = 73
|
|
||||||
XKeySym.XK_semicolon, // KEYCODE_SEMICOLON = 74
|
|
||||||
XKeySym.XK_apostrophe, // KEYCODE_APOSTROPHE = 75
|
|
||||||
XKeySym.XK_slash, // KEYCODE_SLASH = 76
|
|
||||||
XKeySym.XK_at, // KEYCODE_AT = 77
|
|
||||||
0, // KEYCODE_NUM = 78
|
|
||||||
0, // KEYCODE_HEADSETHOOK = 79
|
|
||||||
0, // KEYCODE_FOCUS = 80
|
|
||||||
XKeySym.XK_plus, // KEYCODE_PLUS = 81
|
|
||||||
XKeySym.XK_Menu, // KEYCODE_MENU = 82
|
|
||||||
0, // KEYCODE_NOTIFICATION = 83
|
|
||||||
0, // KEYCODE_SEARCH = 84
|
|
||||||
0, // KEYCODE_MEDIA_PLAY_PAUSE = 85
|
|
||||||
0, // KEYCODE_MEDIA_STOP = 86
|
|
||||||
0, // KEYCODE_MEDIA_NEXT = 87
|
|
||||||
0, // KEYCODE_MEDIA_PREVIOUS = 88
|
|
||||||
0, // KEYCODE_MEDIA_REWIND = 89
|
|
||||||
0, // KEYCODE_MEDIA_FAST_FORWARD = 90
|
|
||||||
0, // KEYCODE_MUTE = 91
|
|
||||||
XKeySym.XK_Page_Up, // KEYCODE_PAGE_UP = 92
|
|
||||||
XKeySym.XK_Page_Down, // KEYCODE_PAGE_DOWN = 93
|
|
||||||
0, // KEYCODE_PICTSYMBOLS = 94
|
|
||||||
0, // KEYCODE_SWITCH_CHARSET = 95
|
|
||||||
0, // KEYCODE_BUTTON_A = 96
|
|
||||||
0, // KEYCODE_BUTTON_B = 97
|
|
||||||
0, // KEYCODE_BUTTON_C = 98
|
|
||||||
0, // KEYCODE_BUTTON_X = 99
|
|
||||||
0, // KEYCODE_BUTTON_Y = 100
|
|
||||||
0, // KEYCODE_BUTTON_Z = 101
|
|
||||||
0, // KEYCODE_BUTTON_L1 = 102
|
|
||||||
0, // KEYCODE_BUTTON_R1 = 103
|
|
||||||
0, // KEYCODE_BUTTON_L2 = 104
|
|
||||||
0, // KEYCODE_BUTTON_R2 = 105
|
|
||||||
0, // KEYCODE_BUTTON_THUMBL = 106
|
|
||||||
0, // KEYCODE_BUTTON_THUMBR = 107
|
|
||||||
0, // KEYCODE_BUTTON_START = 108
|
|
||||||
0, // KEYCODE_BUTTON_SELECT = 109
|
|
||||||
0, // KEYCODE_BUTTON_MODE = 110
|
|
||||||
XKeySym.XK_Escape, // KEYCODE_ESCAPE = 111
|
|
||||||
XKeySym.XK_Delete, // KEYCODE_FORWARD_DEL = 112
|
|
||||||
XKeySym.XK_Control_L, // KEYCODE_CTRL_LEFT = 113
|
|
||||||
XKeySym.XK_Control_R, // KEYCODE_CTRL_RIGHT = 114
|
|
||||||
XKeySym.XK_Caps_Lock, // KEYCODE_CAPS_LOCK = 115
|
|
||||||
XKeySym.XK_Scroll_Lock, // KEYCODE_SCROLL_LOCK = 116
|
|
||||||
XKeySym.XK_Super_L, // KEYCODE_META_LEFT = 117
|
|
||||||
XKeySym.XK_Super_R, // KEYCODE_META_RIGHT = 118
|
|
||||||
0, // KEYCODE_FUNCTION = 119
|
|
||||||
XKeySym.XK_Sys_Req, // KEYCODE_SYSRQ = 120
|
|
||||||
XKeySym.XK_Break, // KEYCODE_BREAK = 121
|
|
||||||
XKeySym.XK_Home, // KEYCODE_MOVE_HOME = 122
|
|
||||||
XKeySym.XK_End, // KEYCODE_MOVE_END = 123
|
|
||||||
XKeySym.XK_Insert, // KEYCODE_INSERT = 124
|
|
||||||
0, // KEYCODE_FORWARD = 125
|
|
||||||
0, // KEYCODE_MEDIA_PLAY = 126
|
|
||||||
0, // KEYCODE_MEDIA_PAUSE = 127
|
|
||||||
0, // KEYCODE_MEDIA_CLOSE = 128
|
|
||||||
0, // KEYCODE_MEDIA_EJECT = 129
|
|
||||||
0, // KEYCODE_MEDIA_RECORD = 130
|
|
||||||
XKeySym.XK_F1, // KEYCODE_F1 = 131
|
|
||||||
XKeySym.XK_F2, // KEYCODE_F2 = 132
|
|
||||||
XKeySym.XK_F3, // KEYCODE_F3 = 133
|
|
||||||
XKeySym.XK_F4, // KEYCODE_F4 = 134
|
|
||||||
XKeySym.XK_F5, // KEYCODE_F5 = 135
|
|
||||||
XKeySym.XK_F6, // KEYCODE_F6 = 136
|
|
||||||
XKeySym.XK_F7, // KEYCODE_F7 = 137
|
|
||||||
XKeySym.XK_F8, // KEYCODE_F8 = 138
|
|
||||||
XKeySym.XK_F9, // KEYCODE_F9 = 139
|
|
||||||
XKeySym.XK_F10, // KEYCODE_F10 = 140
|
|
||||||
XKeySym.XK_F11, // KEYCODE_F11 = 141
|
|
||||||
XKeySym.XK_F12, // KEYCODE_F12 = 142
|
|
||||||
XKeySym.XK_Num_Lock, // KEYCODE_NUM_LOCK = 143
|
|
||||||
XKeySym.XK_KP_0, // KEYCODE_NUMPAD_0 = 144
|
|
||||||
XKeySym.XK_KP_1, // KEYCODE_NUMPAD_1 = 145
|
|
||||||
XKeySym.XK_KP_2, // KEYCODE_NUMPAD_2 = 146
|
|
||||||
XKeySym.XK_KP_3, // KEYCODE_NUMPAD_3 = 147
|
|
||||||
XKeySym.XK_KP_4, // KEYCODE_NUMPAD_4 = 148
|
|
||||||
XKeySym.XK_KP_5, // KEYCODE_NUMPAD_5 = 149
|
|
||||||
XKeySym.XK_KP_6, // KEYCODE_NUMPAD_6 = 150
|
|
||||||
XKeySym.XK_KP_7, // KEYCODE_NUMPAD_7 = 151
|
|
||||||
XKeySym.XK_KP_8, // KEYCODE_NUMPAD_8 = 152
|
|
||||||
XKeySym.XK_KP_9, // KEYCODE_NUMPAD_9 = 153
|
|
||||||
XKeySym.XK_KP_Divide, // KEYCODE_NUMPAD_DIVIDE = 154
|
|
||||||
XKeySym.XK_KP_Multiply, // KEYCODE_NUMPAD_MULTIPLY = 155
|
|
||||||
XKeySym.XK_KP_Subtract, // KEYCODE_NUMPAD_SUBTRACT = 156
|
|
||||||
XKeySym.XK_KP_Add, // KEYCODE_NUMPAD_ADD = 157
|
|
||||||
XKeySym.XK_KP_Decimal, // KEYCODE_NUMPAD_DOT = 158
|
|
||||||
XKeySym.XK_KP_Separator, // KEYCODE_NUMPAD_COMMA = 159
|
|
||||||
XKeySym.XK_KP_Enter, // KEYCODE_NUMPAD_ENTER = 160
|
|
||||||
XKeySym.XK_KP_Equal, // KEYCODE_NUMPAD_EQUALS = 161
|
|
||||||
0, // KEYCODE_NUMPAD_LEFT_PAREN = 162
|
|
||||||
0, // KEYCODE_NUMPAD_RIGHT_PAREN = 163
|
|
||||||
XKeySym.XF86XK_AudioMute, // KEYCODE_VOLUME_MUTE = 164
|
|
||||||
0, // KEYCODE_INFO = 165
|
|
||||||
0, // KEYCODE_CHANNEL_UP = 166
|
|
||||||
0, // KEYCODE_CHANNEL_DOWN = 167
|
|
||||||
0, // KEYCODE_ZOOM_IN = 168
|
|
||||||
0, // KEYCODE_ZOOM_OUT = 169
|
|
||||||
0, // KEYCODE_TV = 170
|
|
||||||
0, // KEYCODE_WINDOW = 171
|
|
||||||
0, // KEYCODE_GUIDE = 172
|
|
||||||
0, // KEYCODE_DVR = 173
|
|
||||||
0, // KEYCODE_BOOKMARK = 174
|
|
||||||
0, // KEYCODE_CAPTIONS = 175
|
|
||||||
0, // KEYCODE_SETTINGS = 176
|
|
||||||
0, // KEYCODE_TV_POWER = 177
|
|
||||||
0, // KEYCODE_TV_INPUT = 178
|
|
||||||
0, // KEYCODE_STB_POWER = 179
|
|
||||||
0, // KEYCODE_STB_INPUT = 180
|
|
||||||
0, // KEYCODE_AVR_POWER = 181
|
|
||||||
0, // KEYCODE_AVR_INPUT = 182
|
|
||||||
0, // KEYCODE_PROG_RED = 183
|
|
||||||
0, // KEYCODE_PROG_GREEN = 184
|
|
||||||
0, // KEYCODE_PROG_YELLOW = 185
|
|
||||||
0, // KEYCODE_PROG_BLUE = 186
|
|
||||||
0, // KEYCODE_APP_SWITCH = 187
|
|
||||||
0, // KEYCODE_BUTTON_1 = 188
|
|
||||||
0, // KEYCODE_BUTTON_2 = 189
|
|
||||||
0, // KEYCODE_BUTTON_3 = 190
|
|
||||||
0, // KEYCODE_BUTTON_4 = 191
|
|
||||||
0, // KEYCODE_BUTTON_5 = 192
|
|
||||||
0, // KEYCODE_BUTTON_6 = 193
|
|
||||||
0, // KEYCODE_BUTTON_7 = 194
|
|
||||||
0, // KEYCODE_BUTTON_8 = 195
|
|
||||||
0, // KEYCODE_BUTTON_9 = 196
|
|
||||||
0, // KEYCODE_BUTTON_10 = 197
|
|
||||||
0, // KEYCODE_BUTTON_11 = 198
|
|
||||||
0, // KEYCODE_BUTTON_12 = 199
|
|
||||||
0, // KEYCODE_BUTTON_13 = 200
|
|
||||||
0, // KEYCODE_BUTTON_14 = 201
|
|
||||||
0, // KEYCODE_BUTTON_15 = 202
|
|
||||||
0, // KEYCODE_BUTTON_16 = 203
|
|
||||||
0, // KEYCODE_LANGUAGE_SWITCH = 204
|
|
||||||
|
|
||||||
/* We currently have no mapping for rest of the key codes.
|
|
||||||
So these are commented to reduce the lookup table size.
|
|
||||||
|
|
||||||
0, // KEYCODE_MANNER_MODE = 205
|
|
||||||
0, // KEYCODE_3D_MODE = 206
|
|
||||||
0, // KEYCODE_CONTACTS = 207
|
|
||||||
0, // KEYCODE_CALENDAR = 208
|
|
||||||
0, // KEYCODE_MUSIC = 209
|
|
||||||
0, // KEYCODE_CALCULATOR = 210
|
|
||||||
0, // KEYCODE_ZENKAKU_HANKAKU = 211
|
|
||||||
0, // KEYCODE_EISU = 212
|
|
||||||
0, // KEYCODE_MUHENKAN = 213
|
|
||||||
0, // KEYCODE_HENKAN = 214
|
|
||||||
0, // KEYCODE_KATAKANA_HIRAGANA = 215
|
|
||||||
0, // KEYCODE_YEN = 216
|
|
||||||
0, // KEYCODE_RO = 217
|
|
||||||
0, // KEYCODE_KANA = 218
|
|
||||||
0, // KEYCODE_ASSIST = 219
|
|
||||||
0, // KEYCODE_BRIGHTNESS_DOWN = 220
|
|
||||||
0, // KEYCODE_BRIGHTNESS_UP = 221
|
|
||||||
0, // KEYCODE_MEDIA_AUDIO_TRACK = 222
|
|
||||||
0, // KEYCODE_SLEEP = 223
|
|
||||||
0, // KEYCODE_WAKEUP = 224
|
|
||||||
0, // KEYCODE_PAIRING = 225
|
|
||||||
0, // KEYCODE_MEDIA_TOP_MENU = 226
|
|
||||||
0, // KEYCODE_11 = 227
|
|
||||||
0, // KEYCODE_12 = 228
|
|
||||||
0, // KEYCODE_LAST_CHANNEL = 229
|
|
||||||
0, // KEYCODE_TV_DATA_SERVICE = 230
|
|
||||||
0, // KEYCODE_VOICE_ASSIST = 231
|
|
||||||
0, // KEYCODE_TV_RADIO_SERVICE = 232
|
|
||||||
0, // KEYCODE_TV_TELETEXT = 233
|
|
||||||
0, // KEYCODE_TV_NUMBER_ENTRY = 234
|
|
||||||
0, // KEYCODE_TV_TERRESTRIAL_ANALOG = 235
|
|
||||||
0, // KEYCODE_TV_TERRESTRIAL_DIGITAL = 236
|
|
||||||
0, // KEYCODE_TV_SATELLITE = 237
|
|
||||||
0, // KEYCODE_TV_SATELLITE_BS = 238
|
|
||||||
0, // KEYCODE_TV_SATELLITE_CS = 239
|
|
||||||
0, // KEYCODE_TV_SATELLITE_SERVICE = 240
|
|
||||||
0, // KEYCODE_TV_NETWORK = 241
|
|
||||||
0, // KEYCODE_TV_ANTENNA_CABLE = 242
|
|
||||||
0, // KEYCODE_TV_INPUT_HDMI_1 = 243
|
|
||||||
0, // KEYCODE_TV_INPUT_HDMI_2 = 244
|
|
||||||
0, // KEYCODE_TV_INPUT_HDMI_3 = 245
|
|
||||||
0, // KEYCODE_TV_INPUT_HDMI_4 = 246
|
|
||||||
0, // KEYCODE_TV_INPUT_COMPOSITE_1 = 247
|
|
||||||
0, // KEYCODE_TV_INPUT_COMPOSITE_2 = 248
|
|
||||||
0, // KEYCODE_TV_INPUT_COMPONENT_1 = 249
|
|
||||||
0, // KEYCODE_TV_INPUT_COMPONENT_2 = 250
|
|
||||||
0, // KEYCODE_TV_INPUT_VGA_1 = 251
|
|
||||||
0, // KEYCODE_TV_AUDIO_DESCRIPTION = 252
|
|
||||||
0, // KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP = 253
|
|
||||||
0, // KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN = 254
|
|
||||||
0, // KEYCODE_TV_ZOOM_MODE = 255
|
|
||||||
0, // KEYCODE_TV_CONTENTS_MENU = 256
|
|
||||||
0, // KEYCODE_TV_MEDIA_CONTEXT_MENU = 257
|
|
||||||
0, // KEYCODE_TV_TIMER_PROGRAMMING = 258
|
|
||||||
0, // KEYCODE_HELP = 259
|
|
||||||
0, // KEYCODE_NAVIGATE_PREVIOUS = 260
|
|
||||||
0, // KEYCODE_NAVIGATE_NEXT = 261
|
|
||||||
0, // KEYCODE_NAVIGATE_IN = 262
|
|
||||||
0, // KEYCODE_NAVIGATE_OUT = 263
|
|
||||||
0, // KEYCODE_STEM_PRIMARY = 264
|
|
||||||
0, // KEYCODE_STEM_1 = 265
|
|
||||||
0, // KEYCODE_STEM_2 = 266
|
|
||||||
0, // KEYCODE_STEM_3 = 267
|
|
||||||
0, // KEYCODE_DPAD_UP_LEFT = 268
|
|
||||||
0, // KEYCODE_DPAD_DOWN_LEFT = 269
|
|
||||||
0, // KEYCODE_DPAD_UP_RIGHT = 270
|
|
||||||
0, // KEYCODE_DPAD_DOWN_RIGHT = 271
|
|
||||||
0, // KEYCODE_MEDIA_SKIP_FORWARD = 272
|
|
||||||
0, // KEYCODE_MEDIA_SKIP_BACKWARD = 273
|
|
||||||
0, // KEYCODE_MEDIA_STEP_FORWARD = 274
|
|
||||||
0, // KEYCODE_MEDIA_STEP_BACKWARD = 275
|
|
||||||
0, // KEYCODE_SOFT_SLEEP = 276
|
|
||||||
0, // KEYCODE_CUT = 277
|
|
||||||
0, // KEYCODE_COPY = 278
|
|
||||||
0, // KEYCODE_PASTE = 279
|
|
||||||
0, // KEYCODE_SYSTEM_NAVIGATION_UP = 280
|
|
||||||
0, // KEYCODE_SYSTEM_NAVIGATION_DOWN = 281
|
|
||||||
0, // KEYCODE_SYSTEM_NAVIGATION_LEFT = 282
|
|
||||||
0, // KEYCODE_SYSTEM_NAVIGATION_RIGHT = 283
|
|
||||||
0, // KEYCODE_ALL_APPS = 284
|
|
||||||
0, // KEYCODE_REFRESH = 285
|
|
||||||
0, // KEYCODE_THUMBS_UP = 286
|
|
||||||
0, // KEYCODE_THUMBS_DOWN = 287
|
|
||||||
0 // KEYCODE_PROFILE_SWITCH = 288
|
|
||||||
|
|
||||||
*/
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,819 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.vnc
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements mapping between Unicode code-points and X KeySyms.
|
|
||||||
*/
|
|
||||||
object XKeySymUnicode {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns X KeySym for [uChar].
|
|
||||||
*/
|
|
||||||
fun getKeySymForUnicodeChar(uChar: Int): Int {
|
|
||||||
return if (uChar < 0x100) uChar else uChar + 0x01000000
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns legacy X KeySym for given [uChar].
|
|
||||||
* Returns 0 if no legacy KeySym is found.
|
|
||||||
*/
|
|
||||||
fun getLegacyKeySymForUnicodeChar(uChar: Int): Int {
|
|
||||||
|
|
||||||
// Check if character is outside of our map
|
|
||||||
if (uChar < UnicodeToLegacyKeysym[0] || uChar > UnicodeToLegacyKeysym[UnicodeToLegacyKeysym.size - 2])
|
|
||||||
return 0
|
|
||||||
|
|
||||||
// Binary Search on the 'first column' of map
|
|
||||||
var low = 0
|
|
||||||
var high = (UnicodeToLegacyKeysym.size / 2) - 1
|
|
||||||
|
|
||||||
while (low <= high) {
|
|
||||||
val mid = (low + high) / 2
|
|
||||||
val midChar = UnicodeToLegacyKeysym[mid * 2]
|
|
||||||
|
|
||||||
when {
|
|
||||||
uChar == midChar -> return UnicodeToLegacyKeysym[mid * 2 + 1]
|
|
||||||
uChar < midChar -> high = mid - 1
|
|
||||||
uChar > midChar -> low = mid + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This array maps Unicode code points to X KeySyms that were used before the
|
|
||||||
* introduction of Unicode KeySyms to X Windows System Protocol.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Source of the map: https://www.cl.cam.ac.uk/~mgk25/ucs/keysym2ucs.c
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This array is used as 2D array with stride = 2.
|
|
||||||
* First column is kept sorted to allow binary search.
|
|
||||||
*/
|
|
||||||
private val UnicodeToLegacyKeysym = intArrayOf(
|
|
||||||
// Unicode, X KeySym
|
|
||||||
0x100, 0x3C0,
|
|
||||||
0x101, 0x3E0,
|
|
||||||
0x102, 0x1C3,
|
|
||||||
0x103, 0x1E3,
|
|
||||||
0x104, 0x1A1,
|
|
||||||
0x105, 0x1B1,
|
|
||||||
0x106, 0x1C6,
|
|
||||||
0x107, 0x1E6,
|
|
||||||
0x108, 0x2C6,
|
|
||||||
0x109, 0x2E6,
|
|
||||||
0x10A, 0x2C5,
|
|
||||||
0x10B, 0x2E5,
|
|
||||||
0x10C, 0x1C8,
|
|
||||||
0x10D, 0x1E8,
|
|
||||||
0x10E, 0x1CF,
|
|
||||||
0x10F, 0x1EF,
|
|
||||||
0x110, 0x1D0,
|
|
||||||
0x111, 0x1F0,
|
|
||||||
0x112, 0x3AA,
|
|
||||||
0x113, 0x3BA,
|
|
||||||
0x116, 0x3CC,
|
|
||||||
0x117, 0x3EC,
|
|
||||||
0x118, 0x1CA,
|
|
||||||
0x119, 0x1EA,
|
|
||||||
0x11A, 0x1CC,
|
|
||||||
0x11B, 0x1EC,
|
|
||||||
0x11C, 0x2D8,
|
|
||||||
0x11D, 0x2F8,
|
|
||||||
0x11E, 0x2AB,
|
|
||||||
0x11F, 0x2BB,
|
|
||||||
0x120, 0x2D5,
|
|
||||||
0x121, 0x2F5,
|
|
||||||
0x122, 0x3AB,
|
|
||||||
0x123, 0x3BB,
|
|
||||||
0x124, 0x2A6,
|
|
||||||
0x125, 0x2B6,
|
|
||||||
0x126, 0x2A1,
|
|
||||||
0x127, 0x2B1,
|
|
||||||
0x128, 0x3A5,
|
|
||||||
0x129, 0x3B5,
|
|
||||||
0x12A, 0x3CF,
|
|
||||||
0x12B, 0x3EF,
|
|
||||||
0x12E, 0x3C7,
|
|
||||||
0x12F, 0x3E7,
|
|
||||||
0x130, 0x2A9,
|
|
||||||
0x131, 0x2B9,
|
|
||||||
0x134, 0x2AC,
|
|
||||||
0x135, 0x2BC,
|
|
||||||
0x136, 0x3D3,
|
|
||||||
0x137, 0x3F3,
|
|
||||||
0x138, 0x3A2,
|
|
||||||
0x139, 0x1C5,
|
|
||||||
0x13A, 0x1E5,
|
|
||||||
0x13B, 0x3A6,
|
|
||||||
0x13C, 0x3B6,
|
|
||||||
0x13D, 0x1A5,
|
|
||||||
0x13E, 0x1B5,
|
|
||||||
0x141, 0x1A3,
|
|
||||||
0x142, 0x1B3,
|
|
||||||
0x143, 0x1D1,
|
|
||||||
0x144, 0x1F1,
|
|
||||||
0x145, 0x3D1,
|
|
||||||
0x146, 0x3F1,
|
|
||||||
0x147, 0x1D2,
|
|
||||||
0x148, 0x1F2,
|
|
||||||
0x14A, 0x3BD,
|
|
||||||
0x14B, 0x3BF,
|
|
||||||
0x14C, 0x3D2,
|
|
||||||
0x14D, 0x3F2,
|
|
||||||
0x150, 0x1D5,
|
|
||||||
0x151, 0x1F5,
|
|
||||||
0x152, 0x13BC,
|
|
||||||
0x153, 0x13BD,
|
|
||||||
0x154, 0x1C0,
|
|
||||||
0x155, 0x1E0,
|
|
||||||
0x156, 0x3A3,
|
|
||||||
0x157, 0x3B3,
|
|
||||||
0x158, 0x1D8,
|
|
||||||
0x159, 0x1F8,
|
|
||||||
0x15A, 0x1A6,
|
|
||||||
0x15B, 0x1B6,
|
|
||||||
0x15C, 0x2DE,
|
|
||||||
0x15D, 0x2FE,
|
|
||||||
0x15E, 0x1AA,
|
|
||||||
0x15F, 0x1BA,
|
|
||||||
0x160, 0x1A9,
|
|
||||||
0x161, 0x1B9,
|
|
||||||
0x162, 0x1DE,
|
|
||||||
0x163, 0x1FE,
|
|
||||||
0x164, 0x1AB,
|
|
||||||
0x165, 0x1BB,
|
|
||||||
0x166, 0x3AC,
|
|
||||||
0x167, 0x3BC,
|
|
||||||
0x168, 0x3DD,
|
|
||||||
0x169, 0x3FD,
|
|
||||||
0x16A, 0x3DE,
|
|
||||||
0x16B, 0x3FE,
|
|
||||||
0x16C, 0x2DD,
|
|
||||||
0x16D, 0x2FD,
|
|
||||||
0x16E, 0x1D9,
|
|
||||||
0x16F, 0x1F9,
|
|
||||||
0x170, 0x1DB,
|
|
||||||
0x171, 0x1FB,
|
|
||||||
0x172, 0x3D9,
|
|
||||||
0x173, 0x3F9,
|
|
||||||
0x178, 0x13BE,
|
|
||||||
0x179, 0x1AC,
|
|
||||||
0x17A, 0x1BC,
|
|
||||||
0x17B, 0x1AF,
|
|
||||||
0x17C, 0x1BF,
|
|
||||||
0x17D, 0x1AE,
|
|
||||||
0x17E, 0x1BE,
|
|
||||||
0x192, 0x8F6,
|
|
||||||
0x2C7, 0x1B7,
|
|
||||||
0x2D8, 0x1A2,
|
|
||||||
0x2D9, 0x1FF,
|
|
||||||
0x2DB, 0x1B2,
|
|
||||||
0x2DD, 0x1BD,
|
|
||||||
0x385, 0x7AE,
|
|
||||||
0x386, 0x7A1,
|
|
||||||
0x388, 0x7A2,
|
|
||||||
0x389, 0x7A3,
|
|
||||||
0x38A, 0x7A4,
|
|
||||||
0x38C, 0x7A7,
|
|
||||||
0x38E, 0x7A8,
|
|
||||||
0x38F, 0x7AB,
|
|
||||||
0x390, 0x7B6,
|
|
||||||
0x391, 0x7C1,
|
|
||||||
0x392, 0x7C2,
|
|
||||||
0x393, 0x7C3,
|
|
||||||
0x394, 0x7C4,
|
|
||||||
0x395, 0x7C5,
|
|
||||||
0x396, 0x7C6,
|
|
||||||
0x397, 0x7C7,
|
|
||||||
0x398, 0x7C8,
|
|
||||||
0x399, 0x7C9,
|
|
||||||
0x39A, 0x7CA,
|
|
||||||
0x39B, 0x7CB,
|
|
||||||
0x39C, 0x7CC,
|
|
||||||
0x39D, 0x7CD,
|
|
||||||
0x39E, 0x7CE,
|
|
||||||
0x39F, 0x7CF,
|
|
||||||
0x3A0, 0x7D0,
|
|
||||||
0x3A1, 0x7D1,
|
|
||||||
0x3A3, 0x7D2,
|
|
||||||
0x3A4, 0x7D4,
|
|
||||||
0x3A5, 0x7D5,
|
|
||||||
0x3A6, 0x7D6,
|
|
||||||
0x3A7, 0x7D7,
|
|
||||||
0x3A8, 0x7D8,
|
|
||||||
0x3A9, 0x7D9,
|
|
||||||
0x3AA, 0x7A5,
|
|
||||||
0x3AB, 0x7A9,
|
|
||||||
0x3AC, 0x7B1,
|
|
||||||
0x3AD, 0x7B2,
|
|
||||||
0x3AE, 0x7B3,
|
|
||||||
0x3AF, 0x7B4,
|
|
||||||
0x3B0, 0x7BA,
|
|
||||||
0x3B1, 0x7E1,
|
|
||||||
0x3B2, 0x7E2,
|
|
||||||
0x3B3, 0x7E3,
|
|
||||||
0x3B4, 0x7E4,
|
|
||||||
0x3B5, 0x7E5,
|
|
||||||
0x3B6, 0x7E6,
|
|
||||||
0x3B7, 0x7E7,
|
|
||||||
0x3B8, 0x7E8,
|
|
||||||
0x3B9, 0x7E9,
|
|
||||||
0x3BA, 0x7EA,
|
|
||||||
0x3BB, 0x7EB,
|
|
||||||
0x3BC, 0x7EC,
|
|
||||||
0x3BD, 0x7ED,
|
|
||||||
0x3BE, 0x7EE,
|
|
||||||
0x3BF, 0x7EF,
|
|
||||||
0x3C0, 0x7F0,
|
|
||||||
0x3C1, 0x7F1,
|
|
||||||
0x3C2, 0x7F3,
|
|
||||||
0x3C3, 0x7F2,
|
|
||||||
0x3C4, 0x7F4,
|
|
||||||
0x3C5, 0x7F5,
|
|
||||||
0x3C6, 0x7F6,
|
|
||||||
0x3C7, 0x7F7,
|
|
||||||
0x3C8, 0x7F8,
|
|
||||||
0x3C9, 0x7F9,
|
|
||||||
0x3CA, 0x7B5,
|
|
||||||
0x3CB, 0x7B9,
|
|
||||||
0x3CC, 0x7B7,
|
|
||||||
0x3CD, 0x7B8,
|
|
||||||
0x3CE, 0x7BB,
|
|
||||||
0x401, 0x6B3,
|
|
||||||
0x402, 0x6B1,
|
|
||||||
0x403, 0x6B2,
|
|
||||||
0x404, 0x6B4,
|
|
||||||
0x405, 0x6B5,
|
|
||||||
0x406, 0x6B6,
|
|
||||||
0x407, 0x6B7,
|
|
||||||
0x408, 0x6B8,
|
|
||||||
0x409, 0x6B9,
|
|
||||||
0x40A, 0x6BA,
|
|
||||||
0x40B, 0x6BB,
|
|
||||||
0x40C, 0x6BC,
|
|
||||||
0x40E, 0x6BE,
|
|
||||||
0x40F, 0x6BF,
|
|
||||||
0x410, 0x6E1,
|
|
||||||
0x411, 0x6E2,
|
|
||||||
0x412, 0x6F7,
|
|
||||||
0x413, 0x6E7,
|
|
||||||
0x414, 0x6E4,
|
|
||||||
0x415, 0x6E5,
|
|
||||||
0x416, 0x6F6,
|
|
||||||
0x417, 0x6FA,
|
|
||||||
0x418, 0x6E9,
|
|
||||||
0x419, 0x6EA,
|
|
||||||
0x41A, 0x6EB,
|
|
||||||
0x41B, 0x6EC,
|
|
||||||
0x41C, 0x6ED,
|
|
||||||
0x41D, 0x6EE,
|
|
||||||
0x41E, 0x6EF,
|
|
||||||
0x41F, 0x6F0,
|
|
||||||
0x420, 0x6F2,
|
|
||||||
0x421, 0x6F3,
|
|
||||||
0x422, 0x6F4,
|
|
||||||
0x423, 0x6F5,
|
|
||||||
0x424, 0x6E6,
|
|
||||||
0x425, 0x6E8,
|
|
||||||
0x426, 0x6E3,
|
|
||||||
0x427, 0x6FE,
|
|
||||||
0x428, 0x6FB,
|
|
||||||
0x429, 0x6FD,
|
|
||||||
0x42A, 0x6FF,
|
|
||||||
0x42B, 0x6F9,
|
|
||||||
0x42C, 0x6F8,
|
|
||||||
0x42D, 0x6FC,
|
|
||||||
0x42E, 0x6E0,
|
|
||||||
0x42F, 0x6F1,
|
|
||||||
0x430, 0x6C1,
|
|
||||||
0x431, 0x6C2,
|
|
||||||
0x432, 0x6D7,
|
|
||||||
0x433, 0x6C7,
|
|
||||||
0x434, 0x6C4,
|
|
||||||
0x435, 0x6C5,
|
|
||||||
0x436, 0x6D6,
|
|
||||||
0x437, 0x6DA,
|
|
||||||
0x438, 0x6C9,
|
|
||||||
0x439, 0x6CA,
|
|
||||||
0x43A, 0x6CB,
|
|
||||||
0x43B, 0x6CC,
|
|
||||||
0x43C, 0x6CD,
|
|
||||||
0x43D, 0x6CE,
|
|
||||||
0x43E, 0x6CF,
|
|
||||||
0x43F, 0x6D0,
|
|
||||||
0x440, 0x6D2,
|
|
||||||
0x441, 0x6D3,
|
|
||||||
0x442, 0x6D4,
|
|
||||||
0x443, 0x6D5,
|
|
||||||
0x444, 0x6C6,
|
|
||||||
0x445, 0x6C8,
|
|
||||||
0x446, 0x6C3,
|
|
||||||
0x447, 0x6DE,
|
|
||||||
0x448, 0x6DB,
|
|
||||||
0x449, 0x6DD,
|
|
||||||
0x44A, 0x6DF,
|
|
||||||
0x44B, 0x6D9,
|
|
||||||
0x44C, 0x6D8,
|
|
||||||
0x44D, 0x6DC,
|
|
||||||
0x44E, 0x6C0,
|
|
||||||
0x44F, 0x6D1,
|
|
||||||
0x451, 0x6A3,
|
|
||||||
0x452, 0x6A1,
|
|
||||||
0x453, 0x6A2,
|
|
||||||
0x454, 0x6A4,
|
|
||||||
0x455, 0x6A5,
|
|
||||||
0x456, 0x6A6,
|
|
||||||
0x457, 0x6A7,
|
|
||||||
0x458, 0x6A8,
|
|
||||||
0x459, 0x6A9,
|
|
||||||
0x45A, 0x6AA,
|
|
||||||
0x45B, 0x6AB,
|
|
||||||
0x45C, 0x6AC,
|
|
||||||
0x45E, 0x6AE,
|
|
||||||
0x45F, 0x6AF,
|
|
||||||
0x5D0, 0xCE0,
|
|
||||||
0x5D1, 0xCE1,
|
|
||||||
0x5D2, 0xCE2,
|
|
||||||
0x5D3, 0xCE3,
|
|
||||||
0x5D4, 0xCE4,
|
|
||||||
0x5D5, 0xCE5,
|
|
||||||
0x5D6, 0xCE6,
|
|
||||||
0x5D7, 0xCE7,
|
|
||||||
0x5D8, 0xCE8,
|
|
||||||
0x5D9, 0xCE9,
|
|
||||||
0x5DA, 0xCEA,
|
|
||||||
0x5DB, 0xCEB,
|
|
||||||
0x5DC, 0xCEC,
|
|
||||||
0x5DD, 0xCED,
|
|
||||||
0x5DE, 0xCEE,
|
|
||||||
0x5DF, 0xCEF,
|
|
||||||
0x5E0, 0xCF0,
|
|
||||||
0x5E1, 0xCF1,
|
|
||||||
0x5E2, 0xCF2,
|
|
||||||
0x5E3, 0xCF3,
|
|
||||||
0x5E4, 0xCF4,
|
|
||||||
0x5E5, 0xCF5,
|
|
||||||
0x5E6, 0xCF6,
|
|
||||||
0x5E7, 0xCF7,
|
|
||||||
0x5E8, 0xCF8,
|
|
||||||
0x5E9, 0xCF9,
|
|
||||||
0x5EA, 0xCFA,
|
|
||||||
0x60C, 0x5AC,
|
|
||||||
0x61B, 0x5BB,
|
|
||||||
0x61F, 0x5BF,
|
|
||||||
0x621, 0x5C1,
|
|
||||||
0x622, 0x5C2,
|
|
||||||
0x623, 0x5C3,
|
|
||||||
0x624, 0x5C4,
|
|
||||||
0x625, 0x5C5,
|
|
||||||
0x626, 0x5C6,
|
|
||||||
0x627, 0x5C7,
|
|
||||||
0x628, 0x5C8,
|
|
||||||
0x629, 0x5C9,
|
|
||||||
0x62A, 0x5CA,
|
|
||||||
0x62B, 0x5CB,
|
|
||||||
0x62C, 0x5CC,
|
|
||||||
0x62D, 0x5CD,
|
|
||||||
0x62E, 0x5CE,
|
|
||||||
0x62F, 0x5CF,
|
|
||||||
0x630, 0x5D0,
|
|
||||||
0x631, 0x5D1,
|
|
||||||
0x632, 0x5D2,
|
|
||||||
0x633, 0x5D3,
|
|
||||||
0x634, 0x5D4,
|
|
||||||
0x635, 0x5D5,
|
|
||||||
0x636, 0x5D6,
|
|
||||||
0x637, 0x5D7,
|
|
||||||
0x638, 0x5D8,
|
|
||||||
0x639, 0x5D9,
|
|
||||||
0x63A, 0x5DA,
|
|
||||||
0x640, 0x5E0,
|
|
||||||
0x641, 0x5E1,
|
|
||||||
0x642, 0x5E2,
|
|
||||||
0x643, 0x5E3,
|
|
||||||
0x644, 0x5E4,
|
|
||||||
0x645, 0x5E5,
|
|
||||||
0x646, 0x5E6,
|
|
||||||
0x647, 0x5E7,
|
|
||||||
0x648, 0x5E8,
|
|
||||||
0x649, 0x5E9,
|
|
||||||
0x64A, 0x5EA,
|
|
||||||
0x64B, 0x5EB,
|
|
||||||
0x64C, 0x5EC,
|
|
||||||
0x64D, 0x5ED,
|
|
||||||
0x64E, 0x5EE,
|
|
||||||
0x64F, 0x5EF,
|
|
||||||
0x650, 0x5F0,
|
|
||||||
0x651, 0x5F1,
|
|
||||||
0x652, 0x5F2,
|
|
||||||
0xE01, 0xDA1,
|
|
||||||
0xE02, 0xDA2,
|
|
||||||
0xE03, 0xDA3,
|
|
||||||
0xE04, 0xDA4,
|
|
||||||
0xE05, 0xDA5,
|
|
||||||
0xE06, 0xDA6,
|
|
||||||
0xE07, 0xDA7,
|
|
||||||
0xE08, 0xDA8,
|
|
||||||
0xE09, 0xDA9,
|
|
||||||
0xE0A, 0xDAA,
|
|
||||||
0xE0B, 0xDAB,
|
|
||||||
0xE0C, 0xDAC,
|
|
||||||
0xE0D, 0xDAD,
|
|
||||||
0xE0E, 0xDAE,
|
|
||||||
0xE0F, 0xDAF,
|
|
||||||
0xE10, 0xDB0,
|
|
||||||
0xE11, 0xDB1,
|
|
||||||
0xE12, 0xDB2,
|
|
||||||
0xE13, 0xDB3,
|
|
||||||
0xE14, 0xDB4,
|
|
||||||
0xE15, 0xDB5,
|
|
||||||
0xE16, 0xDB6,
|
|
||||||
0xE17, 0xDB7,
|
|
||||||
0xE18, 0xDB8,
|
|
||||||
0xE19, 0xDB9,
|
|
||||||
0xE1A, 0xDBA,
|
|
||||||
0xE1B, 0xDBB,
|
|
||||||
0xE1C, 0xDBC,
|
|
||||||
0xE1D, 0xDBD,
|
|
||||||
0xE1E, 0xDBE,
|
|
||||||
0xE1F, 0xDBF,
|
|
||||||
0xE20, 0xDC0,
|
|
||||||
0xE21, 0xDC1,
|
|
||||||
0xE22, 0xDC2,
|
|
||||||
0xE23, 0xDC3,
|
|
||||||
0xE24, 0xDC4,
|
|
||||||
0xE25, 0xDC5,
|
|
||||||
0xE26, 0xDC6,
|
|
||||||
0xE27, 0xDC7,
|
|
||||||
0xE28, 0xDC8,
|
|
||||||
0xE29, 0xDC9,
|
|
||||||
0xE2A, 0xDCA,
|
|
||||||
0xE2B, 0xDCB,
|
|
||||||
0xE2C, 0xDCC,
|
|
||||||
0xE2D, 0xDCD,
|
|
||||||
0xE2E, 0xDCE,
|
|
||||||
0xE2F, 0xDCF,
|
|
||||||
0xE30, 0xDD0,
|
|
||||||
0xE31, 0xDD1,
|
|
||||||
0xE32, 0xDD2,
|
|
||||||
0xE33, 0xDD3,
|
|
||||||
0xE34, 0xDD4,
|
|
||||||
0xE35, 0xDD5,
|
|
||||||
0xE36, 0xDD6,
|
|
||||||
0xE37, 0xDD7,
|
|
||||||
0xE38, 0xDD8,
|
|
||||||
0xE39, 0xDD9,
|
|
||||||
0xE3A, 0xDDA,
|
|
||||||
0xE3F, 0xDDF,
|
|
||||||
0xE40, 0xDE0,
|
|
||||||
0xE41, 0xDE1,
|
|
||||||
0xE42, 0xDE2,
|
|
||||||
0xE43, 0xDE3,
|
|
||||||
0xE44, 0xDE4,
|
|
||||||
0xE45, 0xDE5,
|
|
||||||
0xE46, 0xDE6,
|
|
||||||
0xE47, 0xDE7,
|
|
||||||
0xE48, 0xDE8,
|
|
||||||
0xE49, 0xDE9,
|
|
||||||
0xE4A, 0xDEA,
|
|
||||||
0xE4B, 0xDEB,
|
|
||||||
0xE4C, 0xDEC,
|
|
||||||
0xE4D, 0xDED,
|
|
||||||
0xE50, 0xDF0,
|
|
||||||
0xE51, 0xDF1,
|
|
||||||
0xE52, 0xDF2,
|
|
||||||
0xE53, 0xDF3,
|
|
||||||
0xE54, 0xDF4,
|
|
||||||
0xE55, 0xDF5,
|
|
||||||
0xE56, 0xDF6,
|
|
||||||
0xE57, 0xDF7,
|
|
||||||
0xE58, 0xDF8,
|
|
||||||
0xE59, 0xDF9,
|
|
||||||
0x11A8, 0xED4,
|
|
||||||
0x11A9, 0xED5,
|
|
||||||
0x11AA, 0xED6,
|
|
||||||
0x11AB, 0xED7,
|
|
||||||
0x11AC, 0xED8,
|
|
||||||
0x11AD, 0xED9,
|
|
||||||
0x11AE, 0xEDA,
|
|
||||||
0x11AF, 0xEDB,
|
|
||||||
0x11B0, 0xEDC,
|
|
||||||
0x11B1, 0xEDD,
|
|
||||||
0x11B2, 0xEDE,
|
|
||||||
0x11B3, 0xEDF,
|
|
||||||
0x11B4, 0xEE0,
|
|
||||||
0x11B5, 0xEE1,
|
|
||||||
0x11B6, 0xEE2,
|
|
||||||
0x11B7, 0xEE3,
|
|
||||||
0x11B8, 0xEE4,
|
|
||||||
0x11B9, 0xEE5,
|
|
||||||
0x11BA, 0xEE6,
|
|
||||||
0x11BB, 0xEE7,
|
|
||||||
0x11BC, 0xEE8,
|
|
||||||
0x11BD, 0xEE9,
|
|
||||||
0x11BE, 0xEEA,
|
|
||||||
0x11BF, 0xEEB,
|
|
||||||
0x11C0, 0xEEC,
|
|
||||||
0x11C1, 0xEED,
|
|
||||||
0x11C2, 0xEEE,
|
|
||||||
0x11EB, 0xEF8,
|
|
||||||
0x11F0, 0xEF9,
|
|
||||||
0x11F9, 0xEFA,
|
|
||||||
0x2002, 0xAA2,
|
|
||||||
0x2003, 0xAA1,
|
|
||||||
0x2004, 0xAA3,
|
|
||||||
0x2005, 0xAA4,
|
|
||||||
0x2007, 0xAA5,
|
|
||||||
0x2008, 0xAA6,
|
|
||||||
0x2009, 0xAA7,
|
|
||||||
0x200A, 0xAA8,
|
|
||||||
0x2012, 0xABB,
|
|
||||||
0x2013, 0xAAA,
|
|
||||||
0x2014, 0xAA9,
|
|
||||||
0x2015, 0x7AF,
|
|
||||||
0x2017, 0xCDF,
|
|
||||||
0x2018, 0xAD0,
|
|
||||||
0x2019, 0xAD1,
|
|
||||||
0x201A, 0xAFD,
|
|
||||||
0x201C, 0xAD2,
|
|
||||||
0x201D, 0xAD3,
|
|
||||||
0x201E, 0xAFE,
|
|
||||||
0x2020, 0xAF1,
|
|
||||||
0x2021, 0xAF2,
|
|
||||||
0x2022, 0xAE6,
|
|
||||||
0x2025, 0xAAF,
|
|
||||||
0x2026, 0xAAE,
|
|
||||||
0x2032, 0xAD6,
|
|
||||||
0x2033, 0xAD7,
|
|
||||||
0x2038, 0xAFC,
|
|
||||||
0x203E, 0x47E,
|
|
||||||
0x20A9, 0xEFF,
|
|
||||||
0x20AC, 0x20AC,
|
|
||||||
0x20AC, 0x13A4,
|
|
||||||
0x2105, 0xAB8,
|
|
||||||
0x2116, 0x6B0,
|
|
||||||
0x2117, 0xAFB,
|
|
||||||
0x211E, 0xAD4,
|
|
||||||
0x2122, 0xAC9,
|
|
||||||
0x2153, 0xAB0,
|
|
||||||
0x2154, 0xAB1,
|
|
||||||
0x2155, 0xAB2,
|
|
||||||
0x2156, 0xAB3,
|
|
||||||
0x2157, 0xAB4,
|
|
||||||
0x2158, 0xAB5,
|
|
||||||
0x2159, 0xAB6,
|
|
||||||
0x215A, 0xAB7,
|
|
||||||
0x215B, 0xAC3,
|
|
||||||
0x215C, 0xAC4,
|
|
||||||
0x215D, 0xAC5,
|
|
||||||
0x215E, 0xAC6,
|
|
||||||
0x2190, 0x8FB,
|
|
||||||
0x2191, 0x8FC,
|
|
||||||
0x2192, 0x8FD,
|
|
||||||
0x2193, 0x8FE,
|
|
||||||
0x21D2, 0x8CE,
|
|
||||||
0x21D4, 0x8CD,
|
|
||||||
0x2202, 0x8EF,
|
|
||||||
0x2207, 0x8C5,
|
|
||||||
0x2218, 0xBCA,
|
|
||||||
0x221A, 0x8D6,
|
|
||||||
0x221D, 0x8C1,
|
|
||||||
0x221E, 0x8C2,
|
|
||||||
0x2227, 0x8DE,
|
|
||||||
0x2227, 0xBA9,
|
|
||||||
0x2228, 0x8DF,
|
|
||||||
0x2228, 0xBA8,
|
|
||||||
0x2229, 0x8DC,
|
|
||||||
0x2229, 0xBC3,
|
|
||||||
0x222A, 0xBD6,
|
|
||||||
0x222A, 0x8DD,
|
|
||||||
0x222B, 0x8BF,
|
|
||||||
0x2234, 0x8C0,
|
|
||||||
0x223C, 0x8C8,
|
|
||||||
0x2243, 0x8C9,
|
|
||||||
0x2260, 0x8BD,
|
|
||||||
0x2261, 0x8CF,
|
|
||||||
0x2264, 0x8BC,
|
|
||||||
0x2265, 0x8BE,
|
|
||||||
0x2282, 0xBDA,
|
|
||||||
0x2282, 0x8DA,
|
|
||||||
0x2283, 0xBD8,
|
|
||||||
0x2283, 0x8DB,
|
|
||||||
0x22A2, 0xBDC,
|
|
||||||
0x22A3, 0xBFC,
|
|
||||||
0x22A4, 0xBCE,
|
|
||||||
0x22A5, 0xBC2,
|
|
||||||
0x2308, 0xBD3,
|
|
||||||
0x230A, 0xBC4,
|
|
||||||
0x2315, 0xAFA,
|
|
||||||
0x2320, 0x8A4,
|
|
||||||
0x2321, 0x8A5,
|
|
||||||
0x2329, 0xABC,
|
|
||||||
0x232A, 0xABE,
|
|
||||||
0x2395, 0xBCC,
|
|
||||||
0x239B, 0x8AB,
|
|
||||||
0x239D, 0x8AC,
|
|
||||||
0x239E, 0x8AD,
|
|
||||||
0x23A0, 0x8AE,
|
|
||||||
0x23A1, 0x8A7,
|
|
||||||
0x23A3, 0x8A8,
|
|
||||||
0x23A4, 0x8A9,
|
|
||||||
0x23A6, 0x8AA,
|
|
||||||
0x23A8, 0x8AF,
|
|
||||||
0x23AC, 0x8B0,
|
|
||||||
0x23B7, 0x8A1,
|
|
||||||
0x23BA, 0x9EF,
|
|
||||||
0x23BB, 0x9F0,
|
|
||||||
0x23BC, 0x9F2,
|
|
||||||
0x23BD, 0x9F3,
|
|
||||||
0x2409, 0x9E2,
|
|
||||||
0x240A, 0x9E5,
|
|
||||||
0x240B, 0x9E9,
|
|
||||||
0x240C, 0x9E3,
|
|
||||||
0x240D, 0x9E4,
|
|
||||||
0x2424, 0x9E8,
|
|
||||||
0x2500, 0x9F1,
|
|
||||||
0x2500, 0x8A3,
|
|
||||||
0x2502, 0x8A6,
|
|
||||||
0x2502, 0x9F8,
|
|
||||||
0x250C, 0x9EC,
|
|
||||||
0x250C, 0x8A2,
|
|
||||||
0x2510, 0x9EB,
|
|
||||||
0x2514, 0x9ED,
|
|
||||||
0x2518, 0x9EA,
|
|
||||||
0x251C, 0x9F4,
|
|
||||||
0x2524, 0x9F5,
|
|
||||||
0x252C, 0x9F7,
|
|
||||||
0x2534, 0x9F6,
|
|
||||||
0x253C, 0x9EE,
|
|
||||||
0x2592, 0x9E1,
|
|
||||||
0x25AA, 0xAE7,
|
|
||||||
0x25AB, 0xAE1,
|
|
||||||
0x25AC, 0xADB,
|
|
||||||
0x25AD, 0xAE2,
|
|
||||||
0x25AE, 0xADF,
|
|
||||||
0x25AF, 0xACF,
|
|
||||||
0x25B2, 0xAE8,
|
|
||||||
0x25B3, 0xAE3,
|
|
||||||
0x25B6, 0xADD,
|
|
||||||
0x25B7, 0xACD,
|
|
||||||
0x25BC, 0xAE9,
|
|
||||||
0x25BD, 0xAE4,
|
|
||||||
0x25C0, 0xADC,
|
|
||||||
0x25C1, 0xACC,
|
|
||||||
0x25C6, 0x9E0,
|
|
||||||
0x25CB, 0xBCF,
|
|
||||||
0x25CB, 0xACE,
|
|
||||||
0x25CF, 0xADE,
|
|
||||||
0x25E6, 0xAE0,
|
|
||||||
0x2606, 0xAE5,
|
|
||||||
0x260E, 0xAF9,
|
|
||||||
0x2613, 0xACA,
|
|
||||||
0x261C, 0xAEA,
|
|
||||||
0x261E, 0xAEB,
|
|
||||||
0x2640, 0xAF8,
|
|
||||||
0x2642, 0xAF7,
|
|
||||||
0x2663, 0xAEC,
|
|
||||||
0x2665, 0xAEE,
|
|
||||||
0x2666, 0xAED,
|
|
||||||
0x266D, 0xAF6,
|
|
||||||
0x266F, 0xAF5,
|
|
||||||
0x2713, 0xAF3,
|
|
||||||
0x2717, 0xAF4,
|
|
||||||
0x271D, 0xAD9,
|
|
||||||
0x2720, 0xAF0,
|
|
||||||
0x3001, 0x4A4,
|
|
||||||
0x3002, 0x4A1,
|
|
||||||
0x300C, 0x4A2,
|
|
||||||
0x300D, 0x4A3,
|
|
||||||
0x309B, 0x4DE,
|
|
||||||
0x309C, 0x4DF,
|
|
||||||
0x30A1, 0x4A7,
|
|
||||||
0x30A2, 0x4B1,
|
|
||||||
0x30A3, 0x4A8,
|
|
||||||
0x30A4, 0x4B2,
|
|
||||||
0x30A5, 0x4A9,
|
|
||||||
0x30A6, 0x4B3,
|
|
||||||
0x30A7, 0x4AA,
|
|
||||||
0x30A8, 0x4B4,
|
|
||||||
0x30A9, 0x4AB,
|
|
||||||
0x30AA, 0x4B5,
|
|
||||||
0x30AB, 0x4B6,
|
|
||||||
0x30AD, 0x4B7,
|
|
||||||
0x30AF, 0x4B8,
|
|
||||||
0x30B1, 0x4B9,
|
|
||||||
0x30B3, 0x4BA,
|
|
||||||
0x30B5, 0x4BB,
|
|
||||||
0x30B7, 0x4BC,
|
|
||||||
0x30B9, 0x4BD,
|
|
||||||
0x30BB, 0x4BE,
|
|
||||||
0x30BD, 0x4BF,
|
|
||||||
0x30BF, 0x4C0,
|
|
||||||
0x30C1, 0x4C1,
|
|
||||||
0x30C3, 0x4AF,
|
|
||||||
0x30C4, 0x4C2,
|
|
||||||
0x30C6, 0x4C3,
|
|
||||||
0x30C8, 0x4C4,
|
|
||||||
0x30CA, 0x4C5,
|
|
||||||
0x30CB, 0x4C6,
|
|
||||||
0x30CC, 0x4C7,
|
|
||||||
0x30CD, 0x4C8,
|
|
||||||
0x30CE, 0x4C9,
|
|
||||||
0x30CF, 0x4CA,
|
|
||||||
0x30D2, 0x4CB,
|
|
||||||
0x30D5, 0x4CC,
|
|
||||||
0x30D8, 0x4CD,
|
|
||||||
0x30DB, 0x4CE,
|
|
||||||
0x30DE, 0x4CF,
|
|
||||||
0x30DF, 0x4D0,
|
|
||||||
0x30E0, 0x4D1,
|
|
||||||
0x30E1, 0x4D2,
|
|
||||||
0x30E2, 0x4D3,
|
|
||||||
0x30E3, 0x4AC,
|
|
||||||
0x30E4, 0x4D4,
|
|
||||||
0x30E5, 0x4AD,
|
|
||||||
0x30E6, 0x4D5,
|
|
||||||
0x30E7, 0x4AE,
|
|
||||||
0x30E8, 0x4D6,
|
|
||||||
0x30E9, 0x4D7,
|
|
||||||
0x30EA, 0x4D8,
|
|
||||||
0x30EB, 0x4D9,
|
|
||||||
0x30EC, 0x4DA,
|
|
||||||
0x30ED, 0x4DB,
|
|
||||||
0x30EF, 0x4DC,
|
|
||||||
0x30F2, 0x4A6,
|
|
||||||
0x30F3, 0x4DD,
|
|
||||||
0x30FB, 0x4A5,
|
|
||||||
0x30FC, 0x4B0,
|
|
||||||
0x3131, 0xEA1,
|
|
||||||
0x3132, 0xEA2,
|
|
||||||
0x3133, 0xEA3,
|
|
||||||
0x3134, 0xEA4,
|
|
||||||
0x3135, 0xEA5,
|
|
||||||
0x3136, 0xEA6,
|
|
||||||
0x3137, 0xEA7,
|
|
||||||
0x3138, 0xEA8,
|
|
||||||
0x3139, 0xEA9,
|
|
||||||
0x313A, 0xEAA,
|
|
||||||
0x313B, 0xEAB,
|
|
||||||
0x313C, 0xEAC,
|
|
||||||
0x313D, 0xEAD,
|
|
||||||
0x313E, 0xEAE,
|
|
||||||
0x313F, 0xEAF,
|
|
||||||
0x3140, 0xEB0,
|
|
||||||
0x3141, 0xEB1,
|
|
||||||
0x3142, 0xEB2,
|
|
||||||
0x3143, 0xEB3,
|
|
||||||
0x3144, 0xEB4,
|
|
||||||
0x3145, 0xEB5,
|
|
||||||
0x3146, 0xEB6,
|
|
||||||
0x3147, 0xEB7,
|
|
||||||
0x3148, 0xEB8,
|
|
||||||
0x3149, 0xEB9,
|
|
||||||
0x314A, 0xEBA,
|
|
||||||
0x314B, 0xEBB,
|
|
||||||
0x314C, 0xEBC,
|
|
||||||
0x314D, 0xEBD,
|
|
||||||
0x314E, 0xEBE,
|
|
||||||
0x314F, 0xEBF,
|
|
||||||
0x3150, 0xEC0,
|
|
||||||
0x3151, 0xEC1,
|
|
||||||
0x3152, 0xEC2,
|
|
||||||
0x3153, 0xEC3,
|
|
||||||
0x3154, 0xEC4,
|
|
||||||
0x3155, 0xEC5,
|
|
||||||
0x3156, 0xEC6,
|
|
||||||
0x3157, 0xEC7,
|
|
||||||
0x3158, 0xEC8,
|
|
||||||
0x3159, 0xEC9,
|
|
||||||
0x315A, 0xECA,
|
|
||||||
0x315B, 0xECB,
|
|
||||||
0x315C, 0xECC,
|
|
||||||
0x315D, 0xECD,
|
|
||||||
0x315E, 0xECE,
|
|
||||||
0x315F, 0xECF,
|
|
||||||
0x3160, 0xED0,
|
|
||||||
0x3161, 0xED1,
|
|
||||||
0x3162, 0xED2,
|
|
||||||
0x3163, 0xED3,
|
|
||||||
0x316D, 0xEEF,
|
|
||||||
0x3171, 0xEF0,
|
|
||||||
0x3178, 0xEF1,
|
|
||||||
0x317F, 0xEF2,
|
|
||||||
0x3181, 0xEF3,
|
|
||||||
0x3184, 0xEF4,
|
|
||||||
0x3186, 0xEF5,
|
|
||||||
0x318D, 0xEF6,
|
|
||||||
0x318E, 0xEF7
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,573 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2023 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.vnc
|
|
||||||
|
|
||||||
/**
|
|
||||||
* From [1]:
|
|
||||||
* "An XT keycode is an XT make scancode sequence encoded to fit in a single U32 quantity.
|
|
||||||
* Single byte XT scancodes with a byte value less than 0x7f are encoded as is.
|
|
||||||
* 2-byte XT scancodes whose first byte is 0xe0 and second byte is less than 0x7f
|
|
||||||
* are encoded with the high bit of the first byte set."
|
|
||||||
*
|
|
||||||
* [1] https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#qemu-extended-key-event-message
|
|
||||||
*/
|
|
||||||
object XTKeyCode {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps KeyEvent scancodes to XT keycode.
|
|
||||||
*
|
|
||||||
* KeyEvent scancodes are simply Linux kernel keycodes. Although Android doesn't
|
|
||||||
* explicitly states this, it can confirmed by looking through Android source code.
|
|
||||||
* In any case, all devices tested so far returns Linux keycodes.
|
|
||||||
*
|
|
||||||
* Returns 0 if no mapping exist.
|
|
||||||
*/
|
|
||||||
fun fromAndroidScancode(scancode: Int) = LinuxToQnum.getOrNull(scancode) ?: 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mapping table is generated using output from the following command:
|
|
||||||
*
|
|
||||||
* keymap-gen code-map --lang stdc++ keymaps.csv linux qnum
|
|
||||||
*
|
|
||||||
* qnum refers to XT keycode described above.
|
|
||||||
* Source: https://github.com/qemu/keycodemapdb
|
|
||||||
*/
|
|
||||||
private val LinuxToQnum = intArrayOf(
|
|
||||||
0, /* linux:0 (KEY_RESERVED) -> linux:0 (KEY_RESERVED) -> qnum:None */
|
|
||||||
0x1, /* linux:1 (KEY_ESC) -> linux:1 (KEY_ESC) -> qnum:1 */
|
|
||||||
0x2, /* linux:2 (KEY_1) -> linux:2 (KEY_1) -> qnum:2 */
|
|
||||||
0x3, /* linux:3 (KEY_2) -> linux:3 (KEY_2) -> qnum:3 */
|
|
||||||
0x4, /* linux:4 (KEY_3) -> linux:4 (KEY_3) -> qnum:4 */
|
|
||||||
0x5, /* linux:5 (KEY_4) -> linux:5 (KEY_4) -> qnum:5 */
|
|
||||||
0x6, /* linux:6 (KEY_5) -> linux:6 (KEY_5) -> qnum:6 */
|
|
||||||
0x7, /* linux:7 (KEY_6) -> linux:7 (KEY_6) -> qnum:7 */
|
|
||||||
0x8, /* linux:8 (KEY_7) -> linux:8 (KEY_7) -> qnum:8 */
|
|
||||||
0x9, /* linux:9 (KEY_8) -> linux:9 (KEY_8) -> qnum:9 */
|
|
||||||
0xa, /* linux:10 (KEY_9) -> linux:10 (KEY_9) -> qnum:10 */
|
|
||||||
0xb, /* linux:11 (KEY_0) -> linux:11 (KEY_0) -> qnum:11 */
|
|
||||||
0xc, /* linux:12 (KEY_MINUS) -> linux:12 (KEY_MINUS) -> qnum:12 */
|
|
||||||
0xd, /* linux:13 (KEY_EQUAL) -> linux:13 (KEY_EQUAL) -> qnum:13 */
|
|
||||||
0xe, /* linux:14 (KEY_BACKSPACE) -> linux:14 (KEY_BACKSPACE) -> qnum:14 */
|
|
||||||
0xf, /* linux:15 (KEY_TAB) -> linux:15 (KEY_TAB) -> qnum:15 */
|
|
||||||
0x10, /* linux:16 (KEY_Q) -> linux:16 (KEY_Q) -> qnum:16 */
|
|
||||||
0x11, /* linux:17 (KEY_W) -> linux:17 (KEY_W) -> qnum:17 */
|
|
||||||
0x12, /* linux:18 (KEY_E) -> linux:18 (KEY_E) -> qnum:18 */
|
|
||||||
0x13, /* linux:19 (KEY_R) -> linux:19 (KEY_R) -> qnum:19 */
|
|
||||||
0x14, /* linux:20 (KEY_T) -> linux:20 (KEY_T) -> qnum:20 */
|
|
||||||
0x15, /* linux:21 (KEY_Y) -> linux:21 (KEY_Y) -> qnum:21 */
|
|
||||||
0x16, /* linux:22 (KEY_U) -> linux:22 (KEY_U) -> qnum:22 */
|
|
||||||
0x17, /* linux:23 (KEY_I) -> linux:23 (KEY_I) -> qnum:23 */
|
|
||||||
0x18, /* linux:24 (KEY_O) -> linux:24 (KEY_O) -> qnum:24 */
|
|
||||||
0x19, /* linux:25 (KEY_P) -> linux:25 (KEY_P) -> qnum:25 */
|
|
||||||
0x1a, /* linux:26 (KEY_LEFTBRACE) -> linux:26 (KEY_LEFTBRACE) -> qnum:26 */
|
|
||||||
0x1b, /* linux:27 (KEY_RIGHTBRACE) -> linux:27 (KEY_RIGHTBRACE) -> qnum:27 */
|
|
||||||
0x1c, /* linux:28 (KEY_ENTER) -> linux:28 (KEY_ENTER) -> qnum:28 */
|
|
||||||
0x1d, /* linux:29 (KEY_LEFTCTRL) -> linux:29 (KEY_LEFTCTRL) -> qnum:29 */
|
|
||||||
0x1e, /* linux:30 (KEY_A) -> linux:30 (KEY_A) -> qnum:30 */
|
|
||||||
0x1f, /* linux:31 (KEY_S) -> linux:31 (KEY_S) -> qnum:31 */
|
|
||||||
0x20, /* linux:32 (KEY_D) -> linux:32 (KEY_D) -> qnum:32 */
|
|
||||||
0x21, /* linux:33 (KEY_F) -> linux:33 (KEY_F) -> qnum:33 */
|
|
||||||
0x22, /* linux:34 (KEY_G) -> linux:34 (KEY_G) -> qnum:34 */
|
|
||||||
0x23, /* linux:35 (KEY_H) -> linux:35 (KEY_H) -> qnum:35 */
|
|
||||||
0x24, /* linux:36 (KEY_J) -> linux:36 (KEY_J) -> qnum:36 */
|
|
||||||
0x25, /* linux:37 (KEY_K) -> linux:37 (KEY_K) -> qnum:37 */
|
|
||||||
0x26, /* linux:38 (KEY_L) -> linux:38 (KEY_L) -> qnum:38 */
|
|
||||||
0x27, /* linux:39 (KEY_SEMICOLON) -> linux:39 (KEY_SEMICOLON) -> qnum:39 */
|
|
||||||
0x28, /* linux:40 (KEY_APOSTROPHE) -> linux:40 (KEY_APOSTROPHE) -> qnum:40 */
|
|
||||||
0x29, /* linux:41 (KEY_GRAVE) -> linux:41 (KEY_GRAVE) -> qnum:41 */
|
|
||||||
0x2a, /* linux:42 (KEY_LEFTSHIFT) -> linux:42 (KEY_LEFTSHIFT) -> qnum:42 */
|
|
||||||
0x2b, /* linux:43 (KEY_BACKSLASH) -> linux:43 (KEY_BACKSLASH) -> qnum:43 */
|
|
||||||
0x2c, /* linux:44 (KEY_Z) -> linux:44 (KEY_Z) -> qnum:44 */
|
|
||||||
0x2d, /* linux:45 (KEY_X) -> linux:45 (KEY_X) -> qnum:45 */
|
|
||||||
0x2e, /* linux:46 (KEY_C) -> linux:46 (KEY_C) -> qnum:46 */
|
|
||||||
0x2f, /* linux:47 (KEY_V) -> linux:47 (KEY_V) -> qnum:47 */
|
|
||||||
0x30, /* linux:48 (KEY_B) -> linux:48 (KEY_B) -> qnum:48 */
|
|
||||||
0x31, /* linux:49 (KEY_N) -> linux:49 (KEY_N) -> qnum:49 */
|
|
||||||
0x32, /* linux:50 (KEY_M) -> linux:50 (KEY_M) -> qnum:50 */
|
|
||||||
0x33, /* linux:51 (KEY_COMMA) -> linux:51 (KEY_COMMA) -> qnum:51 */
|
|
||||||
0x34, /* linux:52 (KEY_DOT) -> linux:52 (KEY_DOT) -> qnum:52 */
|
|
||||||
0x35, /* linux:53 (KEY_SLASH) -> linux:53 (KEY_SLASH) -> qnum:53 */
|
|
||||||
0x36, /* linux:54 (KEY_RIGHTSHIFT) -> linux:54 (KEY_RIGHTSHIFT) -> qnum:54 */
|
|
||||||
0x37, /* linux:55 (KEY_KPASTERISK) -> linux:55 (KEY_KPASTERISK) -> qnum:55 */
|
|
||||||
0x38, /* linux:56 (KEY_LEFTALT) -> linux:56 (KEY_LEFTALT) -> qnum:56 */
|
|
||||||
0x39, /* linux:57 (KEY_SPACE) -> linux:57 (KEY_SPACE) -> qnum:57 */
|
|
||||||
0x3a, /* linux:58 (KEY_CAPSLOCK) -> linux:58 (KEY_CAPSLOCK) -> qnum:58 */
|
|
||||||
0x3b, /* linux:59 (KEY_F1) -> linux:59 (KEY_F1) -> qnum:59 */
|
|
||||||
0x3c, /* linux:60 (KEY_F2) -> linux:60 (KEY_F2) -> qnum:60 */
|
|
||||||
0x3d, /* linux:61 (KEY_F3) -> linux:61 (KEY_F3) -> qnum:61 */
|
|
||||||
0x3e, /* linux:62 (KEY_F4) -> linux:62 (KEY_F4) -> qnum:62 */
|
|
||||||
0x3f, /* linux:63 (KEY_F5) -> linux:63 (KEY_F5) -> qnum:63 */
|
|
||||||
0x40, /* linux:64 (KEY_F6) -> linux:64 (KEY_F6) -> qnum:64 */
|
|
||||||
0x41, /* linux:65 (KEY_F7) -> linux:65 (KEY_F7) -> qnum:65 */
|
|
||||||
0x42, /* linux:66 (KEY_F8) -> linux:66 (KEY_F8) -> qnum:66 */
|
|
||||||
0x43, /* linux:67 (KEY_F9) -> linux:67 (KEY_F9) -> qnum:67 */
|
|
||||||
0x44, /* linux:68 (KEY_F10) -> linux:68 (KEY_F10) -> qnum:68 */
|
|
||||||
0x45, /* linux:69 (KEY_NUMLOCK) -> linux:69 (KEY_NUMLOCK) -> qnum:69 */
|
|
||||||
0x46, /* linux:70 (KEY_SCROLLLOCK) -> linux:70 (KEY_SCROLLLOCK) -> qnum:70 */
|
|
||||||
0x47, /* linux:71 (KEY_KP7) -> linux:71 (KEY_KP7) -> qnum:71 */
|
|
||||||
0x48, /* linux:72 (KEY_KP8) -> linux:72 (KEY_KP8) -> qnum:72 */
|
|
||||||
0x49, /* linux:73 (KEY_KP9) -> linux:73 (KEY_KP9) -> qnum:73 */
|
|
||||||
0x4a, /* linux:74 (KEY_KPMINUS) -> linux:74 (KEY_KPMINUS) -> qnum:74 */
|
|
||||||
0x4b, /* linux:75 (KEY_KP4) -> linux:75 (KEY_KP4) -> qnum:75 */
|
|
||||||
0x4c, /* linux:76 (KEY_KP5) -> linux:76 (KEY_KP5) -> qnum:76 */
|
|
||||||
0x4d, /* linux:77 (KEY_KP6) -> linux:77 (KEY_KP6) -> qnum:77 */
|
|
||||||
0x4e, /* linux:78 (KEY_KPPLUS) -> linux:78 (KEY_KPPLUS) -> qnum:78 */
|
|
||||||
0x4f, /* linux:79 (KEY_KP1) -> linux:79 (KEY_KP1) -> qnum:79 */
|
|
||||||
0x50, /* linux:80 (KEY_KP2) -> linux:80 (KEY_KP2) -> qnum:80 */
|
|
||||||
0x51, /* linux:81 (KEY_KP3) -> linux:81 (KEY_KP3) -> qnum:81 */
|
|
||||||
0x52, /* linux:82 (KEY_KP0) -> linux:82 (KEY_KP0) -> qnum:82 */
|
|
||||||
0x53, /* linux:83 (KEY_KPDOT) -> linux:83 (KEY_KPDOT) -> qnum:83 */
|
|
||||||
0x54, /* linux:84 (unnamed) -> linux:84 (unnamed) -> qnum:84 */
|
|
||||||
0x76, /* linux:85 (KEY_ZENKAKUHANKAKU) -> linux:85 (KEY_ZENKAKUHANKAKU) -> qnum:118 */
|
|
||||||
0x56, /* linux:86 (KEY_102ND) -> linux:86 (KEY_102ND) -> qnum:86 */
|
|
||||||
0x57, /* linux:87 (KEY_F11) -> linux:87 (KEY_F11) -> qnum:87 */
|
|
||||||
0x58, /* linux:88 (KEY_F12) -> linux:88 (KEY_F12) -> qnum:88 */
|
|
||||||
0x73, /* linux:89 (KEY_RO) -> linux:89 (KEY_RO) -> qnum:115 */
|
|
||||||
0x78, /* linux:90 (KEY_KATAKANA) -> linux:90 (KEY_KATAKANA) -> qnum:120 */
|
|
||||||
0x77, /* linux:91 (KEY_HIRAGANA) -> linux:91 (KEY_HIRAGANA) -> qnum:119 */
|
|
||||||
0x79, /* linux:92 (KEY_HENKAN) -> linux:92 (KEY_HENKAN) -> qnum:121 */
|
|
||||||
0x70, /* linux:93 (KEY_KATAKANAHIRAGANA) -> linux:93 (KEY_KATAKANAHIRAGANA) -> qnum:112 */
|
|
||||||
0x7b, /* linux:94 (KEY_MUHENKAN) -> linux:94 (KEY_MUHENKAN) -> qnum:123 */
|
|
||||||
0x5c, /* linux:95 (KEY_KPJPCOMMA) -> linux:95 (KEY_KPJPCOMMA) -> qnum:92 */
|
|
||||||
0x9c, /* linux:96 (KEY_KPENTER) -> linux:96 (KEY_KPENTER) -> qnum:156 */
|
|
||||||
0x9d, /* linux:97 (KEY_RIGHTCTRL) -> linux:97 (KEY_RIGHTCTRL) -> qnum:157 */
|
|
||||||
0xb5, /* linux:98 (KEY_KPSLASH) -> linux:98 (KEY_KPSLASH) -> qnum:181 */
|
|
||||||
0x54, /* linux:99 (KEY_SYSRQ) -> linux:99 (KEY_SYSRQ) -> qnum:84 */
|
|
||||||
0xb8, /* linux:100 (KEY_RIGHTALT) -> linux:100 (KEY_RIGHTALT) -> qnum:184 */
|
|
||||||
0x5b, /* linux:101 (KEY_LINEFEED) -> linux:101 (KEY_LINEFEED) -> qnum:91 */
|
|
||||||
0xc7, /* linux:102 (KEY_HOME) -> linux:102 (KEY_HOME) -> qnum:199 */
|
|
||||||
0xc8, /* linux:103 (KEY_UP) -> linux:103 (KEY_UP) -> qnum:200 */
|
|
||||||
0xc9, /* linux:104 (KEY_PAGEUP) -> linux:104 (KEY_PAGEUP) -> qnum:201 */
|
|
||||||
0xcb, /* linux:105 (KEY_LEFT) -> linux:105 (KEY_LEFT) -> qnum:203 */
|
|
||||||
0xcd, /* linux:106 (KEY_RIGHT) -> linux:106 (KEY_RIGHT) -> qnum:205 */
|
|
||||||
0xcf, /* linux:107 (KEY_END) -> linux:107 (KEY_END) -> qnum:207 */
|
|
||||||
0xd0, /* linux:108 (KEY_DOWN) -> linux:108 (KEY_DOWN) -> qnum:208 */
|
|
||||||
0xd1, /* linux:109 (KEY_PAGEDOWN) -> linux:109 (KEY_PAGEDOWN) -> qnum:209 */
|
|
||||||
0xd2, /* linux:110 (KEY_INSERT) -> linux:110 (KEY_INSERT) -> qnum:210 */
|
|
||||||
0xd3, /* linux:111 (KEY_DELETE) -> linux:111 (KEY_DELETE) -> qnum:211 */
|
|
||||||
0xef, /* linux:112 (KEY_MACRO) -> linux:112 (KEY_MACRO) -> qnum:239 */
|
|
||||||
0xa0, /* linux:113 (KEY_MUTE) -> linux:113 (KEY_MUTE) -> qnum:160 */
|
|
||||||
0xae, /* linux:114 (KEY_VOLUMEDOWN) -> linux:114 (KEY_VOLUMEDOWN) -> qnum:174 */
|
|
||||||
0xb0, /* linux:115 (KEY_VOLUMEUP) -> linux:115 (KEY_VOLUMEUP) -> qnum:176 */
|
|
||||||
0xde, /* linux:116 (KEY_POWER) -> linux:116 (KEY_POWER) -> qnum:222 */
|
|
||||||
0x59, /* linux:117 (KEY_KPEQUAL) -> linux:117 (KEY_KPEQUAL) -> qnum:89 */
|
|
||||||
0xce, /* linux:118 (KEY_KPPLUSMINUS) -> linux:118 (KEY_KPPLUSMINUS) -> qnum:206 */
|
|
||||||
0xc6, /* linux:119 (KEY_PAUSE) -> linux:119 (KEY_PAUSE) -> qnum:198 */
|
|
||||||
0x8b, /* linux:120 (KEY_SCALE) -> linux:120 (KEY_SCALE) -> qnum:139 */
|
|
||||||
0x7e, /* linux:121 (KEY_KPCOMMA) -> linux:121 (KEY_KPCOMMA) -> qnum:126 */
|
|
||||||
0x72, /* linux:122 (KEY_HANGEUL) -> linux:122 (KEY_HANGEUL) -> qnum:114 */
|
|
||||||
0x71, /* linux:123 (KEY_HANJA) -> linux:123 (KEY_HANJA) -> qnum:113 */
|
|
||||||
0x7d, /* linux:124 (KEY_YEN) -> linux:124 (KEY_YEN) -> qnum:125 */
|
|
||||||
0xdb, /* linux:125 (KEY_LEFTMETA) -> linux:125 (KEY_LEFTMETA) -> qnum:219 */
|
|
||||||
0xdc, /* linux:126 (KEY_RIGHTMETA) -> linux:126 (KEY_RIGHTMETA) -> qnum:220 */
|
|
||||||
0xdd, /* linux:127 (KEY_COMPOSE) -> linux:127 (KEY_COMPOSE) -> qnum:221 */
|
|
||||||
0xe8, /* linux:128 (KEY_STOP) -> linux:128 (KEY_STOP) -> qnum:232 */
|
|
||||||
0x85, /* linux:129 (KEY_AGAIN) -> linux:129 (KEY_AGAIN) -> qnum:133 */
|
|
||||||
0x86, /* linux:130 (KEY_PROPS) -> linux:130 (KEY_PROPS) -> qnum:134 */
|
|
||||||
0x87, /* linux:131 (KEY_UNDO) -> linux:131 (KEY_UNDO) -> qnum:135 */
|
|
||||||
0x8c, /* linux:132 (KEY_FRONT) -> linux:132 (KEY_FRONT) -> qnum:140 */
|
|
||||||
0xf8, /* linux:133 (KEY_COPY) -> linux:133 (KEY_COPY) -> qnum:248 */
|
|
||||||
0x64, /* linux:134 (KEY_OPEN) -> linux:134 (KEY_OPEN) -> qnum:100 */
|
|
||||||
0x65, /* linux:135 (KEY_PASTE) -> linux:135 (KEY_PASTE) -> qnum:101 */
|
|
||||||
0xc1, /* linux:136 (KEY_FIND) -> linux:136 (KEY_FIND) -> qnum:193 */
|
|
||||||
0xbc, /* linux:137 (KEY_CUT) -> linux:137 (KEY_CUT) -> qnum:188 */
|
|
||||||
0xf5, /* linux:138 (KEY_HELP) -> linux:138 (KEY_HELP) -> qnum:245 */
|
|
||||||
0x9e, /* linux:139 (KEY_MENU) -> linux:139 (KEY_MENU) -> qnum:158 */
|
|
||||||
0xa1, /* linux:140 (KEY_CALC) -> linux:140 (KEY_CALC) -> qnum:161 */
|
|
||||||
0x66, /* linux:141 (KEY_SETUP) -> linux:141 (KEY_SETUP) -> qnum:102 */
|
|
||||||
0xdf, /* linux:142 (KEY_SLEEP) -> linux:142 (KEY_SLEEP) -> qnum:223 */
|
|
||||||
0xe3, /* linux:143 (KEY_WAKEUP) -> linux:143 (KEY_WAKEUP) -> qnum:227 */
|
|
||||||
0x67, /* linux:144 (KEY_FILE) -> linux:144 (KEY_FILE) -> qnum:103 */
|
|
||||||
0x68, /* linux:145 (KEY_SENDFILE) -> linux:145 (KEY_SENDFILE) -> qnum:104 */
|
|
||||||
0x69, /* linux:146 (KEY_DELETEFILE) -> linux:146 (KEY_DELETEFILE) -> qnum:105 */
|
|
||||||
0x93, /* linux:147 (KEY_XFER) -> linux:147 (KEY_XFER) -> qnum:147 */
|
|
||||||
0x9f, /* linux:148 (KEY_PROG1) -> linux:148 (KEY_PROG1) -> qnum:159 */
|
|
||||||
0x97, /* linux:149 (KEY_PROG2) -> linux:149 (KEY_PROG2) -> qnum:151 */
|
|
||||||
0x82, /* linux:150 (KEY_WWW) -> linux:150 (KEY_WWW) -> qnum:130 */
|
|
||||||
0x6a, /* linux:151 (KEY_MSDOS) -> linux:151 (KEY_MSDOS) -> qnum:106 */
|
|
||||||
0x92, /* linux:152 (KEY_SCREENLOCK) -> linux:152 (KEY_SCREENLOCK) -> qnum:146 */
|
|
||||||
0x6b, /* linux:153 (KEY_DIRECTION) -> linux:153 (KEY_DIRECTION) -> qnum:107 */
|
|
||||||
0xa6, /* linux:154 (KEY_CYCLEWINDOWS) -> linux:154 (KEY_CYCLEWINDOWS) -> qnum:166 */
|
|
||||||
0xec, /* linux:155 (KEY_MAIL) -> linux:155 (KEY_MAIL) -> qnum:236 */
|
|
||||||
0xe6, /* linux:156 (KEY_BOOKMARKS) -> linux:156 (KEY_BOOKMARKS) -> qnum:230 */
|
|
||||||
0xeb, /* linux:157 (KEY_COMPUTER) -> linux:157 (KEY_COMPUTER) -> qnum:235 */
|
|
||||||
0xea, /* linux:158 (KEY_BACK) -> linux:158 (KEY_BACK) -> qnum:234 */
|
|
||||||
0xe9, /* linux:159 (KEY_FORWARD) -> linux:159 (KEY_FORWARD) -> qnum:233 */
|
|
||||||
0xa3, /* linux:160 (KEY_CLOSECD) -> linux:160 (KEY_CLOSECD) -> qnum:163 */
|
|
||||||
0x6c, /* linux:161 (KEY_EJECTCD) -> linux:161 (KEY_EJECTCD) -> qnum:108 */
|
|
||||||
0xfd, /* linux:162 (KEY_EJECTCLOSECD) -> linux:162 (KEY_EJECTCLOSECD) -> qnum:253 */
|
|
||||||
0x99, /* linux:163 (KEY_NEXTSONG) -> linux:163 (KEY_NEXTSONG) -> qnum:153 */
|
|
||||||
0xa2, /* linux:164 (KEY_PLAYPAUSE) -> linux:164 (KEY_PLAYPAUSE) -> qnum:162 */
|
|
||||||
0x90, /* linux:165 (KEY_PREVIOUSSONG) -> linux:165 (KEY_PREVIOUSSONG) -> qnum:144 */
|
|
||||||
0xa4, /* linux:166 (KEY_STOPCD) -> linux:166 (KEY_STOPCD) -> qnum:164 */
|
|
||||||
0xb1, /* linux:167 (KEY_RECORD) -> linux:167 (KEY_RECORD) -> qnum:177 */
|
|
||||||
0x98, /* linux:168 (KEY_REWIND) -> linux:168 (KEY_REWIND) -> qnum:152 */
|
|
||||||
0x63, /* linux:169 (KEY_PHONE) -> linux:169 (KEY_PHONE) -> qnum:99 */
|
|
||||||
0, /* linux:170 (KEY_ISO) -> linux:170 (KEY_ISO) -> qnum:None */
|
|
||||||
0x81, /* linux:171 (KEY_CONFIG) -> linux:171 (KEY_CONFIG) -> qnum:129 */
|
|
||||||
0xb2, /* linux:172 (KEY_HOMEPAGE) -> linux:172 (KEY_HOMEPAGE) -> qnum:178 */
|
|
||||||
0xe7, /* linux:173 (KEY_REFRESH) -> linux:173 (KEY_REFRESH) -> qnum:231 */
|
|
||||||
0, /* linux:174 (KEY_EXIT) -> linux:174 (KEY_EXIT) -> qnum:None */
|
|
||||||
0, /* linux:175 (KEY_MOVE) -> linux:175 (KEY_MOVE) -> qnum:None */
|
|
||||||
0x88, /* linux:176 (KEY_EDIT) -> linux:176 (KEY_EDIT) -> qnum:136 */
|
|
||||||
0x75, /* linux:177 (KEY_SCROLLUP) -> linux:177 (KEY_SCROLLUP) -> qnum:117 */
|
|
||||||
0x8f, /* linux:178 (KEY_SCROLLDOWN) -> linux:178 (KEY_SCROLLDOWN) -> qnum:143 */
|
|
||||||
0xf6, /* linux:179 (KEY_KPLEFTPAREN) -> linux:179 (KEY_KPLEFTPAREN) -> qnum:246 */
|
|
||||||
0xfb, /* linux:180 (KEY_KPRIGHTPAREN) -> linux:180 (KEY_KPRIGHTPAREN) -> qnum:251 */
|
|
||||||
0x89, /* linux:181 (KEY_NEW) -> linux:181 (KEY_NEW) -> qnum:137 */
|
|
||||||
0x8a, /* linux:182 (KEY_REDO) -> linux:182 (KEY_REDO) -> qnum:138 */
|
|
||||||
0x5d, /* linux:183 (KEY_F13) -> linux:183 (KEY_F13) -> qnum:93 */
|
|
||||||
0x5e, /* linux:184 (KEY_F14) -> linux:184 (KEY_F14) -> qnum:94 */
|
|
||||||
0x5f, /* linux:185 (KEY_F15) -> linux:185 (KEY_F15) -> qnum:95 */
|
|
||||||
0x55, /* linux:186 (KEY_F16) -> linux:186 (KEY_F16) -> qnum:85 */
|
|
||||||
0x83, /* linux:187 (KEY_F17) -> linux:187 (KEY_F17) -> qnum:131 */
|
|
||||||
0xf7, /* linux:188 (KEY_F18) -> linux:188 (KEY_F18) -> qnum:247 */
|
|
||||||
0x84, /* linux:189 (KEY_F19) -> linux:189 (KEY_F19) -> qnum:132 */
|
|
||||||
0x5a, /* linux:190 (KEY_F20) -> linux:190 (KEY_F20) -> qnum:90 */
|
|
||||||
0x74, /* linux:191 (KEY_F21) -> linux:191 (KEY_F21) -> qnum:116 */
|
|
||||||
0xf9, /* linux:192 (KEY_F22) -> linux:192 (KEY_F22) -> qnum:249 */
|
|
||||||
0x6d, /* linux:193 (KEY_F23) -> linux:193 (KEY_F23) -> qnum:109 */
|
|
||||||
0x6f, /* linux:194 (KEY_F24) -> linux:194 (KEY_F24) -> qnum:111 */
|
|
||||||
0x95, /* linux:195 (unnamed) -> linux:195 (unnamed) -> qnum:149 */
|
|
||||||
0x96, /* linux:196 (unnamed) -> linux:196 (unnamed) -> qnum:150 */
|
|
||||||
0x9a, /* linux:197 (unnamed) -> linux:197 (unnamed) -> qnum:154 */
|
|
||||||
0x9b, /* linux:198 (unnamed) -> linux:198 (unnamed) -> qnum:155 */
|
|
||||||
0xa7, /* linux:199 (unnamed) -> linux:199 (unnamed) -> qnum:167 */
|
|
||||||
0xa8, /* linux:200 (KEY_PLAYCD) -> linux:200 (KEY_PLAYCD) -> qnum:168 */
|
|
||||||
0xa9, /* linux:201 (KEY_PAUSECD) -> linux:201 (KEY_PAUSECD) -> qnum:169 */
|
|
||||||
0xab, /* linux:202 (KEY_PROG3) -> linux:202 (KEY_PROG3) -> qnum:171 */
|
|
||||||
0xac, /* linux:203 (KEY_PROG4) -> linux:203 (KEY_PROG4) -> qnum:172 */
|
|
||||||
0xad, /* linux:204 (KEY_DASHBOARD) -> linux:204 (KEY_DASHBOARD) -> qnum:173 */
|
|
||||||
0xa5, /* linux:205 (KEY_SUSPEND) -> linux:205 (KEY_SUSPEND) -> qnum:165 */
|
|
||||||
0xaf, /* linux:206 (KEY_CLOSE) -> linux:206 (KEY_CLOSE) -> qnum:175 */
|
|
||||||
0xb3, /* linux:207 (KEY_PLAY) -> linux:207 (KEY_PLAY) -> qnum:179 */
|
|
||||||
0xb4, /* linux:208 (KEY_FASTFORWARD) -> linux:208 (KEY_FASTFORWARD) -> qnum:180 */
|
|
||||||
0xb6, /* linux:209 (KEY_BASSBOOST) -> linux:209 (KEY_BASSBOOST) -> qnum:182 */
|
|
||||||
0xb9, /* linux:210 (KEY_PRINT) -> linux:210 (KEY_PRINT) -> qnum:185 */
|
|
||||||
0xba, /* linux:211 (KEY_HP) -> linux:211 (KEY_HP) -> qnum:186 */
|
|
||||||
0xbb, /* linux:212 (KEY_CAMERA) -> linux:212 (KEY_CAMERA) -> qnum:187 */
|
|
||||||
0xbd, /* linux:213 (KEY_SOUND) -> linux:213 (KEY_SOUND) -> qnum:189 */
|
|
||||||
0xbe, /* linux:214 (KEY_QUESTION) -> linux:214 (KEY_QUESTION) -> qnum:190 */
|
|
||||||
0xbf, /* linux:215 (KEY_EMAIL) -> linux:215 (KEY_EMAIL) -> qnum:191 */
|
|
||||||
0xc0, /* linux:216 (KEY_CHAT) -> linux:216 (KEY_CHAT) -> qnum:192 */
|
|
||||||
0xe5, /* linux:217 (KEY_SEARCH) -> linux:217 (KEY_SEARCH) -> qnum:229 */
|
|
||||||
0xc2, /* linux:218 (KEY_CONNECT) -> linux:218 (KEY_CONNECT) -> qnum:194 */
|
|
||||||
0xc3, /* linux:219 (KEY_FINANCE) -> linux:219 (KEY_FINANCE) -> qnum:195 */
|
|
||||||
0xc4, /* linux:220 (KEY_SPORT) -> linux:220 (KEY_SPORT) -> qnum:196 */
|
|
||||||
0xc5, /* linux:221 (KEY_SHOP) -> linux:221 (KEY_SHOP) -> qnum:197 */
|
|
||||||
0x94, /* linux:222 (KEY_ALTERASE) -> linux:222 (KEY_ALTERASE) -> qnum:148 */
|
|
||||||
0xca, /* linux:223 (KEY_CANCEL) -> linux:223 (KEY_CANCEL) -> qnum:202 */
|
|
||||||
0xcc, /* linux:224 (KEY_BRIGHTNESSDOWN) -> linux:224 (KEY_BRIGHTNESSDOWN) -> qnum:204 */
|
|
||||||
0xd4, /* linux:225 (KEY_BRIGHTNESSUP) -> linux:225 (KEY_BRIGHTNESSUP) -> qnum:212 */
|
|
||||||
0xed, /* linux:226 (KEY_MEDIA) -> linux:226 (KEY_MEDIA) -> qnum:237 */
|
|
||||||
0xd6, /* linux:227 (KEY_SWITCHVIDEOMODE) -> linux:227 (KEY_SWITCHVIDEOMODE) -> qnum:214 */
|
|
||||||
0xd7, /* linux:228 (KEY_KBDILLUMTOGGLE) -> linux:228 (KEY_KBDILLUMTOGGLE) -> qnum:215 */
|
|
||||||
0xd8, /* linux:229 (KEY_KBDILLUMDOWN) -> linux:229 (KEY_KBDILLUMDOWN) -> qnum:216 */
|
|
||||||
0xd9, /* linux:230 (KEY_KBDILLUMUP) -> linux:230 (KEY_KBDILLUMUP) -> qnum:217 */
|
|
||||||
0xda, /* linux:231 (KEY_SEND) -> linux:231 (KEY_SEND) -> qnum:218 */
|
|
||||||
0xe4, /* linux:232 (KEY_REPLY) -> linux:232 (KEY_REPLY) -> qnum:228 */
|
|
||||||
0x8e, /* linux:233 (KEY_FORWARDMAIL) -> linux:233 (KEY_FORWARDMAIL) -> qnum:142 */
|
|
||||||
0xd5, /* linux:234 (KEY_SAVE) -> linux:234 (KEY_SAVE) -> qnum:213 */
|
|
||||||
0xf0, /* linux:235 (KEY_DOCUMENTS) -> linux:235 (KEY_DOCUMENTS) -> qnum:240 */
|
|
||||||
0xf1, /* linux:236 (KEY_BATTERY) -> linux:236 (KEY_BATTERY) -> qnum:241 */
|
|
||||||
0xf2, /* linux:237 (KEY_BLUETOOTH) -> linux:237 (KEY_BLUETOOTH) -> qnum:242 */
|
|
||||||
0xf3, /* linux:238 (KEY_WLAN) -> linux:238 (KEY_WLAN) -> qnum:243 */
|
|
||||||
0xf4, /* linux:239 (KEY_UWB) -> linux:239 (KEY_UWB) -> qnum:244 */
|
|
||||||
|
|
||||||
|
|
||||||
/* Rest of the keycodes have no mapping so far
|
|
||||||
|
|
||||||
0, /* linux:240 (KEY_UNKNOWN) -> linux:240 (KEY_UNKNOWN) -> qnum:None */
|
|
||||||
0, /* linux:241 (KEY_VIDEO_NEXT) -> linux:241 (KEY_VIDEO_NEXT) -> qnum:None */
|
|
||||||
0, /* linux:242 (KEY_VIDEO_PREV) -> linux:242 (KEY_VIDEO_PREV) -> qnum:None */
|
|
||||||
0, /* linux:243 (KEY_BRIGHTNESS_CYCLE) -> linux:243 (KEY_BRIGHTNESS_CYCLE) -> qnum:None */
|
|
||||||
0, /* linux:244 (KEY_BRIGHTNESS_ZERO) -> linux:244 (KEY_BRIGHTNESS_ZERO) -> qnum:None */
|
|
||||||
0, /* linux:245 (KEY_DISPLAY_OFF) -> linux:245 (KEY_DISPLAY_OFF) -> qnum:None */
|
|
||||||
0, /* linux:246 (KEY_WIMAX) -> linux:246 (KEY_WIMAX) -> qnum:None */
|
|
||||||
0, /* linux:247 (unnamed) -> linux:247 (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:248 (unnamed) -> linux:248 (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:249 (unnamed) -> linux:249 (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:250 (unnamed) -> linux:250 (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:251 (unnamed) -> linux:251 (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:252 (unnamed) -> linux:252 (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:253 (unnamed) -> linux:253 (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:254 (unnamed) -> linux:254 (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:255 (unnamed) -> linux:255 (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:256 (BTN_0) -> linux:256 (BTN_0) -> qnum:None */
|
|
||||||
0, /* linux:257 (BTN_1) -> linux:257 (BTN_1) -> qnum:None */
|
|
||||||
0, /* linux:258 (BTN_2) -> linux:258 (BTN_2) -> qnum:None */
|
|
||||||
0, /* linux:259 (BTN_3) -> linux:259 (BTN_3) -> qnum:None */
|
|
||||||
0, /* linux:260 (BTN_4) -> linux:260 (BTN_4) -> qnum:None */
|
|
||||||
0, /* linux:261 (BTN_5) -> linux:261 (BTN_5) -> qnum:None */
|
|
||||||
0, /* linux:262 (BTN_6) -> linux:262 (BTN_6) -> qnum:None */
|
|
||||||
0, /* linux:263 (BTN_7) -> linux:263 (BTN_7) -> qnum:None */
|
|
||||||
0, /* linux:264 (BTN_8) -> linux:264 (BTN_8) -> qnum:None */
|
|
||||||
0, /* linux:265 (BTN_9) -> linux:265 (BTN_9) -> qnum:None */
|
|
||||||
0, /* linux:266 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:267 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:268 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:269 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:270 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:271 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:272 (BTN_LEFT) -> linux:272 (BTN_LEFT) -> qnum:None */
|
|
||||||
0, /* linux:273 (BTN_RIGHT) -> linux:273 (BTN_RIGHT) -> qnum:None */
|
|
||||||
0, /* linux:274 (BTN_MIDDLE) -> linux:274 (BTN_MIDDLE) -> qnum:None */
|
|
||||||
0, /* linux:275 (BTN_SIDE) -> linux:275 (BTN_SIDE) -> qnum:None */
|
|
||||||
0, /* linux:276 (BTN_EXTRA) -> linux:276 (BTN_EXTRA) -> qnum:None */
|
|
||||||
0, /* linux:277 (BTN_FORWARD) -> linux:277 (BTN_FORWARD) -> qnum:None */
|
|
||||||
0, /* linux:278 (BTN_BACK) -> linux:278 (BTN_BACK) -> qnum:None */
|
|
||||||
0, /* linux:279 (BTN_TASK) -> linux:279 (BTN_TASK) -> qnum:None */
|
|
||||||
0, /* linux:280 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:281 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:282 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:283 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:284 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:285 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:286 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:287 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:288 (BTN_TRIGGER) -> linux:288 (BTN_TRIGGER) -> qnum:None */
|
|
||||||
0, /* linux:289 (BTN_THUMB) -> linux:289 (BTN_THUMB) -> qnum:None */
|
|
||||||
0, /* linux:290 (BTN_THUMB2) -> linux:290 (BTN_THUMB2) -> qnum:None */
|
|
||||||
0, /* linux:291 (BTN_TOP) -> linux:291 (BTN_TOP) -> qnum:None */
|
|
||||||
0, /* linux:292 (BTN_TOP2) -> linux:292 (BTN_TOP2) -> qnum:None */
|
|
||||||
0, /* linux:293 (BTN_PINKIE) -> linux:293 (BTN_PINKIE) -> qnum:None */
|
|
||||||
0, /* linux:294 (BTN_BASE) -> linux:294 (BTN_BASE) -> qnum:None */
|
|
||||||
0, /* linux:295 (BTN_BASE2) -> linux:295 (BTN_BASE2) -> qnum:None */
|
|
||||||
0, /* linux:296 (BTN_BASE3) -> linux:296 (BTN_BASE3) -> qnum:None */
|
|
||||||
0, /* linux:297 (BTN_BASE4) -> linux:297 (BTN_BASE4) -> qnum:None */
|
|
||||||
0, /* linux:298 (BTN_BASE5) -> linux:298 (BTN_BASE5) -> qnum:None */
|
|
||||||
0, /* linux:299 (BTN_BASE6) -> linux:299 (BTN_BASE6) -> qnum:None */
|
|
||||||
0, /* linux:300 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:301 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:302 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:303 (BTN_DEAD) -> linux:303 (BTN_DEAD) -> qnum:None */
|
|
||||||
0, /* linux:304 (BTN_A) -> linux:304 (BTN_A) -> qnum:None */
|
|
||||||
0, /* linux:305 (BTN_B) -> linux:305 (BTN_B) -> qnum:None */
|
|
||||||
0, /* linux:306 (BTN_C) -> linux:306 (BTN_C) -> qnum:None */
|
|
||||||
0, /* linux:307 (BTN_X) -> linux:307 (BTN_X) -> qnum:None */
|
|
||||||
0, /* linux:308 (BTN_Y) -> linux:308 (BTN_Y) -> qnum:None */
|
|
||||||
0, /* linux:309 (BTN_Z) -> linux:309 (BTN_Z) -> qnum:None */
|
|
||||||
0, /* linux:310 (BTN_TL) -> linux:310 (BTN_TL) -> qnum:None */
|
|
||||||
0, /* linux:311 (BTN_TR) -> linux:311 (BTN_TR) -> qnum:None */
|
|
||||||
0, /* linux:312 (BTN_TL2) -> linux:312 (BTN_TL2) -> qnum:None */
|
|
||||||
0, /* linux:313 (BTN_TR2) -> linux:313 (BTN_TR2) -> qnum:None */
|
|
||||||
0, /* linux:314 (BTN_SELECT) -> linux:314 (BTN_SELECT) -> qnum:None */
|
|
||||||
0, /* linux:315 (BTN_START) -> linux:315 (BTN_START) -> qnum:None */
|
|
||||||
0, /* linux:316 (BTN_MODE) -> linux:316 (BTN_MODE) -> qnum:None */
|
|
||||||
0, /* linux:317 (BTN_THUMBL) -> linux:317 (BTN_THUMBL) -> qnum:None */
|
|
||||||
0, /* linux:318 (BTN_THUMBR) -> linux:318 (BTN_THUMBR) -> qnum:None */
|
|
||||||
0, /* linux:319 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:320 (BTN_TOOL_PEN) -> linux:320 (BTN_TOOL_PEN) -> qnum:None */
|
|
||||||
0, /* linux:321 (BTN_TOOL_RUBBER) -> linux:321 (BTN_TOOL_RUBBER) -> qnum:None */
|
|
||||||
0, /* linux:322 (BTN_TOOL_BRUSH) -> linux:322 (BTN_TOOL_BRUSH) -> qnum:None */
|
|
||||||
0, /* linux:323 (BTN_TOOL_PENCIL) -> linux:323 (BTN_TOOL_PENCIL) -> qnum:None */
|
|
||||||
0, /* linux:324 (BTN_TOOL_AIRBRUSH) -> linux:324 (BTN_TOOL_AIRBRUSH) -> qnum:None */
|
|
||||||
0, /* linux:325 (BTN_TOOL_FINGER) -> linux:325 (BTN_TOOL_FINGER) -> qnum:None */
|
|
||||||
0, /* linux:326 (BTN_TOOL_MOUSE) -> linux:326 (BTN_TOOL_MOUSE) -> qnum:None */
|
|
||||||
0, /* linux:327 (BTN_TOOL_LENS) -> linux:327 (BTN_TOOL_LENS) -> qnum:None */
|
|
||||||
0, /* linux:328 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:329 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:330 (BTN_TOUCH) -> linux:330 (BTN_TOUCH) -> qnum:None */
|
|
||||||
0, /* linux:331 (BTN_STYLUS) -> linux:331 (BTN_STYLUS) -> qnum:None */
|
|
||||||
0, /* linux:332 (BTN_STYLUS2) -> linux:332 (BTN_STYLUS2) -> qnum:None */
|
|
||||||
0, /* linux:333 (BTN_TOOL_DOUBLETAP) -> linux:333 (BTN_TOOL_DOUBLETAP) -> qnum:None */
|
|
||||||
0, /* linux:334 (BTN_TOOL_TRIPLETAP) -> linux:334 (BTN_TOOL_TRIPLETAP) -> qnum:None */
|
|
||||||
0, /* linux:335 (BTN_TOOL_QUADTAP) -> linux:335 (BTN_TOOL_QUADTAP) -> qnum:None */
|
|
||||||
0, /* linux:336 (BTN_GEAR_DOWN) -> linux:336 (BTN_GEAR_DOWN) -> qnum:None */
|
|
||||||
0, /* linux:337 (BTN_GEAR_UP) -> linux:337 (BTN_GEAR_UP) -> qnum:None */
|
|
||||||
0, /* linux:338 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:339 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:340 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:341 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:342 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:343 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:344 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:345 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:346 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:347 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:348 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:349 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:350 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:351 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:352 (KEY_OK) -> linux:352 (KEY_OK) -> qnum:None */
|
|
||||||
0, /* linux:353 (KEY_SELECT) -> linux:353 (KEY_SELECT) -> qnum:None */
|
|
||||||
0, /* linux:354 (KEY_GOTO) -> linux:354 (KEY_GOTO) -> qnum:None */
|
|
||||||
0, /* linux:355 (KEY_CLEAR) -> linux:355 (KEY_CLEAR) -> qnum:None */
|
|
||||||
0, /* linux:356 (KEY_POWER2) -> linux:356 (KEY_POWER2) -> qnum:None */
|
|
||||||
0, /* linux:357 (KEY_OPTION) -> linux:357 (KEY_OPTION) -> qnum:None */
|
|
||||||
0, /* linux:358 (KEY_INFO) -> linux:358 (KEY_INFO) -> qnum:None */
|
|
||||||
0, /* linux:359 (KEY_TIME) -> linux:359 (KEY_TIME) -> qnum:None */
|
|
||||||
0, /* linux:360 (KEY_VENDOR) -> linux:360 (KEY_VENDOR) -> qnum:None */
|
|
||||||
0, /* linux:361 (KEY_ARCHIVE) -> linux:361 (KEY_ARCHIVE) -> qnum:None */
|
|
||||||
0, /* linux:362 (KEY_PROGRAM) -> linux:362 (KEY_PROGRAM) -> qnum:None */
|
|
||||||
0, /* linux:363 (KEY_CHANNEL) -> linux:363 (KEY_CHANNEL) -> qnum:None */
|
|
||||||
0, /* linux:364 (KEY_FAVORITES) -> linux:364 (KEY_FAVORITES) -> qnum:None */
|
|
||||||
0, /* linux:365 (KEY_EPG) -> linux:365 (KEY_EPG) -> qnum:None */
|
|
||||||
0, /* linux:366 (KEY_PVR) -> linux:366 (KEY_PVR) -> qnum:None */
|
|
||||||
0, /* linux:367 (KEY_MHP) -> linux:367 (KEY_MHP) -> qnum:None */
|
|
||||||
0, /* linux:368 (KEY_LANGUAGE) -> linux:368 (KEY_LANGUAGE) -> qnum:None */
|
|
||||||
0, /* linux:369 (KEY_TITLE) -> linux:369 (KEY_TITLE) -> qnum:None */
|
|
||||||
0, /* linux:370 (KEY_SUBTITLE) -> linux:370 (KEY_SUBTITLE) -> qnum:None */
|
|
||||||
0, /* linux:371 (KEY_ANGLE) -> linux:371 (KEY_ANGLE) -> qnum:None */
|
|
||||||
0, /* linux:372 (KEY_ZOOM) -> linux:372 (KEY_ZOOM) -> qnum:None */
|
|
||||||
0, /* linux:373 (KEY_MODE) -> linux:373 (KEY_MODE) -> qnum:None */
|
|
||||||
0, /* linux:374 (KEY_KEYBOARD) -> linux:374 (KEY_KEYBOARD) -> qnum:None */
|
|
||||||
0, /* linux:375 (KEY_SCREEN) -> linux:375 (KEY_SCREEN) -> qnum:None */
|
|
||||||
0, /* linux:376 (KEY_PC) -> linux:376 (KEY_PC) -> qnum:None */
|
|
||||||
0, /* linux:377 (KEY_TV) -> linux:377 (KEY_TV) -> qnum:None */
|
|
||||||
0, /* linux:378 (KEY_TV2) -> linux:378 (KEY_TV2) -> qnum:None */
|
|
||||||
0, /* linux:379 (KEY_VCR) -> linux:379 (KEY_VCR) -> qnum:None */
|
|
||||||
0, /* linux:380 (KEY_VCR2) -> linux:380 (KEY_VCR2) -> qnum:None */
|
|
||||||
0, /* linux:381 (KEY_SAT) -> linux:381 (KEY_SAT) -> qnum:None */
|
|
||||||
0, /* linux:382 (KEY_SAT2) -> linux:382 (KEY_SAT2) -> qnum:None */
|
|
||||||
0, /* linux:383 (KEY_CD) -> linux:383 (KEY_CD) -> qnum:None */
|
|
||||||
0, /* linux:384 (KEY_TAPE) -> linux:384 (KEY_TAPE) -> qnum:None */
|
|
||||||
0, /* linux:385 (KEY_RADIO) -> linux:385 (KEY_RADIO) -> qnum:None */
|
|
||||||
0, /* linux:386 (KEY_TUNER) -> linux:386 (KEY_TUNER) -> qnum:None */
|
|
||||||
0, /* linux:387 (KEY_PLAYER) -> linux:387 (KEY_PLAYER) -> qnum:None */
|
|
||||||
0, /* linux:388 (KEY_TEXT) -> linux:388 (KEY_TEXT) -> qnum:None */
|
|
||||||
0, /* linux:389 (KEY_DVD) -> linux:389 (KEY_DVD) -> qnum:None */
|
|
||||||
0, /* linux:390 (KEY_AUX) -> linux:390 (KEY_AUX) -> qnum:None */
|
|
||||||
0, /* linux:391 (KEY_MP3) -> linux:391 (KEY_MP3) -> qnum:None */
|
|
||||||
0, /* linux:392 (KEY_AUDIO) -> linux:392 (KEY_AUDIO) -> qnum:None */
|
|
||||||
0, /* linux:393 (KEY_VIDEO) -> linux:393 (KEY_VIDEO) -> qnum:None */
|
|
||||||
0, /* linux:394 (KEY_DIRECTORY) -> linux:394 (KEY_DIRECTORY) -> qnum:None */
|
|
||||||
0, /* linux:395 (KEY_LIST) -> linux:395 (KEY_LIST) -> qnum:None */
|
|
||||||
0, /* linux:396 (KEY_MEMO) -> linux:396 (KEY_MEMO) -> qnum:None */
|
|
||||||
0, /* linux:397 (KEY_CALENDAR) -> linux:397 (KEY_CALENDAR) -> qnum:None */
|
|
||||||
0, /* linux:398 (KEY_RED) -> linux:398 (KEY_RED) -> qnum:None */
|
|
||||||
0, /* linux:399 (KEY_GREEN) -> linux:399 (KEY_GREEN) -> qnum:None */
|
|
||||||
0, /* linux:400 (KEY_YELLOW) -> linux:400 (KEY_YELLOW) -> qnum:None */
|
|
||||||
0, /* linux:401 (KEY_BLUE) -> linux:401 (KEY_BLUE) -> qnum:None */
|
|
||||||
0, /* linux:402 (KEY_CHANNELUP) -> linux:402 (KEY_CHANNELUP) -> qnum:None */
|
|
||||||
0, /* linux:403 (KEY_CHANNELDOWN) -> linux:403 (KEY_CHANNELDOWN) -> qnum:None */
|
|
||||||
0, /* linux:404 (KEY_FIRST) -> linux:404 (KEY_FIRST) -> qnum:None */
|
|
||||||
0, /* linux:405 (KEY_LAST) -> linux:405 (KEY_LAST) -> qnum:None */
|
|
||||||
0, /* linux:406 (KEY_AB) -> linux:406 (KEY_AB) -> qnum:None */
|
|
||||||
0, /* linux:407 (KEY_NEXT) -> linux:407 (KEY_NEXT) -> qnum:None */
|
|
||||||
0, /* linux:408 (KEY_RESTART) -> linux:408 (KEY_RESTART) -> qnum:None */
|
|
||||||
0, /* linux:409 (KEY_SLOW) -> linux:409 (KEY_SLOW) -> qnum:None */
|
|
||||||
0, /* linux:410 (KEY_SHUFFLE) -> linux:410 (KEY_SHUFFLE) -> qnum:None */
|
|
||||||
0, /* linux:411 (KEY_BREAK) -> linux:411 (KEY_BREAK) -> qnum:None */
|
|
||||||
0, /* linux:412 (KEY_PREVIOUS) -> linux:412 (KEY_PREVIOUS) -> qnum:None */
|
|
||||||
0, /* linux:413 (KEY_DIGITS) -> linux:413 (KEY_DIGITS) -> qnum:None */
|
|
||||||
0, /* linux:414 (KEY_TEEN) -> linux:414 (KEY_TEEN) -> qnum:None */
|
|
||||||
0, /* linux:415 (KEY_TWEN) -> linux:415 (KEY_TWEN) -> qnum:None */
|
|
||||||
0, /* linux:416 (KEY_VIDEOPHONE) -> linux:416 (KEY_VIDEOPHONE) -> qnum:None */
|
|
||||||
0, /* linux:417 (KEY_GAMES) -> linux:417 (KEY_GAMES) -> qnum:None */
|
|
||||||
0, /* linux:418 (KEY_ZOOMIN) -> linux:418 (KEY_ZOOMIN) -> qnum:None */
|
|
||||||
0, /* linux:419 (KEY_ZOOMOUT) -> linux:419 (KEY_ZOOMOUT) -> qnum:None */
|
|
||||||
0, /* linux:420 (KEY_ZOOMRESET) -> linux:420 (KEY_ZOOMRESET) -> qnum:None */
|
|
||||||
0, /* linux:421 (KEY_WORDPROCESSOR) -> linux:421 (KEY_WORDPROCESSOR) -> qnum:None */
|
|
||||||
0, /* linux:422 (KEY_EDITOR) -> linux:422 (KEY_EDITOR) -> qnum:None */
|
|
||||||
0, /* linux:423 (KEY_SPREADSHEET) -> linux:423 (KEY_SPREADSHEET) -> qnum:None */
|
|
||||||
0, /* linux:424 (KEY_GRAPHICSEDITOR) -> linux:424 (KEY_GRAPHICSEDITOR) -> qnum:None */
|
|
||||||
0, /* linux:425 (KEY_PRESENTATION) -> linux:425 (KEY_PRESENTATION) -> qnum:None */
|
|
||||||
0, /* linux:426 (KEY_DATABASE) -> linux:426 (KEY_DATABASE) -> qnum:None */
|
|
||||||
0, /* linux:427 (KEY_NEWS) -> linux:427 (KEY_NEWS) -> qnum:None */
|
|
||||||
0, /* linux:428 (KEY_VOICEMAIL) -> linux:428 (KEY_VOICEMAIL) -> qnum:None */
|
|
||||||
0, /* linux:429 (KEY_ADDRESSBOOK) -> linux:429 (KEY_ADDRESSBOOK) -> qnum:None */
|
|
||||||
0, /* linux:430 (KEY_MESSENGER) -> linux:430 (KEY_MESSENGER) -> qnum:None */
|
|
||||||
0, /* linux:431 (KEY_DISPLAYTOGGLE) -> linux:431 (KEY_DISPLAYTOGGLE) -> qnum:None */
|
|
||||||
0, /* linux:432 (KEY_SPELLCHECK) -> linux:432 (KEY_SPELLCHECK) -> qnum:None */
|
|
||||||
0, /* linux:433 (KEY_LOGOFF) -> linux:433 (KEY_LOGOFF) -> qnum:None */
|
|
||||||
0, /* linux:434 (KEY_DOLLAR) -> linux:434 (KEY_DOLLAR) -> qnum:None */
|
|
||||||
0, /* linux:435 (KEY_EURO) -> linux:435 (KEY_EURO) -> qnum:None */
|
|
||||||
0, /* linux:436 (KEY_FRAMEBACK) -> linux:436 (KEY_FRAMEBACK) -> qnum:None */
|
|
||||||
0, /* linux:437 (KEY_FRAMEFORWARD) -> linux:437 (KEY_FRAMEFORWARD) -> qnum:None */
|
|
||||||
0, /* linux:438 (KEY_CONTEXT_MENU) -> linux:438 (KEY_CONTEXT_MENU) -> qnum:None */
|
|
||||||
0, /* linux:439 (KEY_MEDIA_REPEAT) -> linux:439 (KEY_MEDIA_REPEAT) -> qnum:None */
|
|
||||||
0, /* linux:440 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:441 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:442 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:443 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:444 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:445 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:446 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:447 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:448 (KEY_DEL_EOL) -> linux:448 (KEY_DEL_EOL) -> qnum:None */
|
|
||||||
0, /* linux:449 (KEY_DEL_EOS) -> linux:449 (KEY_DEL_EOS) -> qnum:None */
|
|
||||||
0, /* linux:450 (KEY_INS_LINE) -> linux:450 (KEY_INS_LINE) -> qnum:None */
|
|
||||||
0, /* linux:451 (KEY_DEL_LINE) -> linux:451 (KEY_DEL_LINE) -> qnum:None */
|
|
||||||
0, /* linux:452 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:453 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:454 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:455 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:456 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:457 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:458 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:459 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:460 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:461 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:462 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:463 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:464 (KEY_FN) -> linux:464 (KEY_FN) -> qnum:None */
|
|
||||||
0, /* linux:465 (KEY_FN_ESC) -> linux:465 (KEY_FN_ESC) -> qnum:None */
|
|
||||||
0, /* linux:466 (KEY_FN_F1) -> linux:466 (KEY_FN_F1) -> qnum:None */
|
|
||||||
0, /* linux:467 (KEY_FN_F2) -> linux:467 (KEY_FN_F2) -> qnum:None */
|
|
||||||
0, /* linux:468 (KEY_FN_F3) -> linux:468 (KEY_FN_F3) -> qnum:None */
|
|
||||||
0, /* linux:469 (KEY_FN_F4) -> linux:469 (KEY_FN_F4) -> qnum:None */
|
|
||||||
0, /* linux:470 (KEY_FN_F5) -> linux:470 (KEY_FN_F5) -> qnum:None */
|
|
||||||
0, /* linux:471 (KEY_FN_F6) -> linux:471 (KEY_FN_F6) -> qnum:None */
|
|
||||||
0, /* linux:472 (KEY_FN_F7) -> linux:472 (KEY_FN_F7) -> qnum:None */
|
|
||||||
0, /* linux:473 (KEY_FN_F8) -> linux:473 (KEY_FN_F8) -> qnum:None */
|
|
||||||
0, /* linux:474 (KEY_FN_F9) -> linux:474 (KEY_FN_F9) -> qnum:None */
|
|
||||||
0, /* linux:475 (KEY_FN_F10) -> linux:475 (KEY_FN_F10) -> qnum:None */
|
|
||||||
0, /* linux:476 (KEY_FN_F11) -> linux:476 (KEY_FN_F11) -> qnum:None */
|
|
||||||
0, /* linux:477 (KEY_FN_F12) -> linux:477 (KEY_FN_F12) -> qnum:None */
|
|
||||||
0, /* linux:478 (KEY_FN_1) -> linux:478 (KEY_FN_1) -> qnum:None */
|
|
||||||
0, /* linux:479 (KEY_FN_2) -> linux:479 (KEY_FN_2) -> qnum:None */
|
|
||||||
0, /* linux:480 (KEY_FN_D) -> linux:480 (KEY_FN_D) -> qnum:None */
|
|
||||||
0, /* linux:481 (KEY_FN_E) -> linux:481 (KEY_FN_E) -> qnum:None */
|
|
||||||
0, /* linux:482 (KEY_FN_F) -> linux:482 (KEY_FN_F) -> qnum:None */
|
|
||||||
0, /* linux:483 (KEY_FN_S) -> linux:483 (KEY_FN_S) -> qnum:None */
|
|
||||||
0, /* linux:484 (KEY_FN_B) -> linux:484 (KEY_FN_B) -> qnum:None */
|
|
||||||
0, /* linux:485 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:486 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:487 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:488 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:489 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:490 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:491 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:492 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:493 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:494 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:495 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:496 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:497 (KEY_BRL_DOT1) -> linux:497 (KEY_BRL_DOT1) -> qnum:None */
|
|
||||||
0, /* linux:498 (KEY_BRL_DOT2) -> linux:498 (KEY_BRL_DOT2) -> qnum:None */
|
|
||||||
0, /* linux:499 (KEY_BRL_DOT3) -> linux:499 (KEY_BRL_DOT3) -> qnum:None */
|
|
||||||
0, /* linux:500 (KEY_BRL_DOT4) -> linux:500 (KEY_BRL_DOT4) -> qnum:None */
|
|
||||||
0, /* linux:501 (KEY_BRL_DOT5) -> linux:501 (KEY_BRL_DOT5) -> qnum:None */
|
|
||||||
0, /* linux:502 (KEY_BRL_DOT6) -> linux:502 (KEY_BRL_DOT6) -> qnum:None */
|
|
||||||
0, /* linux:503 (KEY_BRL_DOT7) -> linux:503 (KEY_BRL_DOT7) -> qnum:None */
|
|
||||||
0, /* linux:504 (KEY_BRL_DOT8) -> linux:504 (KEY_BRL_DOT8) -> qnum:None */
|
|
||||||
0, /* linux:505 (KEY_BRL_DOT9) -> linux:505 (KEY_BRL_DOT9) -> qnum:None */
|
|
||||||
0, /* linux:506 (KEY_BRL_DOT10) -> linux:506 (KEY_BRL_DOT10) -> qnum:None */
|
|
||||||
0, /* linux:507 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:508 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:509 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:510 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:511 (unnamed) -> linux:None (unnamed) -> qnum:None */
|
|
||||||
0, /* linux:512 (KEY_NUMERIC_0) -> linux:512 (KEY_NUMERIC_0) -> qnum:None */
|
|
||||||
0, /* linux:513 (KEY_NUMERIC_1) -> linux:513 (KEY_NUMERIC_1) -> qnum:None */
|
|
||||||
0, /* linux:514 (KEY_NUMERIC_2) -> linux:514 (KEY_NUMERIC_2) -> qnum:None */
|
|
||||||
0, /* linux:515 (KEY_NUMERIC_3) -> linux:515 (KEY_NUMERIC_3) -> qnum:None */
|
|
||||||
0, /* linux:516 (KEY_NUMERIC_4) -> linux:516 (KEY_NUMERIC_4) -> qnum:None */
|
|
||||||
0, /* linux:517 (KEY_NUMERIC_5) -> linux:517 (KEY_NUMERIC_5) -> qnum:None */
|
|
||||||
0, /* linux:518 (KEY_NUMERIC_6) -> linux:518 (KEY_NUMERIC_6) -> qnum:None */
|
|
||||||
0, /* linux:519 (KEY_NUMERIC_7) -> linux:519 (KEY_NUMERIC_7) -> qnum:None */
|
|
||||||
0, /* linux:520 (KEY_NUMERIC_8) -> linux:520 (KEY_NUMERIC_8) -> qnum:None */
|
|
||||||
0, /* linux:521 (KEY_NUMERIC_9) -> linux:521 (KEY_NUMERIC_9) -> qnum:None */
|
|
||||||
0, /* linux:522 (KEY_NUMERIC_STAR) -> linux:522 (KEY_NUMERIC_STAR) -> qnum:None */
|
|
||||||
0, /* linux:523 (KEY_NUMERIC_POUND) -> linux:523 (KEY_NUMERIC_POUND) -> qnum:None */
|
|
||||||
0, /* linux:524 (KEY_RFKILL) -> linux:524 (KEY_RFKILL) -> qnum:None */
|
|
||||||
*/
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
~
|
|
||||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
~
|
|
||||||
~ See COPYING.txt for more details.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!--
|
|
||||||
This color can be used for the icon of controls (e.g. EditText).
|
|
||||||
It is only valid for API >= 23
|
|
||||||
-->
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:alpha="?android:disabledAlpha" android:color="?colorControlNormal" android:state_enabled="false" />
|
|
||||||
<item android:color="?colorControlNormal" android:state_focused="false" android:state_pressed="false" />
|
|
||||||
<item android:color="?colorControlActivated" />
|
|
||||||
</selector>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
~
|
|
||||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
~
|
|
||||||
~ See COPYING.txt for more details.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:color="?colorControlHighlight">
|
|
||||||
<item>
|
|
||||||
|
|
||||||
<shape android:shape="oval">
|
|
||||||
<solid android:color="?attr/colorSurface" />
|
|
||||||
<size
|
|
||||||
|
|
||||||
android:width="@dimen/action_btn_size"
|
|
||||||
android:height="@dimen/action_btn_size" />
|
|
||||||
<stroke
|
|
||||||
android:width="1dp"
|
|
||||||
android:color="@color/colorBorder" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</ripple>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
~
|
|
||||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
~
|
|
||||||
~ See COPYING.txt for more details.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- Custom background for FrameView. Used to avoid white overlay when not in touch-mode-->
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:drawable="@android:color/transparent" android:state_focused="true" />
|
|
||||||
<item android:drawable="@android:color/transparent" android:state_window_focused="true" />
|
|
||||||
<item android:drawable="@android:color/transparent" />
|
|
||||||
</selector>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
~
|
|
||||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
~
|
|
||||||
~ See COPYING.txt for more details.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<solid android:color="?attr/colorSurface" />
|
|
||||||
<corners android:radius="5dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="1dp"
|
|
||||||
android:color="@color/colorBorder" />
|
|
||||||
</shape>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
~
|
|
||||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
~
|
|
||||||
~ See COPYING.txt for more details.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_checked="true">
|
|
||||||
<color android:color="?colorControlHighlight" />
|
|
||||||
</item>
|
|
||||||
</selector>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
~
|
|
||||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
~
|
|
||||||
~ See COPYING.txt for more details.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:color="?colorControlHighlight">
|
|
||||||
<item>
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<corners android:radius="@dimen/urlbar_bg_corner_radius" />
|
|
||||||
<solid android:color="@color/colorUrlBarBackground" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</ripple>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:autoMirrored="true"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="?colorOnSurface"
|
|
||||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="?colorOnSurface"
|
|
||||||
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="?colorOnSurface"
|
|
||||||
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="?colorOnSurface"
|
|
||||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
|
||||||
</vector>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user