mirror of
https://github.com/Cateners/tiny_computer.git
synced 2026-05-20 16:35:47 +08:00
Compare commits
28 Commits
v1.0.6+1
...
v1.0.14+10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a956d26f6d | ||
|
|
c2ee3b694c | ||
|
|
09bf75beed | ||
|
|
b00ede9483 | ||
|
|
b8393dacfd | ||
|
|
ce5ad3b758 | ||
|
|
cf15e2e07d | ||
|
|
938745036d | ||
|
|
b3428555c6 | ||
|
|
2a19c5eb78 | ||
|
|
54a941da63 | ||
|
|
87beedef68 | ||
|
|
c6afc4d468 | ||
|
|
6dbe710fdc | ||
|
|
cf8ce47662 | ||
|
|
86ce2315d4 | ||
|
|
6e51e5b2d2 | ||
|
|
0971059111 | ||
|
|
2cf19179f9 | ||
|
|
b6d4d2f11b | ||
|
|
5a6f04d094 | ||
|
|
195a2f50a3 | ||
|
|
16a14c8a3e | ||
|
|
f15899be95 | ||
|
|
27a5073551 | ||
|
|
b4ca9ae4f7 | ||
|
|
2ef590beb7 | ||
|
|
e6555b2166 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,6 +42,7 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
/android/app/.cxx
|
||||||
|
|
||||||
/backup
|
/backup
|
||||||
|
|
||||||
|
|||||||
67
README.md
67
README.md
@@ -2,9 +2,32 @@
|
|||||||
|
|
||||||
<img decoding="async" src="readme/cover0.png" width="50%">
|
<img decoding="async" src="readme/cover0.png" width="50%">
|
||||||
|
|
||||||
即开即用的类PC环境,内置火狐浏览器和fcitx输入法等常用软件
|
给所有安卓arm64设备的“PC应用引擎”平替
|
||||||
|
|
||||||
Click-to-run debian bookworm xfce on android for Chinese users, with fcitx pinyin input method and wps office preinstalled. No termux required.
|
Click-to-run debian bookworm xfce on android for Chinese users, with fcitx pinyin input method preinstalled. No termux required.
|
||||||
|
|
||||||
|
## 特点
|
||||||
|
|
||||||
|
- 一键安装,即开即用
|
||||||
|
- 来自kali-undercover的win10主题(仅xfce版本),友好的界面
|
||||||
|
|
||||||
|
<img decoding="async" src="readme/img1.png" width="50%">
|
||||||
|
|
||||||
|
- 提供常用软件的一键安装指令
|
||||||
|
|
||||||
|
<img decoding="async" src="readme/img2.png" width="50%">
|
||||||
|
|
||||||
|
- 可方便地改变屏幕缩放,不用担心屏幕过大或过小
|
||||||
|
|
||||||
|
<img decoding="async" src="readme/img3.gif" width="50%">
|
||||||
|
|
||||||
|
- 便捷访问设备文件,或通过设备SAF访问软件文件
|
||||||
|
|
||||||
|
<img decoding="async" src="readme/img4.png" width="50%">
|
||||||
|
|
||||||
|
- 提供终端和众多可调节参数供高级用户使用
|
||||||
|
|
||||||
|
<img decoding="async" src="readme/img5.png" width="50%">
|
||||||
|
|
||||||
## 原理
|
## 原理
|
||||||
|
|
||||||
@@ -12,45 +35,35 @@ Click-to-run debian bookworm xfce on android for Chinese users, with fcitx pinyi
|
|||||||
|
|
||||||
内置[noVNC](https://github.com/novnc/noVNC)显示图形界面
|
内置[noVNC](https://github.com/novnc/noVNC)显示图形界面
|
||||||
|
|
||||||
初次启动由于解压的缘故要点时间
|
|
||||||
以后点开就能用
|
|
||||||
|
|
||||||
只支持arm64安卓
|
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
assets的文件来源如下:
|
assets的文件来源信息可以在[这里](extra/readme.md)找到。
|
||||||
|
|
||||||
- [proot](https://github.com/termux/proot/), 使用[build-proot-android](https://github.com/green-green-avk/build-proot-android)脚本编译
|
完整的容器制作过程可以在[这里](extra/build-tiny-rootfs.md)看到。
|
||||||
- [busybox](https://github.com/meefik/busybox)
|
|
||||||
- [Xserver XSDL, pulseaudio相关文件](https://github.com/pelya/commandergenius/tree/sdl_android/project/jni/application/xserver)
|
|
||||||
- [Tmoe Linux, debian包来源](https://github.com/2moe/tmoe)
|
|
||||||
|
|
||||||
其中busybox和pulseaudio相关文件都是直接用了二进制文件。
|
|
||||||
|
|
||||||
(pulseaudio我真的编译不来,如果你会的话请教教我吧)
|
|
||||||
|
|
||||||
对debian容器进行了如下修改:
|
|
||||||
- 使用tmoe安装了xfce环境和全套VNC;
|
|
||||||
- 使用kali-undercover提供的Win10主题美化xfce;
|
|
||||||
- (使用tmoe)安装了fcitx输入法和云拼音组件。按<Ctrl+空格>切换输入法。
|
|
||||||
- 强烈建议**不要**使用安卓中文输入法直接输入中文,而是使用英文键盘通过容器的输入法输入中文,避免丢字错字。
|
|
||||||
- 对noVNC进行[修改](https://github.com/Cateners/noVNC) (scale_factor分支),添加了scale factor滑块控制缩放,添加了上下左右shift等按键
|
|
||||||
- 在主目录下可以方便地访问手机存储(如果提供了存储权限的话)
|
|
||||||
- 启动时会尝试挂载手机的一些字体目录(AppFiles/Fonts、Fonts和/system/fonts), 如果这些目录下有字体文件的话会一并加载到系统中,无需额外安装
|
|
||||||
- 最后采用tar.xz压缩,用split命令分成了xa*等多个文件
|
|
||||||
|
|
||||||
数据包不再在assets中更新,而是随releases提供,主要是为了避免git越来越大
|
数据包不再在assets中更新,而是随releases提供,主要是为了避免git越来越大
|
||||||
|
|
||||||
lib目录:
|
lib目录:
|
||||||
|
|
||||||
- main.dart文件,页面布局,老实说已经有点乱了
|
- main.dart文件,页面布局,有点乱
|
||||||
- workflow.dart文件,逻辑部分,目前也还可以理解
|
- workflow.dart文件,逻辑部分,目前也还可以理解
|
||||||
- Util 工具类
|
- Util 工具类
|
||||||
- TermPty 一个终端
|
- TermPty 一个终端
|
||||||
- G 全局变量类
|
- G 全局变量类
|
||||||
- Workflow 从软件点开到容器启动的所有步骤
|
- Workflow 从软件点开到容器启动的所有步骤
|
||||||
|
|
||||||
|
## 编译
|
||||||
|
|
||||||
|
你需要配置好flutter和安卓sdk,然后克隆此项目。
|
||||||
|
|
||||||
|
在编译之前,需要在release中下载系统rootfs(或者[自行制作](extra/build-tiny-rootfs.md)),之后使用split命令分割,拷贝到assets。一般我将其分为98MB。
|
||||||
|
|
||||||
|
`split -b 98M debian.tar.xz`
|
||||||
|
|
||||||
|
接下来就可以编译了。我使用的命令如下:
|
||||||
|
|
||||||
|
`flutter build apk --target-platform android-arm64 --split-per-abi --obfuscate --split-debug-info=tiny_computer/sdi`
|
||||||
|
|
||||||
## 目前已知bug
|
## 目前已知bug
|
||||||
|
|
||||||
多用户/分身情形无法sudo, 其它见issue
|
多用户/分身情形无法sudo, 其它见issue
|
||||||
|
|||||||
106
android/app/CMakeLists.txt
Normal file
106
android/app/CMakeLists.txt
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
|
||||||
|
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,6 +2,10 @@ 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()
|
||||||
@@ -38,6 +42,7 @@ android {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
main.java.srcDirs += 'src/main/java'
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -45,10 +50,17 @@ 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 flutter.minSdkVersion
|
minSdkVersion 24 //ffmpeg_kit; flutter.minSdkVersion
|
||||||
targetSdkVersion 28 //https://github.com/termux/termux-app/issues/1072; native; linker; flutter.targetSdkVersion
|
targetSdkVersion 28 //https://github.com/termux/termux-app/issues/1072; native; linker; flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|
||||||
|
javaCompileOptions {
|
||||||
|
annotationProcessorOptions {
|
||||||
|
arguments += ["room.schemaLocation": "$projectDir/roomSchema/".toString()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -59,14 +71,56 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
dataBinding true
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
version '3.22.1'
|
||||||
|
path file('CMakeLists.txt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
//checkReleaseBuilds false
|
//checkReleaseBuilds false
|
||||||
abortOnError false
|
abortOnError false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||||
|
pickFirst 'lib/x86_64/libc++_shared.so'
|
||||||
|
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
|
||||||
|
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source '../..'
|
source '../..'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {}
|
dependencies {
|
||||||
|
implementation "androidx.core:core-ktx:1.12.0"
|
||||||
|
implementation "androidx.activity:activity-ktx:1.8.2"
|
||||||
|
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 "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0"
|
||||||
|
implementation "org.connectbot:sshlib:2.2.23"
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
160
android/app/roomSchema/com.gaurav.avnc.model.db.MainDb/1.json
Normal file
160
android/app/roomSchema/com.gaurav.avnc.model.db.MainDb/1.json
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
{
|
||||||
|
"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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
188
android/app/roomSchema/com.gaurav.avnc.model.db.MainDb/2.json
Normal file
188
android/app/roomSchema/com.gaurav.avnc.model.db.MainDb/2.json
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
{
|
||||||
|
"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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
209
android/app/roomSchema/com.gaurav.avnc.model.db.MainDb/3.json
Normal file
209
android/app/roomSchema/com.gaurav.avnc.model.db.MainDb/3.json
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
{
|
||||||
|
"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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
216
android/app/roomSchema/com.gaurav.avnc.model.db.MainDb/4.json
Normal file
216
android/app/roomSchema/com.gaurav.avnc.model.db.MainDb/4.json
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
{
|
||||||
|
"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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
215
android/app/roomSchema/com.gaurav.avnc.model.db.MainDb/5.json
Normal file
215
android/app/roomSchema/com.gaurav.avnc.model.db.MainDb/5.json
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
{
|
||||||
|
"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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<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.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<application
|
<application
|
||||||
android:label="小小电脑"
|
android:label="小小电脑"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true"
|
||||||
|
android:theme="@style/App.Theme">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="navigation|orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
@@ -28,6 +33,42 @@
|
|||||||
<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
|
||||||
|
android:name="com.gaurav.avnc.ui.vnc.VncActivity"
|
||||||
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
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
|
||||||
|
android:name="com.example.tiny_computer.filepicker.TinyDocumentsProvider"
|
||||||
|
android:authorities="com.example.tiny_computer.documents"
|
||||||
|
android:exported="true"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||||
|
</intent-filter>
|
||||||
|
</provider>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
202
android/app/src/main/assets/license/Apache-2.0.txt
Normal file
202
android/app/src/main/assets/license/Apache-2.0.txt
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
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.
|
||||||
29
android/app/src/main/assets/license/BSD-libjpeg-turbo.txt
Normal file
29
android/app/src/main/assets/license/BSD-libjpeg-turbo.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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.
|
||||||
674
android/app/src/main/assets/license/GPL-3.0.txt
Normal file
674
android/app/src/main/assets/license/GPL-3.0.txt
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
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>.
|
||||||
48
android/app/src/main/assets/license/X11.txt
Normal file
48
android/app/src/main/assets/license/X11.txt
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
/***********************************************************
|
||||||
|
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.
|
||||||
|
|
||||||
|
******************************************************************/
|
||||||
87
android/app/src/main/assets/license/sshlib.txt
Normal file
87
android/app/src/main/assets/license/sshlib.txt
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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.
|
||||||
87
android/app/src/main/cpp/ClientEx.h
Normal file
87
android/app/src/main/cpp/ClientEx.h
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
111
android/app/src/main/cpp/Cursor.h
Normal file
111
android/app/src/main/cpp/Cursor.h
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
94
android/app/src/main/cpp/Utility.h
Normal file
94
android/app/src/main/cpp/Utility.h
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
561
android/app/src/main/cpp/native-vnc.cpp
Normal file
561
android/app/src/main/cpp/native-vnc.cpp
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
package com.example.tiny_computer.filepicker;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.res.AssetFileDescriptor;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.MatrixCursor;
|
||||||
|
import android.graphics.Point;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.CancellationSignal;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.DocumentsContract.Document;
|
||||||
|
import android.provider.DocumentsContract.Root;
|
||||||
|
import android.provider.DocumentsProvider;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import com.example.tiny_computer.R;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
//This file is mainly copied from Termux :P
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A document provider for the Storage Access Framework which exposes the files in the
|
||||||
|
* $HOME/ directory to other apps.
|
||||||
|
* <p/>
|
||||||
|
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
|
||||||
|
* <p/>
|
||||||
|
* "A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you
|
||||||
|
* support both of them simultaneously, your app will appear twice in the system picker UI,
|
||||||
|
* offering two different ways of accessing your stored data. This would be confusing for users."
|
||||||
|
* - http://developer.android.com/guide/topics/providers/document-provider.html#43
|
||||||
|
*/
|
||||||
|
public class TinyDocumentsProvider extends DocumentsProvider {
|
||||||
|
|
||||||
|
private static final String ALL_MIME_TYPES = "*/*";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// The default columns to return information about a root if no specific
|
||||||
|
// columns are requested in a query.
|
||||||
|
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{
|
||||||
|
Root.COLUMN_ROOT_ID,
|
||||||
|
Root.COLUMN_MIME_TYPES,
|
||||||
|
Root.COLUMN_FLAGS,
|
||||||
|
Root.COLUMN_ICON,
|
||||||
|
Root.COLUMN_TITLE,
|
||||||
|
Root.COLUMN_SUMMARY,
|
||||||
|
Root.COLUMN_DOCUMENT_ID,
|
||||||
|
Root.COLUMN_AVAILABLE_BYTES
|
||||||
|
};
|
||||||
|
|
||||||
|
// The default columns to return information about a document if no specific
|
||||||
|
// columns are requested in a query.
|
||||||
|
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{
|
||||||
|
Document.COLUMN_DOCUMENT_ID,
|
||||||
|
Document.COLUMN_MIME_TYPE,
|
||||||
|
Document.COLUMN_DISPLAY_NAME,
|
||||||
|
Document.COLUMN_LAST_MODIFIED,
|
||||||
|
Document.COLUMN_FLAGS,
|
||||||
|
Document.COLUMN_SIZE
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor queryRoots(String[] projection) {
|
||||||
|
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
|
||||||
|
final String applicationName = "小小电脑";
|
||||||
|
final File BASE_DIR = new File(getContext().getFilesDir(), "containers");
|
||||||
|
final MatrixCursor.RowBuilder row = result.newRow();
|
||||||
|
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
||||||
|
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
|
||||||
|
row.add(Root.COLUMN_SUMMARY, null);
|
||||||
|
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
|
||||||
|
row.add(Root.COLUMN_TITLE, applicationName);
|
||||||
|
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
||||||
|
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
||||||
|
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
|
||||||
|
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
||||||
|
includeFile(result, documentId, null);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
|
||||||
|
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
||||||
|
final File parent = getFileForDocId(parentDocumentId);
|
||||||
|
for (File file : parent.listFiles()) {
|
||||||
|
includeFile(result, null, file);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
|
||||||
|
final File file = getFileForDocId(documentId);
|
||||||
|
final int accessMode = ParcelFileDescriptor.parseMode(mode);
|
||||||
|
return ParcelFileDescriptor.open(file, accessMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
|
||||||
|
final File file = getFileForDocId(documentId);
|
||||||
|
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||||
|
return new AssetFileDescriptor(pfd, 0, file.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreate() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
|
||||||
|
File newFile = new File(parentDocumentId, displayName);
|
||||||
|
int noConflictId = 2;
|
||||||
|
while (newFile.exists()) {
|
||||||
|
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
boolean succeeded;
|
||||||
|
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||||
|
succeeded = newFile.mkdir();
|
||||||
|
} else {
|
||||||
|
succeeded = newFile.createNewFile();
|
||||||
|
}
|
||||||
|
if (!succeeded) {
|
||||||
|
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
||||||
|
}
|
||||||
|
return newFile.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteDocument(String documentId) throws FileNotFoundException {
|
||||||
|
File file = getFileForDocId(documentId);
|
||||||
|
if (!file.delete()) {
|
||||||
|
throw new FileNotFoundException("Failed to delete document with id " + documentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDocumentType(String documentId) throws FileNotFoundException {
|
||||||
|
File file = getFileForDocId(documentId);
|
||||||
|
return getMimeType(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException {
|
||||||
|
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
||||||
|
final File parent = getFileForDocId(rootId);
|
||||||
|
|
||||||
|
// This example implementation searches file names for the query and doesn't rank search
|
||||||
|
// results, so we can stop as soon as we find a sufficient number of matches. Other
|
||||||
|
// implementations might rank results and use other data about files, rather than the file
|
||||||
|
// name, to produce a match.
|
||||||
|
final LinkedList<File> pending = new LinkedList<>();
|
||||||
|
pending.add(parent);
|
||||||
|
|
||||||
|
final int MAX_SEARCH_RESULTS = 50;
|
||||||
|
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
|
||||||
|
final File file = pending.removeFirst();
|
||||||
|
// Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search
|
||||||
|
// through the whole SD card).
|
||||||
|
boolean isInsideHome;
|
||||||
|
try {
|
||||||
|
isInsideHome = file.getCanonicalPath().startsWith(new File(getContext().getFilesDir(), "containers").getAbsolutePath());
|
||||||
|
} catch (IOException e) {
|
||||||
|
isInsideHome = true;
|
||||||
|
}
|
||||||
|
if (isInsideHome) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
Collections.addAll(pending, file.listFiles());
|
||||||
|
} else {
|
||||||
|
if (file.getName().toLowerCase().contains(query)) {
|
||||||
|
includeFile(result, null, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isChildDocument(String parentDocumentId, String documentId) {
|
||||||
|
return documentId.startsWith(parentDocumentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the document id given a file. This document id must be consistent across time as other
|
||||||
|
* applications may save the ID and use it to reference documents later.
|
||||||
|
* <p/>
|
||||||
|
* The reverse of @{link #getFileForDocId}.
|
||||||
|
*/
|
||||||
|
private static String getDocIdForFile(File file) {
|
||||||
|
return file.getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file given a document id (the reverse of {@link #getDocIdForFile(File)}).
|
||||||
|
*/
|
||||||
|
private static File getFileForDocId(String docId) throws FileNotFoundException {
|
||||||
|
final File f = new File(docId);
|
||||||
|
if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getMimeType(File file) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
return Document.MIME_TYPE_DIR;
|
||||||
|
} else {
|
||||||
|
final String name = file.getName();
|
||||||
|
final int lastDot = name.lastIndexOf('.');
|
||||||
|
if (lastDot >= 0) {
|
||||||
|
final String extension = name.substring(lastDot + 1).toLowerCase();
|
||||||
|
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
|
if (mime != null) return mime;
|
||||||
|
}
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a representation of a file to a cursor.
|
||||||
|
*
|
||||||
|
* @param result the cursor to modify
|
||||||
|
* @param docId the document ID representing the desired file (may be null if given file)
|
||||||
|
* @param file the File object representing the desired file (may be null if given docID)
|
||||||
|
*/
|
||||||
|
private void includeFile(MatrixCursor result, String docId, File file)
|
||||||
|
throws FileNotFoundException {
|
||||||
|
if (docId == null) {
|
||||||
|
docId = getDocIdForFile(file);
|
||||||
|
} else {
|
||||||
|
file = getFileForDocId(docId);
|
||||||
|
}
|
||||||
|
|
||||||
|
int flags = 0;
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||||
|
} else if (file.canWrite()) {
|
||||||
|
flags |= Document.FLAG_SUPPORTS_WRITE;
|
||||||
|
}
|
||||||
|
if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
|
||||||
|
|
||||||
|
final String displayName = file.getName();
|
||||||
|
final String mimeType = getMimeType(file);
|
||||||
|
if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
|
||||||
|
|
||||||
|
final MatrixCursor.RowBuilder row = result.newRow();
|
||||||
|
row.add(Document.COLUMN_DOCUMENT_ID, docId);
|
||||||
|
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
|
||||||
|
row.add(Document.COLUMN_SIZE, file.length());
|
||||||
|
row.add(Document.COLUMN_MIME_TYPE, mimeType);
|
||||||
|
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||||
|
row.add(Document.COLUMN_FLAGS, flags);
|
||||||
|
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,56 @@
|
|||||||
package com.example.tiny_computer
|
package com.example.tiny_computer
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.annotation.NonNull
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
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.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) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
prefs = AppPreferences(this)
|
||||||
|
prefs.ui.theme.observeForever { updateNightMode(it) }
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "avnc").setMethodCallHandler {
|
||||||
|
// 注册通道并设置方法调用处理器
|
||||||
|
call, result ->
|
||||||
|
// 判断方法名
|
||||||
|
when (call.method) {
|
||||||
|
"launchUsingUri" -> {
|
||||||
|
com.gaurav.avnc.ui.vnc.startVncActivity(this, com.gaurav.avnc.vnc.VncUri(call.argument("vncUri")!!))
|
||||||
|
result.success(0)
|
||||||
|
}
|
||||||
|
"launchPrefsPage" -> {
|
||||||
|
startActivity(Intent(this, com.gaurav.avnc.ui.prefs.PrefsActivity::class.java))
|
||||||
|
result.success(0)
|
||||||
|
}
|
||||||
|
"launchAboutPage" -> {
|
||||||
|
startActivity(Intent(this, com.gaurav.avnc.ui.about.AboutActivity::class.java))
|
||||||
|
result.success(0)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// 不支持的方法名
|
||||||
|
result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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"),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
310
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/Dispatcher.kt
Normal file
310
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/Dispatcher.kt
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
291
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/FrameState.kt
Normal file
291
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/FrameState.kt
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
106
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/FrameView.kt
Normal file
106
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/FrameView.kt
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
386
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/KeyHandler.kt
Normal file
386
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/KeyHandler.kt
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
294
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/Toolbar.kt
Normal file
294
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/Toolbar.kt
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,543 @@
|
|||||||
|
/*
|
||||||
|
* 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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
108
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/gl/Frame.kt
Normal file
108
android/app/src/main/kotlin/com/gaurav/avnc/ui/vnc/gl/Frame.kt
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}"""
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
android/app/src/main/kotlin/com/gaurav/avnc/util/Binding.kt
Normal file
101
android/app/src/main/kotlin/com/gaurav/avnc/util/Binding.kt
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
* 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!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
android/app/src/main/kotlin/com/gaurav/avnc/vnc/Messenger.kt
Normal file
102
android/app/src/main/kotlin/com/gaurav/avnc/vnc/Messenger.kt
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* 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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* 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 = ""
|
||||||
|
)
|
||||||
348
android/app/src/main/kotlin/com/gaurav/avnc/vnc/VncClient.kt
Normal file
348
android/app/src/main/kotlin/com/gaurav/avnc/vnc/VncClient.kt
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
android/app/src/main/kotlin/com/gaurav/avnc/vnc/VncUri.kt
Normal file
78
android/app/src/main/kotlin/com/gaurav/avnc/vnc/VncUri.kt
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
2525
android/app/src/main/kotlin/com/gaurav/avnc/vnc/XKeySym.kt
Normal file
2525
android/app/src/main/kotlin/com/gaurav/avnc/vnc/XKeySym.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,337 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
*/
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,819 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
)
|
||||||
|
}
|
||||||
573
android/app/src/main/kotlin/com/gaurav/avnc/vnc/XTKeyCode.kt
Normal file
573
android/app/src/main/kotlin/com/gaurav/avnc/vnc/XTKeyCode.kt
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
/*
|
||||||
|
* 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 */
|
||||||
|
*/
|
||||||
|
)
|
||||||
|
}
|
||||||
17
android/app/src/main/res/color/control_icon.xml
Normal file
17
android/app/src/main/res/color/control_icon.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?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>
|
||||||
25
android/app/src/main/res/drawable/bg_circular_button.xml
Normal file
25
android/app/src/main/res/drawable/bg_circular_button.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?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>
|
||||||
14
android/app/src/main/res/drawable/bg_frame_view.xml
Normal file
14
android/app/src/main/res/drawable/bg_frame_view.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?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>
|
||||||
16
android/app/src/main/res/drawable/bg_round_rect.xml
Normal file
16
android/app/src/main/res/drawable/bg_round_rect.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?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>
|
||||||
13
android/app/src/main/res/drawable/bg_toggle_button.xml
Normal file
13
android/app/src/main/res/drawable/bg_toggle_button.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?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>
|
||||||
18
android/app/src/main/res/drawable/bg_urlbar.xml
Normal file
18
android/app/src/main/res/drawable/bg_urlbar.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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>
|
||||||
10
android/app/src/main/res/drawable/ic_arrow_back.xml
Normal file
10
android/app/src/main/res/drawable/ic_arrow_back.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<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>
|
||||||
9
android/app/src/main/res/drawable/ic_bookmark.xml
Normal file
9
android/app/src/main/res/drawable/ic_bookmark.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
9
android/app/src/main/res/drawable/ic_bug.xml
Normal file
9
android/app/src/main/res/drawable/ic_bug.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
9
android/app/src/main/res/drawable/ic_clear.xml
Normal file
9
android/app/src/main/res/drawable/ic_clear.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
9
android/app/src/main/res/drawable/ic_computer.xml
Normal file
9
android/app/src/main/res/drawable/ic_computer.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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,18c1.1,0 1.99,-0.9 1.99,-2L22,6c0,-1.1 -0.9,-2 -2,-2H4c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2H0v2h24v-2h-4zM4,6h16v10H4V6z" />
|
||||||
|
</vector>
|
||||||
22
android/app/src/main/res/drawable/ic_computer_shortcut.xml
Normal file
22
android/app/src/main/res/drawable/ic_computer_shortcut.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="#f5f5f5" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item
|
||||||
|
android:bottom="16dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="16dp">
|
||||||
|
<vector
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#2128ab"
|
||||||
|
android:pathData="M20,18c1.1,0 1.99,-0.9 1.99,-2L22,6c0,-1.1 -0.9,-2 -2,-2H4c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2H0v2h24v-2h-4zM4,6h16v10H4V6z" />
|
||||||
|
</vector>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
9
android/app/src/main/res/drawable/ic_download.xml
Normal file
9
android/app/src/main/res/drawable/ic_download.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_experimental.xml
Normal file
9
android/app/src/main/res/drawable/ic_experimental.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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.8,18.4L14,10.67V6.5l1.35,-1.69C15.61,4.48 15.38,4 14.96,4H9.04C8.62,4 8.39,4.48 8.65,4.81L10,6.5v4.17L4.2,18.4C3.71,19.06 4.18,20 5,20h14C19.82,20 20.29,19.06 19.8,18.4z" />
|
||||||
|
</vector>
|
||||||
10
android/app/src/main/res/drawable/ic_file.xml
Normal file
10
android/app/src/main/res/drawable/ic_file.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<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="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_gesture.xml
Normal file
9
android/app/src/main/res/drawable/ic_gesture.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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="M4.59,6.89c0.7,-0.71 1.4,-1.35 1.71,-1.22 0.5,0.2 0,1.03 -0.3,1.52 -0.25,0.42 -2.86,3.89 -2.86,6.31 0,1.28 0.48,2.34 1.34,2.98 0.75,0.56 1.74,0.73 2.64,0.46 1.07,-0.31 1.95,-1.4 3.06,-2.77 1.21,-1.49 2.83,-3.44 4.08,-3.44 1.63,0 1.65,1.01 1.76,1.79 -3.78,0.64 -5.38,3.67 -5.38,5.37 0,1.7 1.44,3.09 3.21,3.09 1.63,0 4.29,-1.33 4.69,-6.1L21,14.88v-2.5h-2.47c-0.15,-1.65 -1.09,-4.2 -4.03,-4.2 -2.25,0 -4.18,1.91 -4.94,2.84 -0.58,0.73 -2.06,2.48 -2.29,2.72 -0.25,0.3 -0.68,0.84 -1.11,0.84 -0.45,0 -0.72,-0.83 -0.36,-1.92 0.35,-1.09 1.4,-2.86 1.85,-3.52 0.78,-1.14 1.3,-1.92 1.3,-3.28C8.95,3.69 7.31,3 6.44,3 5.12,3 3.97,4 3.72,4.25c-0.36,0.36 -0.66,0.66 -0.88,0.93l1.75,1.71zM13.88,18.55c-0.31,0 -0.74,-0.26 -0.74,-0.72 0,-0.6 0.73,-2.2 2.87,-2.76 -0.3,2.69 -1.43,3.48 -2.13,3.48z" />
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_github.xml
Normal file
9
android/app/src/main/res/drawable/ic_github.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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="m11.999,1c-6.0742,-0 -10.999,5.0495 -10.999,11.2787 0,4.983 3.1515,9.2099 7.5226,10.702 0.5504,0.1032 0.7509,-0.2451 0.7509,-0.5442 0,-0.2673 -0.0095,-0.9769 -0.0149,-1.9179 -3.0597,0.6813 -3.7053,-1.5121 -3.7053,-1.5121 -0.5004,-1.3024 -1.2216,-1.6492 -1.2216,-1.6492 -0.9987,-0.7 0.0756,-0.6861 0.0756,-0.6861 1.1041,0.0803 1.6848,1.1625 1.6848,1.1625 0.9812,1.7233 2.5748,1.2255 3.2015,0.9375 0.0999,-0.7291 0.3836,-1.2262 0.6982,-1.508 -2.4425,-0.2846 -5.0106,-1.2525 -5.0106,-5.5743 0,-1.231 0.4288,-2.2377 1.1324,-3.0264 -0.1134,-0.2853 -0.4909,-1.4318 0.1074,-2.9848 0,-0 0.9238,-0.3033 3.0253,1.1563 0.8772,-0.2499 1.8185,-0.3753 2.7538,-0.3794 0.9339,0.0042 1.8753,0.1295 2.7538,0.3794 2.1001,-1.4595 3.0219,-1.1563 3.0219,-1.1563 0.6003,1.553 0.2228,2.6996 0.1094,2.9848 0.705,0.7886 1.1311,1.7953 1.1311,3.0264 0,4.3329 -2.5721,5.2863 -5.0227,5.5653 0.395,0.3483 0.7469,1.0365 0.7469,2.0889 0,1.5073 -0.0135,2.7238 -0.0135,3.0935 0,0.3019 0.1979,0.6529 0.7563,0.5428 4.3677,-1.4948 7.5166,-5.719 7.5166,-10.7006C23,6.0495 18.0745,1 11.999,1" />
|
||||||
|
</vector>
|
||||||
92
android/app/src/main/res/drawable/ic_gpl.xml
Normal file
92
android/app/src/main/res/drawable/ic_gpl.xml
Normal file
File diff suppressed because one or more lines are too long
9
android/app/src/main/res/drawable/ic_help.xml
Normal file
9
android/app/src/main/res/drawable/ic_help.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z" />
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_info.xml
Normal file
9
android/app/src/main/res/drawable/ic_info.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_key.xml
Normal file
9
android/app/src/main/res/drawable/ic_key.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_keyboard.xml
Normal file
9
android/app/src/main/res/drawable/ic_keyboard.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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,5L4,5c-1.1,0 -1.99,0.9 -1.99,2L2,17c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,7c0,-1.1 -0.9,-2 -2,-2zM11,8h2v2h-2L11,8zM11,11h2v2h-2v-2zM8,8h2v2L8,10L8,8zM8,11h2v2L8,13v-2zM7,13L5,13v-2h2v2zM7,10L5,10L5,8h2v2zM16,17L8,17v-2h8v2zM16,13h-2v-2h2v2zM16,10h-2L14,8h2v2zM19,13h-2v-2h2v2zM19,10h-2L17,8h2v2z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<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="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<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="M15.41,16.59L10.83,12l4.58,-4.59L14,6l-6,6 6,6 1.41,-1.41z" />
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user