mirror of
https://github.com/Cateners/tiny_computer.git
synced 2026-05-21 00:45:49 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e443ceedc | ||
|
|
b50622787d | ||
|
|
231d1167e0 | ||
|
|
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,6 +42,7 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
/android/app/.cxx
|
||||
|
||||
/backup
|
||||
|
||||
|
||||
64
README.md
64
README.md
@@ -2,54 +2,68 @@
|
||||
|
||||
<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 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%">
|
||||
|
||||
## 原理
|
||||
|
||||
使用proot运行debian环境
|
||||
|
||||
内置[noVNC](https://github.com/novnc/noVNC)显示图形界面
|
||||
|
||||
初次启动由于解压的缘故要点时间
|
||||
以后点开就能用
|
||||
|
||||
只支持arm64安卓
|
||||
|
||||
## 项目结构
|
||||
|
||||
assets的文件来源如下:
|
||||
assets的文件来源信息可以在[这里](extra/readme.md)找到。
|
||||
|
||||
- [proot](https://github.com/Cateners/proot), 使用[build-proot-android](https://github.com/green-green-avk/build-proot-android)脚本编译
|
||||
- [busybox](https://github.com/meefik/busybox)
|
||||
- [tar](https://github.com/Rprop/tar-android-static)
|
||||
- [Xserver XSDL, pulseaudio相关文件](https://github.com/pelya/commandergenius/tree/sdl_android/project/jni/application/xserver)
|
||||
- [Tmoe Linux, debian包来源](https://github.com/2moe/tmoe)
|
||||
|
||||
其中tar、busybox和pulseaudio相关文件都是直接用了二进制文件。
|
||||
|
||||
对debian容器进行了如下修改:
|
||||
- 使用tmoe安装了xfce环境和全套VNC;
|
||||
- 使用kali-undercover提供的Win10主题美化xfce;
|
||||
- (使用tmoe)安装了fcitx输入法和云拼音组件。按<Ctrl+空格>切换输入法。
|
||||
- 强烈建议**不要**使用安卓中文输入法直接输入中文,而是使用英文键盘通过容器的输入法输入中文,避免丢字错字。
|
||||
- 对noVNC进行[修改](https://github.com/Cateners/noVNC),添加了scale factor滑块控制缩放(scale_factor分支),添加了上下左右shift等按键(arrow_key分支)
|
||||
- 在主目录下可以方便地访问手机存储(如果提供了存储权限的话)
|
||||
- 启动时会尝试挂载手机的一些字体目录(AppFiles/Fonts、Fonts和/system/fonts), 如果这些目录下有字体文件的话会一并加载到系统中,无需额外安装
|
||||
- 最后采用tar.xz压缩,用split命令分成了xa*等多个文件
|
||||
完整的容器制作过程可以在[这里](extra/build-tiny-rootfs.md)看到。
|
||||
|
||||
数据包不再在assets中更新,而是随releases提供,主要是为了避免git越来越大
|
||||
|
||||
lib目录:
|
||||
|
||||
- main.dart文件,页面布局,老实说已经有点乱了
|
||||
- main.dart文件,页面布局,有点乱
|
||||
- workflow.dart文件,逻辑部分,目前也还可以理解
|
||||
- Util 工具类
|
||||
- TermPty 一个终端
|
||||
- G 全局变量类
|
||||
- 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
|
||||
|
||||
多用户/分身情形无法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 "kotlin-android"
|
||||
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()
|
||||
@@ -38,6 +42,7 @@ android {
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
main.java.srcDirs += 'src/main/java'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@@ -45,10 +50,17 @@ android {
|
||||
applicationId "com.fct.tiny"
|
||||
// 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.
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
minSdkVersion 24 //ffmpeg_kit; flutter.minSdkVersion
|
||||
targetSdkVersion 28 //https://github.com/termux/termux-app/issues/1072; native; linker; flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments += ["room.schemaLocation": "$projectDir/roomSchema/".toString()]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -59,14 +71,56 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version '3.22.1'
|
||||
path file('CMakeLists.txt')
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
//checkReleaseBuilds 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 {
|
||||
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.READ_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
|
||||
android:label="小小电脑"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/App.Theme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
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:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
@@ -28,6 +33,42 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</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.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<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
|
||||
|
||||
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.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
||||
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", "touchpad")!!
|
||||
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