mirror of
https://git.sr.ht/~thestr4ng3r/chiaki
synced 2025-08-19 21:13:12 -07:00
Compare commits
No commits in common. "master" and "v1.0.2" have entirely different histories.
450 changed files with 3737 additions and 30301 deletions
|
@ -1,51 +1,25 @@
|
||||||
image:
|
image: 'Visual Studio 2017'
|
||||||
- macOS
|
|
||||||
- 'Visual Studio 2019'
|
|
||||||
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
- /^v\d.*$/
|
|
||||||
- /^deploy-test(-.*)?$/
|
|
||||||
|
|
||||||
configuration:
|
configuration:
|
||||||
- Release
|
- Release
|
||||||
|
|
||||||
for:
|
install:
|
||||||
- matrix:
|
- git submodule update --init --recursive
|
||||||
only:
|
|
||||||
- image: 'Visual Studio 2019'
|
|
||||||
|
|
||||||
install:
|
build_script:
|
||||||
- git submodule update --init --recursive
|
- call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
|
||||||
|
- C:\msys64\usr\bin\bash -lc "cd \"%APPVEYOR_BUILD_FOLDER%\" && scripts/appveyor.sh"
|
||||||
|
|
||||||
build_script:
|
artifacts:
|
||||||
- call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
|
- path: Chiaki
|
||||||
- C:\msys64\usr\bin\bash -lc "cd \"%APPVEYOR_BUILD_FOLDER%\" && scripts/appveyor-win.sh"
|
name: Chiaki
|
||||||
|
|
||||||
artifacts:
|
deploy:
|
||||||
- path: Chiaki
|
description: 'Chiaki Binaries'
|
||||||
name: Chiaki
|
provider: GitHub
|
||||||
- path: Chiaki-PDB
|
auth_token:
|
||||||
name: Chiaki-PDB
|
secure: Amvzm3PMM5nv+iFsqaU7TZ9fgyYt/YIrOLV0QMiCyOoUlLRIaiYxWiJ7maTpxhZ9
|
||||||
|
artifact: "Chiaki"
|
||||||
|
on:
|
||||||
|
appveyor_repo_tag: true
|
||||||
|
|
||||||
- matrix:
|
|
||||||
only:
|
|
||||||
- image: macOS
|
|
||||||
|
|
||||||
install:
|
|
||||||
- git submodule update --init --recursive
|
|
||||||
- sudo pip3 install protobuf
|
|
||||||
- HOMEBREW_NO_AUTO_UPDATE=1 brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf
|
|
||||||
- scripts/build-ffmpeg.sh
|
|
||||||
|
|
||||||
build_script:
|
|
||||||
- export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix;/usr/local/opt/openssl@1.1;/usr/local/opt/qt@5"
|
|
||||||
- scripts/build-common.sh
|
|
||||||
- cp -a build/gui/chiaki.app Chiaki.app
|
|
||||||
- /usr/local/opt/qt@5/bin/macdeployqt Chiaki.app -dmg
|
|
||||||
|
|
||||||
artifacts:
|
|
||||||
- path: Chiaki.dmg
|
|
||||||
name: Chiaki
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
image: alpine/latest
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
sources:
|
|
||||||
- https://git.sr.ht/~thestr4ng3r/chiaki
|
|
||||||
|
|
||||||
artifacts:
|
|
||||||
- Chiaki.apk
|
|
||||||
- Chiaki.aab
|
|
||||||
|
|
||||||
secrets:
|
|
||||||
- 163950ff-2ac4-423d-a280-d2d9edbef000
|
|
||||||
- f4bce41f-f96b-4633-80d8-0ff5dd74dc2a
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- build: |
|
|
||||||
cp -v ~/chiaki-local.properties chiaki/android/local.properties || echo "Secrets not available"
|
|
||||||
sudo service docker start
|
|
||||||
timeout 15 sh -c "until sudo docker info; do sleep 0.5; done"
|
|
||||||
sudo docker run \
|
|
||||||
-v /home/build:/home/build \
|
|
||||||
-u $(id -u):$(id -g) \
|
|
||||||
thestr4ng3r/android:90d826e \
|
|
||||||
/bin/bash -c "cd /home/build/chiaki/android && ./gradlew assembleRelease bundleRelease"
|
|
||||||
cp chiaki/android/app/build/outputs/apk/release/app-release*.apk Chiaki.apk
|
|
||||||
cp chiaki/android/app/build/outputs/bundle/release/app-release*.aab Chiaki.aab
|
|
|
@ -1,55 +0,0 @@
|
||||||
|
|
||||||
image: alpine/latest
|
|
||||||
|
|
||||||
sources:
|
|
||||||
- https://git.sr.ht/~thestr4ng3r/chiaki
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- cmake
|
|
||||||
- ninja
|
|
||||||
- protoc
|
|
||||||
- py3-protobuf
|
|
||||||
- py3-setuptools
|
|
||||||
- opus-dev
|
|
||||||
- qt5-qtbase-dev
|
|
||||||
- qt5-qtsvg-dev
|
|
||||||
- qt5-qtmultimedia-dev
|
|
||||||
- ffmpeg-dev
|
|
||||||
- sdl2-dev
|
|
||||||
- podman
|
|
||||||
- fuse
|
|
||||||
- udev
|
|
||||||
- argp-standalone
|
|
||||||
|
|
||||||
artifacts:
|
|
||||||
- chiaki.nro
|
|
||||||
- Chiaki.AppImage
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- setup_podman: |
|
|
||||||
sudo rc-service udev start
|
|
||||||
sudo rc-service cgroups start
|
|
||||||
sudo rc-service fuse start # Fuse for AppImages
|
|
||||||
echo build:100000:65536 | sudo tee /etc/subuid
|
|
||||||
echo build:100000:65536 | sudo tee /etc/subgid
|
|
||||||
# https://www.kernel.org/doc/Documentation/networking/tuntap.txt
|
|
||||||
# for slirp4netns
|
|
||||||
sudo mkdir -p /dev/net
|
|
||||||
sudo mknod /dev/net/tun c 10 200
|
|
||||||
sudo chmod 0666 /dev/net/tun
|
|
||||||
- local_build_and_test: |
|
|
||||||
cd chiaki
|
|
||||||
cmake -Bbuild -GNinja -DCHIAKI_ENABLE_CLI=ON -DCHIAKI_ENABLE_GUI=ON -DCHIAKI_CLI_ARGP_STANDALONE=ON
|
|
||||||
ninja -C build
|
|
||||||
build/test/chiaki-unit
|
|
||||||
- appimage: |
|
|
||||||
cd chiaki
|
|
||||||
scripts/run-podman-build-appimage.sh
|
|
||||||
cp appimage/Chiaki.AppImage ../Chiaki.AppImage
|
|
||||||
- switch: |
|
|
||||||
cd chiaki
|
|
||||||
scripts/switch/run-podman-build-chiaki.sh
|
|
||||||
cp build_switch/switch/chiaki.nro ../chiaki.nro
|
|
||||||
- bullseye: |
|
|
||||||
cd chiaki
|
|
||||||
scripts/run-podman-build-bullseye.sh
|
|
|
@ -1,32 +0,0 @@
|
||||||
|
|
||||||
image: freebsd/14.x
|
|
||||||
|
|
||||||
sources:
|
|
||||||
- https://git.sr.ht/~thestr4ng3r/chiaki
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- cmake
|
|
||||||
- protobuf
|
|
||||||
- py311-setuptools # should not be needed with nanopb >= 0.4.9
|
|
||||||
- py311-protobuf
|
|
||||||
- opus
|
|
||||||
- qt5-core
|
|
||||||
- qt5-qmake
|
|
||||||
- qt5-buildtools
|
|
||||||
- qt5-gui
|
|
||||||
- qt5-widgets
|
|
||||||
- qt5-concurrent
|
|
||||||
- qt5-svg
|
|
||||||
- qt5-opengl
|
|
||||||
- qt5-multimedia
|
|
||||||
- ffmpeg
|
|
||||||
- sdl2
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- build: |
|
|
||||||
cd chiaki
|
|
||||||
mkdir build && cd build
|
|
||||||
cmake ..
|
|
||||||
make -j4
|
|
||||||
test/chiaki-unit
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
|
|
||||||
image: openbsd/latest
|
|
||||||
|
|
||||||
sources:
|
|
||||||
- https://git.sr.ht/~thestr4ng3r/chiaki
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- cmake
|
|
||||||
- protobuf
|
|
||||||
- py3-setuptools # should not be needed with nanopb >= 0.4.9
|
|
||||||
- py3-protobuf
|
|
||||||
- opus
|
|
||||||
- qtbase
|
|
||||||
- qtsvg
|
|
||||||
- qtmultimedia
|
|
||||||
- ffmpeg
|
|
||||||
- sdl2
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- build: |
|
|
||||||
cd chiaki
|
|
||||||
mkdir build && cd build
|
|
||||||
cmake -DCMAKE_PREFIX_PATH="/usr/local/lib;/usr/local/lib/qt5/cmake" ..
|
|
||||||
make -j4
|
|
||||||
test/chiaki-unit
|
|
||||||
|
|
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,2 +0,0 @@
|
||||||
/test/*.inl linguist-generated=true
|
|
||||||
/lib/*.h linguist-language=C
|
|
18
.gitignore
vendored
18
.gitignore
vendored
|
@ -3,8 +3,6 @@
|
||||||
.idea
|
.idea
|
||||||
build
|
build
|
||||||
cmake-build-*
|
cmake-build-*
|
||||||
/build_*
|
|
||||||
/build-*
|
|
||||||
.DS_store
|
.DS_store
|
||||||
*.AppImage
|
*.AppImage
|
||||||
appdir
|
appdir
|
||||||
|
@ -13,21 +11,5 @@ appdir
|
||||||
/SDL2-*
|
/SDL2-*
|
||||||
/opus*
|
/opus*
|
||||||
/ffmpeg*
|
/ffmpeg*
|
||||||
/sdl2-*
|
|
||||||
/protoc*
|
/protoc*
|
||||||
/openssl*
|
/openssl*
|
||||||
.vs
|
|
||||||
CMakeSettings.json
|
|
||||||
chiaki.rb
|
|
||||||
*.jks
|
|
||||||
secret.tar
|
|
||||||
keystore-env.sh
|
|
||||||
compile_commands.json
|
|
||||||
.ccls-cache
|
|
||||||
.cache/
|
|
||||||
.gdb_history
|
|
||||||
chiaki.conf
|
|
||||||
/appimage
|
|
||||||
.cache/
|
|
||||||
/*.app
|
|
||||||
/*.dmg
|
|
||||||
|
|
10
.gitmodules
vendored
10
.gitmodules
vendored
|
@ -6,13 +6,7 @@
|
||||||
url = https://github.com/nanopb/nanopb.git
|
url = https://github.com/nanopb/nanopb.git
|
||||||
[submodule "third-party/jerasure"]
|
[submodule "third-party/jerasure"]
|
||||||
path = third-party/jerasure
|
path = third-party/jerasure
|
||||||
url = https://git.sr.ht/~thestr4ng3r/jerasure
|
url = https://github.com/thestr4ng3r/jerasure.git
|
||||||
[submodule "third-party/gf-complete"]
|
[submodule "third-party/gf-complete"]
|
||||||
path = third-party/gf-complete
|
path = third-party/gf-complete
|
||||||
url = https://git.sr.ht/~thestr4ng3r/gf-complete
|
url = https://github.com/thestr4ng3r/gf-complete.git
|
||||||
[submodule "android/app/src/main/cpp/oboe"]
|
|
||||||
path = android/app/src/main/cpp/oboe
|
|
||||||
url = https://github.com/google/oboe
|
|
||||||
[submodule "switch/borealis"]
|
|
||||||
path = switch/borealis
|
|
||||||
url = https://git.sr.ht/~thestr4ng3r/borealis
|
|
||||||
|
|
94
.travis.yml
Normal file
94
.travis.yml
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
|
||||||
|
language: cpp
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: Linux
|
||||||
|
os: linux
|
||||||
|
dist: bionic
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
sources:
|
||||||
|
- sourceline: "ppa:beineri/opt-qt-5.12.0-bionic"
|
||||||
|
packages:
|
||||||
|
- libprotoc-dev
|
||||||
|
- protobuf-compiler
|
||||||
|
- python3-protobuf
|
||||||
|
- libopus-dev
|
||||||
|
- qt512base
|
||||||
|
- qt512multimedia
|
||||||
|
- qt512gamepad
|
||||||
|
- qt512svg
|
||||||
|
- libgl1-mesa-dev
|
||||||
|
- nasm
|
||||||
|
- libsdl2-dev
|
||||||
|
env:
|
||||||
|
- CMAKE_PREFIX_PATH="$TRAVIS_BUILD_DIR/ffmpeg-prefix;/opt/qt512"
|
||||||
|
- CMAKE_EXTRA_ARGS="-DCMAKE_INSTALL_PREFIX=/usr"
|
||||||
|
- DEPLOY_FILE="Chiaki-x86_64.AppImage"
|
||||||
|
after_success:
|
||||||
|
- make install DESTDIR=../appdir
|
||||||
|
- cd ..
|
||||||
|
- wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-x86_64.AppImage && chmod +x linuxdeploy-x86_64.AppImage
|
||||||
|
- wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage && chmod +x linuxdeploy-plugin-qt-x86_64.AppImage
|
||||||
|
- source /opt/qt512/bin/qt512-env.sh
|
||||||
|
- ./linuxdeploy-x86_64.AppImage --appdir=appdir -e appdir/usr/bin/chiaki -d appdir/usr/share/applications/chiaki.desktop --plugin qt --output appimage
|
||||||
|
- mv Chiaki-*-x86_64.AppImage Chiaki-x86_64.AppImage
|
||||||
|
|
||||||
|
- name: macOS
|
||||||
|
os: osx
|
||||||
|
osx_image: xcode11
|
||||||
|
addons:
|
||||||
|
homebrew:
|
||||||
|
packages:
|
||||||
|
- qt
|
||||||
|
- opus
|
||||||
|
- openssl@1.1
|
||||||
|
- nasm
|
||||||
|
- sdl2
|
||||||
|
env:
|
||||||
|
- CMAKE_PREFIX_PATH="$TRAVIS_BUILD_DIR/ffmpeg-prefix;/usr/local/opt/openssl@1.1;/usr/local/opt/qt"
|
||||||
|
- CMAKE_EXTRA_ARGS=""
|
||||||
|
- DEPLOY_FILE="Chiaki.dmg"
|
||||||
|
after_success:
|
||||||
|
- cd ..
|
||||||
|
- cp -a build/gui/chiaki.app Chiaki.app
|
||||||
|
- /usr/local/opt/qt/bin/macdeployqt Chiaki.app -dmg
|
||||||
|
|
||||||
|
- name: "Source Package"
|
||||||
|
os: linux
|
||||||
|
dist: bionic
|
||||||
|
install: ~
|
||||||
|
script:
|
||||||
|
- find . -name ".git*" | xargs rm -rfv
|
||||||
|
- mkdir chiaki && shopt -s extglob && mv !(chiaki) chiaki
|
||||||
|
- tar -czvf "$DEPLOY_FILE" chiaki
|
||||||
|
env:
|
||||||
|
- DEPLOY_FILE="chiaki-src.tar.gz"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- scripts/build-ffmpeg.sh
|
||||||
|
|
||||||
|
script:
|
||||||
|
- mkdir build && cd build
|
||||||
|
- cmake
|
||||||
|
-DCMAKE_BUILD_TYPE=Release
|
||||||
|
-DCMAKE_VERBOSE_MAKEFILE=ON
|
||||||
|
-DCMAKE_PREFIX_PATH=$CMAKE_PREFIX_PATH
|
||||||
|
-DCHIAKI_ENABLE_TESTS=ON
|
||||||
|
-DCHIAKI_ENABLE_CLI=OFF
|
||||||
|
-DCHIAKI_GUI_ENABLE_QT_GAMEPAD=OFF
|
||||||
|
-DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON
|
||||||
|
$CMAKE_EXTRA_ARGS
|
||||||
|
..
|
||||||
|
- make -j4
|
||||||
|
- test/chiaki-unit
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
skip_cleanup: true
|
||||||
|
provider: releases
|
||||||
|
api_key:
|
||||||
|
secure: R7RjLOuGFda05EJeNX2lNG135xKU2w9IQn7p1H1P2zw4zlQMgSBpNRaW8hE408x5KJUjptJTF6QaYYmPWbHlf9VEPFVIcVzSp8YSd2Cdr+GKhmFgWF+fJPBj5y9NNqohwxvK3Nrugh0v6yVQiEYEGF7WArU6dvymSNNTw/EqXtfrOvwUgSf1bDAzQAsXn3E6Ptzf9DrQU8+mOgMSqT/3Wy5207KLmWTtwBWDgkskKwS9OEXk3tDd6U4uT7NFHHmcw+ZjQXRD+yHSHUWYs1oKR4IfgPFxQfEK0Txhkxdf3yj1aNweuk7GGC3cfRaarUfRQpoYqYYCxhTfGZ2b4rVgX3XpssMY7ZmSZHRi/SX08ETXF/c7PZGzr0RPFXZLgAGjgN6O2Dbb9agc3tOUGDUuqKEWX9sALm82WS0FRAFrFLENgMFsj5hu+DKIIkAU2yEsadYKjjhC+q+mTAEkxKKknvM50Xpx3tE1TlP/31Z53v4/NydHIHXPJ72V3mnuoTacwhG2SkGtjMbLCnEZDCtu9C4556oa7Z29cqafv90ZD7lTQMV+ijKvjxgOC9u1GeemmZLofRGDFyYSqKxOpYxxxXGOhs+7FMAdKP00h++MTLwRwIebKQs0fW0XiNKmwushWOUU8sXI1jxTbwe9dPQsspxHRv/mVo6l2vUcBjC19K0=
|
||||||
|
file: $DEPLOY_FILE
|
||||||
|
on:
|
||||||
|
tags: true
|
180
CMakeLists.txt
180
CMakeLists.txt
|
@ -3,199 +3,31 @@ cmake_minimum_required(VERSION 3.2)
|
||||||
|
|
||||||
project(chiaki)
|
project(chiaki)
|
||||||
|
|
||||||
# Like option(), but the value can also be AUTO
|
|
||||||
macro(tri_option name desc default)
|
|
||||||
set("${name}" "${default}" CACHE STRING "${desc}")
|
|
||||||
set_property(CACHE "${name}" PROPERTY STRINGS AUTO ON OFF)
|
|
||||||
endmacro()
|
|
||||||
|
|
||||||
option(CHIAKI_ENABLE_TESTS "Enable tests for Chiaki" ON)
|
option(CHIAKI_ENABLE_TESTS "Enable tests for Chiaki" ON)
|
||||||
option(CHIAKI_ENABLE_CLI "Enable CLI for Chiaki" OFF)
|
option(CHIAKI_ENABLE_CLI "Enable CLI for Chiaki" OFF)
|
||||||
option(CHIAKI_ENABLE_GUI "Enable Qt GUI" ON)
|
option(CHIAKI_GUI_ENABLE_QT_GAMEPAD "Use QtGamepad for Input" OFF)
|
||||||
option(CHIAKI_ENABLE_ANDROID "Enable Android (Use only as part of the Gradle Project)" OFF)
|
|
||||||
option(CHIAKI_ENABLE_BOREALIS "Enable Borealis GUI (For Nintendo Switch or PC)" OFF)
|
|
||||||
tri_option(CHIAKI_ENABLE_SETSU "Enable libsetsu for touchpad input from controller" AUTO)
|
|
||||||
option(CHIAKI_LIB_ENABLE_OPUS "Use Opus as part of Chiaki Lib" ON)
|
|
||||||
if(CHIAKI_ENABLE_GUI OR CHIAKI_ENABLE_BOREALIS)
|
|
||||||
set(CHIAKI_FFMPEG_DEFAULT ON)
|
|
||||||
else()
|
|
||||||
set(CHIAKI_FFMPEG_DEFAULT AUTO)
|
|
||||||
endif()
|
|
||||||
tri_option(CHIAKI_ENABLE_FFMPEG_DECODER "Enable FFMPEG video decoder" ${CHIAKI_FFMPEG_DEFAULT})
|
|
||||||
tri_option(CHIAKI_ENABLE_PI_DECODER "Enable Raspberry Pi-specific video decoder (requires libraspberrypi0 and libraspberrypi-doc)" AUTO)
|
|
||||||
option(CHIAKI_LIB_ENABLE_MBEDTLS "Use mbedtls instead of OpenSSL as part of Chiaki Lib" OFF)
|
|
||||||
option(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT "Fetch Mbed TLS instead of using system-provided libs" OFF)
|
|
||||||
option(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT "Use OpenSSL as CMake external project" OFF)
|
|
||||||
option(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER "Use SDL Gamecontroller for Input" ON)
|
option(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER "Use SDL Gamecontroller for Input" ON)
|
||||||
option(CHIAKI_CLI_ARGP_STANDALONE "Search for standalone argp lib for CLI" OFF)
|
|
||||||
tri_option(CHIAKI_USE_SYSTEM_JERASURE "Use system-provided jerasure instead of submodule" AUTO)
|
|
||||||
tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submodule" AUTO)
|
|
||||||
|
|
||||||
set(CHIAKI_VERSION_MAJOR 2)
|
set(CHIAKI_VERSION_MAJOR 1)
|
||||||
set(CHIAKI_VERSION_MINOR 2)
|
set(CHIAKI_VERSION_MINOR 0)
|
||||||
set(CHIAKI_VERSION_PATCH 0)
|
set(CHIAKI_VERSION_PATCH 2)
|
||||||
set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH})
|
set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH})
|
||||||
|
|
||||||
set(CPACK_PACKAGE_NAME "chiaki")
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||||
set(CPACK_PACKAGE_DESCRIPTION "Open Source PS4 remote play client")
|
|
||||||
set(CPACK_PACKAGE_VERSION_MAJOR ${CHIAKI_VERSION_MAJOR})
|
|
||||||
set(CPACK_PACKAGE_VERSION_MINOR ${CHIAKI_VERSION_MINOR})
|
|
||||||
set(CPACK_PACKAGE_VERSION_PATCH ${CHIAKI_VERSION_PATCH})
|
|
||||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
|
||||||
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION ${CPACK_PACKAGE_DESCRIPTION})
|
|
||||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
|
||||||
include(CPack)
|
|
||||||
|
|
||||||
set(CHIAKI_IS_SWITCH ${NSWITCH})
|
|
||||||
|
|
||||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_CURRENT_SOURCE_DIR}/setsu/cmake")
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 11)
|
|
||||||
|
|
||||||
if(CHIAKI_IS_SWITCH)
|
|
||||||
# force mbedtls as crypto lib
|
|
||||||
set(CHIAKI_LIB_ENABLE_MBEDTLS ON)
|
|
||||||
add_definitions(-D__SWITCH__)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_USE_SYSTEM_JERASURE)
|
|
||||||
if(CHIAKI_USE_SYSTEM_JERASURE STREQUAL AUTO)
|
|
||||||
find_package(Jerasure QUIET)
|
|
||||||
set(CHIAKI_USE_SYSTEM_JERASURE ${Jerasure_FOUND})
|
|
||||||
else()
|
|
||||||
find_package(Jerasure REQUIRED)
|
|
||||||
set(CHIAKI_USE_SYSTEM_JERASURE ON)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
find_package(PythonInterp 3 REQUIRED) # Make sure nanopb doesn't find Python 2.7 because Python 2 should just die.
|
|
||||||
|
|
||||||
if(CHIAKI_USE_SYSTEM_NANOPB)
|
|
||||||
if(CHIAKI_USE_SYSTEM_NANOPB STREQUAL AUTO)
|
|
||||||
find_package(Nanopb QUIET)
|
|
||||||
set(CHIAKI_USE_SYSTEM_NANOPB ${Nanopb_FOUND})
|
|
||||||
else()
|
|
||||||
find_package(Nanopb REQUIRED)
|
|
||||||
set(CHIAKI_USE_SYSTEM_NANOPB ON)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_subdirectory(third-party)
|
add_subdirectory(third-party)
|
||||||
|
|
||||||
add_definitions(-DCHIAKI_VERSION_MAJOR=${CHIAKI_VERSION_MAJOR} -DCHIAKI_VERSION_MINOR=${CHIAKI_VERSION_MINOR} -DCHIAKI_VERSION_PATCH=${CHIAKI_VERSION_PATCH} -DCHIAKI_VERSION=\"${CHIAKI_VERSION}\")
|
add_definitions(-DCHIAKI_VERSION_MAJOR=${CHIAKI_VERSION_MAJOR} -DCHIAKI_VERSION_MINOR=${CHIAKI_VERSION_MINOR} -DCHIAKI_VERSION_PATCH=${CHIAKI_VERSION_PATCH} -DCHIAKI_VERSION=\"${CHIAKI_VERSION}\")
|
||||||
|
|
||||||
if(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT)
|
|
||||||
include(OpenSSLExternalProject)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_LIB_ENABLE_MBEDTLS)
|
|
||||||
add_definitions(-DCHIAKI_LIB_ENABLE_MBEDTLS)
|
|
||||||
if(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT)
|
|
||||||
set(FETCHCONTENT_QUIET CACHE BOOL FALSE)
|
|
||||||
include(FetchContent)
|
|
||||||
set(ENABLE_TESTING CACHE INTERNAL OFF)
|
|
||||||
set(ENABLE_PROGRAMS CACHE INTERNAL OFF)
|
|
||||||
set(USE_SHARED_MBEDTLS_LIBRARY CACHE INTERNAL OFF)
|
|
||||||
FetchContent_Declare(
|
|
||||||
mbedtls
|
|
||||||
GIT_REPOSITORY https://github.com/Mbed-TLS/mbedtls.git
|
|
||||||
GIT_TAG 8b3f26a5ac38d4fdccbc5c5366229f3e01dafcc0 # v2.28.0
|
|
||||||
GIT_PROGRESS TRUE
|
|
||||||
)
|
|
||||||
FetchContent_MakeAvailable(mbedtls)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_FFMPEG_DECODER)
|
|
||||||
find_package(FFMPEG COMPONENTS avcodec avutil)
|
|
||||||
if(FFMPEG_FOUND)
|
|
||||||
set(CHIAKI_ENABLE_FFMPEG_DECODER ON)
|
|
||||||
else()
|
|
||||||
if(NOT CHIAKI_ENABLE_FFMPEG_DECODER STREQUAL AUTO)
|
|
||||||
message(FATAL_ERROR "CHIAKI_ENABLE_FFMPEG_DECODER is set to ON, but ffmpeg could not be found.")
|
|
||||||
endif()
|
|
||||||
set(CHIAKI_ENABLE_FFMPEG_DECODER OFF)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_FFMPEG_DECODER)
|
|
||||||
message(STATUS "FFMPEG Decoder enabled")
|
|
||||||
else()
|
|
||||||
message(STATUS "FFMPEG Decoder disabled")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_PI_DECODER)
|
|
||||||
find_package(ILClient)
|
|
||||||
if(ILClient_FOUND)
|
|
||||||
set(CHIAKI_ENABLE_PI_DECODER ON)
|
|
||||||
else()
|
|
||||||
if(NOT CHIAKI_ENABLE_PI_DECODER STREQUAL AUTO)
|
|
||||||
message(FATAL_ERROR "
|
|
||||||
CHIAKI_ENABLE_PI_DECODER is set to ON, but its dependencies (ilclient source and libs) could not be resolved.
|
|
||||||
The Raspberry Pi Decoder is only supported on Raspberry Pi OS and requires libraspberrypi0 and libraspberrypi-doc.")
|
|
||||||
endif()
|
|
||||||
set(CHIAKI_ENABLE_PI_DECODER OFF)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_PI_DECODER)
|
|
||||||
message(STATUS "Pi Decoder enabled")
|
|
||||||
else()
|
|
||||||
message(STATUS "Pi Decoder disabled")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_subdirectory(lib)
|
add_subdirectory(lib)
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_CLI)
|
if(CHIAKI_ENABLE_CLI)
|
||||||
add_subdirectory(cli)
|
add_subdirectory(cli)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_GUI AND CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER)
|
add_subdirectory(gui)
|
||||||
find_package(SDL2 MODULE REQUIRED)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_SETSU)
|
|
||||||
if(CHIAKI_ENABLE_SETSU STREQUAL AUTO AND SDL2_FOUND AND (SDL2_VERSION_MINOR GREATER 0 OR SDL2_VERSION_PATCH GREATER_EQUAL 14))
|
|
||||||
message(STATUS "SDL version ${SDL2_VERSION} is >= 2.0.14, disabling Setsu")
|
|
||||||
set(CHIAKI_ENABLE_SETSU OFF)
|
|
||||||
else()
|
|
||||||
find_package(Udev QUIET)
|
|
||||||
find_package(Evdev QUIET)
|
|
||||||
if(Udev_FOUND AND Evdev_FOUND)
|
|
||||||
set(CHIAKI_ENABLE_SETSU ON)
|
|
||||||
else()
|
|
||||||
if(NOT CHIAKI_ENABLE_SETSU STREQUAL AUTO)
|
|
||||||
message(FATAL_ERROR "
|
|
||||||
CHIAKI_ENABLE_SETSU is set to ON, but its dependencies (udev and evdev) could not be resolved.
|
|
||||||
Keep in mind that setsu is only supported on Linux!")
|
|
||||||
endif()
|
|
||||||
set(CHIAKI_ENABLE_SETSU OFF)
|
|
||||||
endif()
|
|
||||||
if(CHIAKI_ENABLE_SETSU)
|
|
||||||
add_subdirectory(setsu)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_SETSU)
|
|
||||||
message(STATUS "Setsu enabled")
|
|
||||||
else()
|
|
||||||
message(STATUS "Setsu disabled")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_GUI)
|
|
||||||
add_subdirectory(gui)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_TESTS)
|
if(CHIAKI_ENABLE_TESTS)
|
||||||
enable_testing()
|
enable_testing()
|
||||||
add_subdirectory(test)
|
add_subdirectory(test)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_ANDROID)
|
|
||||||
add_subdirectory(android/app)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CHIAKI_ENABLE_BOREALIS)
|
|
||||||
add_subdirectory(switch)
|
|
||||||
endif()
|
|
||||||
|
|
150
COPYING
150
COPYING
|
@ -1,5 +1,5 @@
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
@ -7,15 +7,17 @@
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
The GNU General Public License is a free, copyleft license for
|
||||||
software and other kinds of works, specifically designed to ensure
|
software and other kinds of works.
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
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
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users.
|
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
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
@ -24,34 +26,44 @@ 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
|
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.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
To protect your rights, we need to prevent others from denying you
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
you this License which gives you legal permission to copy, distribute
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
and/or modify the software.
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
For example, if you distribute copies of such a program, whether
|
||||||
improvements made in alternate versions of the program, if they
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
receive widespread use, become available for other developers to
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
incorporate. Many developers of free software are heartened and
|
or can get the source code. And you must show them these terms so they
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
know their rights.
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
ensure that, in such cases, the modified source code becomes available
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
to the community. It requires the operator of a network server to
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
that there is no warranty for this free software. For both users' and
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
changed, so that their problems will not be attributed erroneously to
|
||||||
this license.
|
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
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
|
@ -60,7 +72,7 @@ modification follow.
|
||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
|
@ -537,45 +549,35 @@ 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
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU General Public License into a single
|
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
|
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,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the work with which it is combined will remain governed by version
|
but the special requirements of the GNU Affero General Public License,
|
||||||
3 of the GNU General Public License.
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
the GNU General Public License from time to time. Such new versions will
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
Program specifies that a certain numbered version of the GNU General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
GNU General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
@ -633,40 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
If the program does terminal interaction, make it output a short
|
||||||
network, you should also make sure that it provides a way for users to
|
notice like this when it starts in an interactive mode:
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
<program> Copyright (C) <year> <name of author>
|
||||||
of the code. There are many ways you could offer source, and different
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
solutions will be better for different programs; see section 13 for the
|
This is free software, and you are welcome to redistribute it
|
||||||
specific requirements.
|
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,
|
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.
|
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 AGPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Additional permission under GNU AGPL version 3 section 7
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
If you modify this program, or any covered work, by linking or
|
may consider it more useful to permit linking proprietary applications with
|
||||||
combining it with the OpenSSL project's OpenSSL library (or a
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
modified version of that library), containing parts covered by the
|
Public License instead of this License. But first, please read
|
||||||
terms of the OpenSSL or SSLeay licenses, the Free Software Foundation
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
grants you additional permission to convey the resulting work.
|
|
||||||
Corresponding Source for a non-source form of such a combination
|
|
||||||
shall include the source code for the parts of OpenSSL used as well
|
|
||||||
as that of the covered work.
|
|
||||||
|
|
|
@ -1,672 +0,0 @@
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 19 November 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 Affero General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
our General Public Licenses are 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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
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 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 work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero 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 your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
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 AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Additional permission under GNU AGPL version 3 section 7
|
|
||||||
|
|
||||||
If you modify this program, or any covered work, by linking or
|
|
||||||
combining it with the OpenSSL project's OpenSSL library (or a
|
|
||||||
modified version of that library), containing parts covered by the
|
|
||||||
terms of the OpenSSL or SSLeay licenses, the Free Software Foundation
|
|
||||||
grants you additional permission to convey the resulting work.
|
|
||||||
Corresponding Source for a non-source form of such a combination
|
|
||||||
shall include the source code for the parts of OpenSSL used as well
|
|
||||||
as that of the covered work.
|
|
100
README.md
100
README.md
|
@ -5,110 +5,68 @@
|
||||||
|
|
||||||
**Disclaimer:** This project is not endorsed or certified by Sony Interactive Entertainment LLC.
|
**Disclaimer:** This project is not endorsed or certified by Sony Interactive Entertainment LLC.
|
||||||
|
|
||||||
[](https://ci.appveyor.com/project/thestr4ng3r/chiaki) [](https://builds.sr.ht/~thestr4ng3r/chiaki?)
|
[](https://travis-ci.com/thestr4ng3r/chiaki) [](https://ci.appveyor.com/project/thestr4ng3r/chiaki)
|
||||||
|
|
||||||
Chiaki is a Free and Open Source Software Client for PlayStation 4 and PlayStation 5 Remote Play
|
Chiaki is a Free and Open Source Software Client for PlayStation 4 Remote Play
|
||||||
for Linux, FreeBSD, OpenBSD, NetBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms.
|
for Linux, macOS, Windows and potentially even more platforms.
|
||||||
|
|
||||||
## Project Status and Contributing
|
|
||||||
|
|
||||||
As all relevant features are implemented, this project is considered to be finished and in maintenance mode only.
|
|
||||||
No major updates are planned and contributions are only accepted in special cases such as security issues.
|
|
||||||
The objective is to keep a stable base and not break existing support for less mainstream platforms such as BSDs.
|
|
||||||
|
|
||||||
**For a more active, fast moving and community-oriented project, refer
|
|
||||||
to [chiaki-ng](https://streetpea.github.io/chiaki-ng/) ("next generation").
|
|
||||||
If you would like to contribute, this will likely also be the best place to do so.**
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installing
|
## Features
|
||||||
|
|
||||||
You can either download a pre-built release or build Chiaki from source.
|
Everything necessary for a full streaming session, including the initial
|
||||||
|
registration and wakeup of the console, is supported.
|
||||||
|
The following features however are yet to be implemented:
|
||||||
|
* Congestion Control
|
||||||
|
* H264 Error Concealment (FEC and active error recovery however are implemented)
|
||||||
|
* Make the console send in higher Bitrate (if possible)
|
||||||
|
* Touchpad support (Triggering the Touchpad Button is currently possible by pressing `T` on the keyboard)
|
||||||
|
* Configurable Keybindings
|
||||||
|
|
||||||
### Downloading a Release
|
## Downloading a Release
|
||||||
|
|
||||||
Builds are provided for Linux, Android, macOS, Nintendo Switch and Windows.
|
Builds are provided for Linux, macOS and Windows. You can find them [here](https://github.com/thestr4ng3r/chiaki/releases).
|
||||||
|
|
||||||
You can download them [here](https://git.sr.ht/~thestr4ng3r/chiaki/refs).
|
|
||||||
|
|
||||||
* **Linux**: The provided file is an [AppImage](https://appimage.org/). Simply make it executable (`chmod +x <file>.AppImage`) and run it.
|
* **Linux**: The provided file is an [AppImage](https://appimage.org/). Simply make it executable (`chmod +x <file>.AppImage`) and run it.
|
||||||
* **Android**: Install from [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from Sourcehut.
|
|
||||||
* **macOS**: Drag the application from the `.dmg` into your Applications folder.
|
* **macOS**: Drag the application from the `.dmg` into your Applications folder.
|
||||||
* **Windows**: Extract the `.zip` file and execute `chiaki.exe`.
|
* **Windows**: Extract the `.zip` file and execute `chiaki.exe`.
|
||||||
* **Switch**: Download the `.nro` file and copy it into the `switch/` directory on your SD card.
|
|
||||||
|
|
||||||
### Building from Source
|
## Building from Source
|
||||||
|
|
||||||
Dependencies are CMake, Qt 5 with QtMultimedia, QtOpenGL and QtSvg, FFMPEG (libavcodec with H264 is enough), libopus, OpenSSL 1.1, SDL 2,
|
Dependencies are CMake, Qt 5 with QtMultimedia, QtOpenGL and QtSvg, FFMPEG (libavcodec with H264 is enough), libopus, OpenSSL 1.1,
|
||||||
protoc and the protobuf Python library (only used during compilation for Nanopb). Then, Chiaki builds just like any other CMake project:
|
protoc and the protobuf Python library (only used during compilation for Nanopb).
|
||||||
|
Then, Chiaki builds just like any other CMake project:
|
||||||
```
|
```
|
||||||
git submodule update --init
|
|
||||||
mkdir build && cd build
|
mkdir build && cd build
|
||||||
cmake ..
|
cmake ..
|
||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
For more detailed platform-specific instructions, see [doc/platform-build.md](doc/platform-build.md) or [switch/](./switch/README.md) for Nintendo Switch.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
If your Console is on your local network, is turned on or in standby mode and does not have Discovery explicitly disabled, Chiaki should find it.
|
|
||||||
Otherwise, you can add it manually.
|
|
||||||
To do so, click the "+" icon in the top right, and enter your Console's IP address.
|
|
||||||
|
|
||||||
You will then need to register your Console with Chiaki. You will need two more pieces of information to do this.
|
|
||||||
|
|
||||||
### Obtaining your PSN AccountID
|
|
||||||
|
|
||||||
Starting with PS4 7.0, it is necessary to use a so-called "AccountID" as opposed to the "Online-ID" for registration (streaming itself did not change).
|
|
||||||
This ID seems to be a unique identifier for a PSN Account and it can be obtained from the PSN after logging in using OAuth.
|
|
||||||
A Python 3 script which does this is provided in [scripts/psn-account-id.py](scripts/psn-account-id.py).
|
|
||||||
Simply run it in a terminal and follow the instructions. Once you know your ID, write it down. You will likely never have to do this process again.
|
|
||||||
|
|
||||||
### Obtaining a Registration PIN
|
|
||||||
|
|
||||||
To register a Console with a PIN, it must be put into registration mode. To do this on a PS4, simply go to:
|
|
||||||
Settings -> Remote Play -> Add Device, or on a PS5: Settings -> System -> Remote Play -> Link Device.
|
|
||||||
|
|
||||||
You can now double-click your Console in Chiaki's main window to start Remote Play.
|
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
This project has only been made possible because of the following Open Source projects:
|
This project has only been made possible because of the following Open Source projects:
|
||||||
[Rizin](https://rizin.re),
|
[radare2](https://github.com/radare/radare2),
|
||||||
[Cutter](https://cutter.re),
|
[Cutter](https://cutter.re/),
|
||||||
[Frida](https://www.frida.re) and
|
[Frida](https://www.frida.re/) and
|
||||||
[x64dbg](https://x64dbg.com).
|
[x64dbg](https://x64dbg.com/).
|
||||||
|
|
||||||
Also thanks to [delroth](https://github.com/delroth) for analyzing the registration and wakeup protocol,
|
Also thanks to [delroth](https://github.com/delroth) for analyzing the registration and wakeup protocol,
|
||||||
[grill2010](https://github.com/grill2010) for analyzing the PSN's OAuth Login,
|
|
||||||
as well as a huge thank you to [FioraAeterna](https://github.com/FioraAeterna) for giving me some
|
as well as a huge thank you to [FioraAeterna](https://github.com/FioraAeterna) for giving me some
|
||||||
extremely helpful information about FEC and error correction.
|
extremely helpful information about FEC and error correction.
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
Created by Florian Märkl
|
Created by Florian Märkl.
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License version 3
|
it under the terms of the GNU General Public License as published by
|
||||||
as published by the Free Software Foundation.
|
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,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Additional permission under GNU AGPL version 3 section 7
|
|
||||||
|
|
||||||
If you modify this program, or any covered work, by linking or
|
|
||||||
combining it with the OpenSSL project's OpenSSL library (or a
|
|
||||||
modified version of that library), containing parts covered by the
|
|
||||||
terms of the OpenSSL or SSLeay licenses, the Free Software Foundation
|
|
||||||
grants you additional permission to convey the resulting work.
|
|
||||||
Corresponding Source for a non-source form of such a combination
|
|
||||||
shall include the source code for the parts of OpenSSL used as well
|
|
||||||
as that of the covered work.
|
|
16
android/.gitignore
vendored
16
android/.gitignore
vendored
|
@ -1,16 +0,0 @@
|
||||||
*.iml
|
|
||||||
.gradle
|
|
||||||
/local.properties
|
|
||||||
/.idea/caches
|
|
||||||
/.idea/libraries
|
|
||||||
/.idea/modules.xml
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/navEditor.xml
|
|
||||||
/.idea/assetWizardSettings.xml
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
/captures
|
|
||||||
.externalNativeBuild
|
|
||||||
.cxx
|
|
||||||
/app/release
|
|
||||||
keystore.jks
|
|
1
android/app/.gitignore
vendored
1
android/app/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
/build
|
|
|
@ -1,25 +0,0 @@
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.2)
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 14)
|
|
||||||
|
|
||||||
add_library(chiaki-jni SHARED
|
|
||||||
src/main/cpp/chiaki-jni.c
|
|
||||||
src/main/cpp/log.h
|
|
||||||
src/main/cpp/log.c
|
|
||||||
src/main/cpp/video-decoder.h
|
|
||||||
src/main/cpp/video-decoder.c
|
|
||||||
src/main/cpp/audio-decoder.h
|
|
||||||
src/main/cpp/audio-decoder.c
|
|
||||||
src/main/cpp/audio-output.h
|
|
||||||
src/main/cpp/audio-output.cpp
|
|
||||||
src/main/cpp/circular-fifo.hpp)
|
|
||||||
target_link_libraries(chiaki-jni chiaki-lib)
|
|
||||||
|
|
||||||
find_library(ANDROID_LIB_LOG log)
|
|
||||||
find_library(ANDROID_LIB_MEDIANDK mediandk)
|
|
||||||
find_library(ANDROID_LIB_ANDROID android)
|
|
||||||
target_link_libraries(chiaki-jni "${ANDROID_LIB_LOG}" "${ANDROID_LIB_MEDIANDK}" "${ANDROID_LIB_ANDROID}")
|
|
||||||
|
|
||||||
add_subdirectory(src/main/cpp/oboe)
|
|
||||||
target_link_libraries(chiaki-jni oboe)
|
|
|
@ -1,116 +0,0 @@
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply plugin: 'kotlin-parcelize'
|
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
|
|
||||||
def rootCMakeLists = "../../CMakeLists.txt"
|
|
||||||
def rootCMakeListsContent = file(rootCMakeLists).text
|
|
||||||
static def grepVersionComponent(content, varname) {
|
|
||||||
def match = content =~ /set\($varname ([0-9]+)\)/
|
|
||||||
if(!match.find())
|
|
||||||
throw new GradleException("Failed to find $varname in CMakeLists.txt")
|
|
||||||
return match.group(1)
|
|
||||||
}
|
|
||||||
def chiakiVersionMajor = grepVersionComponent(rootCMakeListsContent, "CHIAKI_VERSION_MAJOR")
|
|
||||||
def chiakiVersionMinor = grepVersionComponent(rootCMakeListsContent, "CHIAKI_VERSION_MINOR")
|
|
||||||
def chiakiVersionPatch = grepVersionComponent(rootCMakeListsContent, "CHIAKI_VERSION_PATCH")
|
|
||||||
def chiakiVersion = "$chiakiVersionMajor.$chiakiVersionMinor.$chiakiVersionPatch"
|
|
||||||
println("Determined Chiaki Version: $chiakiVersion")
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 33
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.metallic.chiaki"
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion 33
|
|
||||||
versionCode 12
|
|
||||||
versionName chiakiVersion
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
arguments "-DCHIAKI_ENABLE_TESTS=OFF",
|
|
||||||
"-DCHIAKI_ENABLE_CLI=OFF",
|
|
||||||
"-DCHIAKI_ENABLE_GUI=OFF",
|
|
||||||
"-DCHIAKI_ENABLE_SETSU=OFF",
|
|
||||||
"-DCHIAKI_ENABLE_ANDROID=ON",
|
|
||||||
"-DCHIAKI_ENABLE_FFMPEG_DECODER=OFF",
|
|
||||||
"-DCHIAKI_ENABLE_PI_DECODER=OFF",
|
|
||||||
"-DCHIAKI_LIB_ENABLE_OPUS=OFF",
|
|
||||||
"-DCHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT=ON"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
}
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
version "3.22.1"
|
|
||||||
path rootCMakeLists
|
|
||||||
}
|
|
||||||
}
|
|
||||||
signingConfigs {
|
|
||||||
release
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Properties properties = new Properties()
|
|
||||||
def propertiesFile = file("../local.properties")
|
|
||||||
if (propertiesFile.exists()) {
|
|
||||||
properties.load(propertiesFile.newDataInputStream())
|
|
||||||
}
|
|
||||||
|
|
||||||
if(properties.containsKey("chiakiKeystore")) {
|
|
||||||
println("Enabling Local Signing")
|
|
||||||
buildTypes.release.signingConfig = signingConfigs.release
|
|
||||||
buildTypes.debug.signingConfig = signingConfigs.release
|
|
||||||
signingConfigs.release.storeFile = file(properties.get("chiakiKeystore"))
|
|
||||||
signingConfigs.release.storePassword = properties.get("chiakiKeystorePW")
|
|
||||||
signingConfigs.release.keyAlias = properties.get("chiakiKeyAlias")
|
|
||||||
signingConfigs.release.keyPassword = properties.get("chiakiKeyPW")
|
|
||||||
} else {
|
|
||||||
println("Signing not enabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
|
||||||
kotlinOptions {
|
|
||||||
freeCompilerArgs += "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.0'
|
|
||||||
implementation 'androidx.core:core-ktx:1.9.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
|
||||||
implementation 'androidx.preference:preference:1.2.0'
|
|
||||||
implementation 'com.google.android.material:material:1.8.0'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.5.1'
|
|
||||||
implementation "io.reactivex.rxjava2:rxjava:2.2.20"
|
|
||||||
implementation "io.reactivex.rxjava2:rxkotlin:2.4.0"
|
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
|
||||||
def room_version = "2.5.0"
|
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
|
||||||
implementation "androidx.room:room-ktx:$room_version"
|
|
||||||
implementation "androidx.room:room-rxjava2:$room_version"
|
|
||||||
implementation "com.squareup.moshi:moshi:1.14.0"
|
|
||||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.14.0"
|
|
||||||
}
|
|
91
android/app/proguard-rules.pro
vendored
91
android/app/proguard-rules.pro
vendored
|
@ -1,91 +0,0 @@
|
||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
|
|
||||||
-dontobfuscate
|
|
||||||
-keep class com.metallic.chiaki.** { *; }
|
|
||||||
|
|
||||||
|
|
||||||
##########################################
|
|
||||||
# Moshi
|
|
||||||
##########################################
|
|
||||||
|
|
||||||
# JSR 305 annotations are for embedding nullability information.
|
|
||||||
-dontwarn javax.annotation.**
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@com.squareup.moshi.* <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep @com.squareup.moshi.JsonQualifier interface *
|
|
||||||
|
|
||||||
# Enum field names are used by the integrated EnumJsonAdapter.
|
|
||||||
# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly
|
|
||||||
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
|
|
||||||
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
|
|
||||||
<fields>;
|
|
||||||
**[] values();
|
|
||||||
}
|
|
||||||
|
|
||||||
# The name of @JsonClass types is used to look up the generated adapter.
|
|
||||||
-keepnames @com.squareup.moshi.JsonClass class *
|
|
||||||
|
|
||||||
# Retain generated target class's synthetic defaults constructor and keep DefaultConstructorMarker's
|
|
||||||
# name. We will look this up reflectively to invoke the type's constructor.
|
|
||||||
#
|
|
||||||
# We can't _just_ keep the defaults constructor because Proguard/R8's spec doesn't allow wildcard
|
|
||||||
# matching preceding parameters.
|
|
||||||
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
|
|
||||||
-keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
|
|
||||||
synthetic <init>(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
# Retain generated JsonAdapters if annotated type is retained.
|
|
||||||
-if @com.squareup.moshi.JsonClass class *
|
|
||||||
-keep class <1>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
-if @com.squareup.moshi.JsonClass class **$*
|
|
||||||
-keep class <1>_<2>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
-if @com.squareup.moshi.JsonClass class **$*$*
|
|
||||||
-keep class <1>_<2>_<3>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
-if @com.squareup.moshi.JsonClass class **$*$*$*
|
|
||||||
-keep class <1>_<2>_<3>_<4>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
|
|
||||||
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
|
|
||||||
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="com.metallic.chiaki">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
|
||||||
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/AppTheme"
|
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:authorities="com.metallic.chiaki.fileprovider"
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:grantUriPermissions="true"
|
|
||||||
android:exported="false">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/filepaths"
|
|
||||||
/>
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<activity android:name=".main.MainActivity"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".stream.StreamActivity"
|
|
||||||
android:theme="@style/StreamTheme"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
|
||||||
android:screenOrientation="userLandscape"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".settings.SettingsActivity" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".regist.RegistActivity"
|
|
||||||
android:theme="@style/MageTheme"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
|
||||||
android:windowSoftInputMode="adjustResize"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".regist.RegistExecuteActivity"
|
|
||||||
android:theme="@style/MageTheme"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".manualconsole.EditManualConsoleActivity"
|
|
||||||
android:theme="@style/MageTheme"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize" />
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
|
@ -1,205 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#include "audio-decoder.h"
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
|
|
||||||
#include <media/NdkMediaCodec.h>
|
|
||||||
#include <media/NdkMediaFormat.h>
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define INPUT_BUFFER_TIMEOUT_MS 10
|
|
||||||
|
|
||||||
static void *android_chiaki_audio_decoder_output_thread_func(void *user);
|
|
||||||
static void android_chiaki_audio_decoder_header(ChiakiAudioHeader *header, void *user);
|
|
||||||
static void android_chiaki_audio_decoder_frame(uint8_t *buf, size_t buf_size, void *user);
|
|
||||||
|
|
||||||
ChiakiErrorCode android_chiaki_audio_decoder_init(AndroidChiakiAudioDecoder *decoder, ChiakiLog *log)
|
|
||||||
{
|
|
||||||
decoder->log = log;
|
|
||||||
memset(&decoder->audio_header, 0, sizeof(decoder->audio_header));
|
|
||||||
decoder->codec = NULL;
|
|
||||||
decoder->timestamp_cur = 0;
|
|
||||||
|
|
||||||
decoder->cb_user = NULL;
|
|
||||||
decoder->settings_cb = NULL;
|
|
||||||
decoder->frame_cb = NULL;
|
|
||||||
|
|
||||||
return chiaki_mutex_init(&decoder->codec_mutex, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void android_chiaki_audio_decoder_shutdown_codec(AndroidChiakiAudioDecoder *decoder)
|
|
||||||
{
|
|
||||||
chiaki_mutex_lock(&decoder->codec_mutex);
|
|
||||||
ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, -1);
|
|
||||||
if(codec_buf_index >= 0)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGI(decoder->log, "Audio Decoder sending EOS buffer");
|
|
||||||
AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, 0, decoder->timestamp_cur++, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
CHIAKI_LOGE(decoder->log, "Failed to get input buffer for shutting down Audio Decoder!");
|
|
||||||
chiaki_mutex_unlock(&decoder->codec_mutex);
|
|
||||||
chiaki_thread_join(&decoder->output_thread, NULL);
|
|
||||||
AMediaCodec_delete(decoder->codec);
|
|
||||||
decoder->codec = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
void android_chiaki_audio_decoder_fini(AndroidChiakiAudioDecoder *decoder)
|
|
||||||
{
|
|
||||||
if(decoder->codec)
|
|
||||||
android_chiaki_audio_decoder_shutdown_codec(decoder);
|
|
||||||
chiaki_mutex_fini(&decoder->codec_mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void android_chiaki_audio_decoder_get_sink(AndroidChiakiAudioDecoder *decoder, ChiakiAudioSink *sink)
|
|
||||||
{
|
|
||||||
sink->user = decoder;
|
|
||||||
sink->header_cb = android_chiaki_audio_decoder_header;
|
|
||||||
sink->frame_cb = android_chiaki_audio_decoder_frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void *android_chiaki_audio_decoder_output_thread_func(void *user)
|
|
||||||
{
|
|
||||||
AndroidChiakiAudioDecoder *decoder = user;
|
|
||||||
|
|
||||||
while(1)
|
|
||||||
{
|
|
||||||
AMediaCodecBufferInfo info;
|
|
||||||
ssize_t codec_buf_index = AMediaCodec_dequeueOutputBuffer(decoder->codec, &info, -1);
|
|
||||||
if(codec_buf_index >= 0)
|
|
||||||
{
|
|
||||||
if(decoder->settings_cb)
|
|
||||||
{
|
|
||||||
size_t codec_buf_size;
|
|
||||||
uint8_t *codec_buf = AMediaCodec_getOutputBuffer(decoder->codec, (size_t)codec_buf_index, &codec_buf_size);
|
|
||||||
size_t samples_count = info.size / sizeof(int16_t);
|
|
||||||
//CHIAKI_LOGD(decoder->log, "Got %llu samples => %f ms of audio", (unsigned long long)samples_count, 1000.0f * (float)(samples_count / 2) / (float)decoder->audio_header.rate);
|
|
||||||
decoder->frame_cb((int16_t *)codec_buf, samples_count, decoder->cb_user);
|
|
||||||
}
|
|
||||||
AMediaCodec_releaseOutputBuffer(decoder->codec, (size_t)codec_buf_index, false);
|
|
||||||
if(info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGI(decoder->log, "AMediaCodec for Audio Decoder reported EOS");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CHIAKI_LOGI(decoder->log, "Audio Decoder Output Thread exiting");
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void android_chiaki_audio_decoder_header(ChiakiAudioHeader *header, void *user)
|
|
||||||
{
|
|
||||||
AndroidChiakiAudioDecoder *decoder = user;
|
|
||||||
chiaki_mutex_lock(&decoder->codec_mutex);
|
|
||||||
memcpy(&decoder->audio_header, header, sizeof(decoder->audio_header));
|
|
||||||
|
|
||||||
if(decoder->codec)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGI(decoder->log, "Audio decoder already initialized, shutting down the old one");
|
|
||||||
android_chiaki_audio_decoder_shutdown_codec(decoder);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *mime = "audio/opus";
|
|
||||||
decoder->codec = AMediaCodec_createDecoderByType(mime);
|
|
||||||
if(!decoder->codec)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "Failed to create AMediaCodec for mime type %s", mime);
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
AMediaFormat *format = AMediaFormat_new();
|
|
||||||
AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, mime);
|
|
||||||
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, header->channels);
|
|
||||||
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_SAMPLE_RATE, header->rate);
|
|
||||||
// AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_PCM_ENCODING)
|
|
||||||
|
|
||||||
AMediaCodec_configure(decoder->codec, format, NULL, NULL, 0); // TODO: check result
|
|
||||||
AMediaCodec_start(decoder->codec); // TODO: check result
|
|
||||||
|
|
||||||
AMediaFormat_delete(format);
|
|
||||||
|
|
||||||
ChiakiErrorCode err = chiaki_thread_create(&decoder->output_thread, android_chiaki_audio_decoder_output_thread_func, decoder);
|
|
||||||
if(err != CHIAKI_ERR_SUCCESS)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "Failed to create output thread for AMediaCodec");
|
|
||||||
AMediaCodec_delete(decoder->codec);
|
|
||||||
decoder->codec = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t opus_id_head[0x13];
|
|
||||||
memcpy(opus_id_head, "OpusHead", 8);
|
|
||||||
opus_id_head[0x8] = 1; // version
|
|
||||||
opus_id_head[0x9] = header->channels;
|
|
||||||
uint16_t pre_skip = 3840;
|
|
||||||
opus_id_head[0xa] = (uint8_t)(pre_skip & 0xff);
|
|
||||||
opus_id_head[0xb] = (uint8_t)(pre_skip >> 8);
|
|
||||||
opus_id_head[0xc] = (uint8_t)(header->rate & 0xff);
|
|
||||||
opus_id_head[0xd] = (uint8_t)((header->rate >> 0x8) & 0xff);
|
|
||||||
opus_id_head[0xe] = (uint8_t)((header->rate >> 0x10) & 0xff);
|
|
||||||
opus_id_head[0xf] = (uint8_t)(header->rate >> 0x18);
|
|
||||||
uint16_t output_gain = 0;
|
|
||||||
opus_id_head[0x10] = (uint8_t)(output_gain & 0xff);
|
|
||||||
opus_id_head[0x11] = (uint8_t)(output_gain >> 8);
|
|
||||||
opus_id_head[0x12] = 0; // channel map
|
|
||||||
//AMediaFormat_setBuffer(format, AMEDIAFORMAT_KEY_CSD_0, opus_id_head, sizeof(opus_id_head));
|
|
||||||
android_chiaki_audio_decoder_frame(opus_id_head, sizeof(opus_id_head), decoder);
|
|
||||||
|
|
||||||
uint64_t pre_skip_ns = 0;
|
|
||||||
uint8_t csd1[8] = { (uint8_t)(pre_skip_ns & 0xff), (uint8_t)((pre_skip_ns >> 0x8) & 0xff), (uint8_t)((pre_skip_ns >> 0x10) & 0xff), (uint8_t)((pre_skip_ns >> 0x18) & 0xff),
|
|
||||||
(uint8_t)((pre_skip_ns >> 0x20) & 0xff), (uint8_t)((pre_skip_ns >> 0x28) & 0xff), (uint8_t)((pre_skip_ns >> 0x30) & 0xff), (uint8_t)(pre_skip_ns >> 0x38)};
|
|
||||||
android_chiaki_audio_decoder_frame(csd1, sizeof(csd1), decoder);
|
|
||||||
|
|
||||||
uint64_t pre_roll_ns = 0;
|
|
||||||
uint8_t csd2[8] = { (uint8_t)(pre_roll_ns & 0xff), (uint8_t)((pre_roll_ns >> 0x8) & 0xff), (uint8_t)((pre_roll_ns >> 0x10) & 0xff), (uint8_t)((pre_roll_ns >> 0x18) & 0xff),
|
|
||||||
(uint8_t)((pre_roll_ns >> 0x20) & 0xff), (uint8_t)((pre_roll_ns >> 0x28) & 0xff), (uint8_t)((pre_roll_ns >> 0x30) & 0xff), (uint8_t)(pre_roll_ns >> 0x38)};
|
|
||||||
android_chiaki_audio_decoder_frame(csd2, sizeof(csd2), decoder);
|
|
||||||
|
|
||||||
if(decoder->settings_cb)
|
|
||||||
decoder->settings_cb(header->channels, header->rate, decoder->cb_user);
|
|
||||||
|
|
||||||
beach:
|
|
||||||
chiaki_mutex_unlock(&decoder->codec_mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void android_chiaki_audio_decoder_frame(uint8_t *buf, size_t buf_size, void *user)
|
|
||||||
{
|
|
||||||
AndroidChiakiAudioDecoder *decoder = user;
|
|
||||||
chiaki_mutex_lock(&decoder->codec_mutex);
|
|
||||||
|
|
||||||
if(!decoder->codec)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "Received audio data, but decoder is not initialized!");
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
while(buf_size > 0)
|
|
||||||
{
|
|
||||||
ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, INPUT_BUFFER_TIMEOUT_MS * 1000);
|
|
||||||
if(codec_buf_index < 0)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "Failed to get input audio buffer");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t codec_buf_size;
|
|
||||||
uint8_t *codec_buf = AMediaCodec_getInputBuffer(decoder->codec, (size_t)codec_buf_index, &codec_buf_size);
|
|
||||||
size_t codec_sample_size = buf_size;
|
|
||||||
if(codec_sample_size > codec_buf_size)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGD(decoder->log, "Sample is bigger than audio buffer, splitting");
|
|
||||||
codec_sample_size = codec_buf_size;
|
|
||||||
}
|
|
||||||
memcpy(codec_buf, buf, codec_sample_size);
|
|
||||||
AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, codec_sample_size, decoder->timestamp_cur++, 0); // timestamp just raised by 1 for maximum realtime
|
|
||||||
buf += codec_sample_size;
|
|
||||||
buf_size -= codec_sample_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
beach:
|
|
||||||
chiaki_mutex_unlock(&decoder->codec_mutex);
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#ifndef CHIAKI_JNI_AUDIO_DECODER_H
|
|
||||||
#define CHIAKI_JNI_AUDIO_DECODER_H
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
|
|
||||||
#include <chiaki/thread.h>
|
|
||||||
#include <chiaki/log.h>
|
|
||||||
#include <chiaki/audioreceiver.h>
|
|
||||||
|
|
||||||
typedef void (*AndroidChiakiAudioDecoderSettingsCallback)(uint32_t channels, uint32_t rate, void *user);
|
|
||||||
typedef void (*AndroidChiakiAudioDecoderFrameCallback)(int16_t *buf, size_t samples_count, void *user);
|
|
||||||
|
|
||||||
typedef struct android_chiaki_audio_decoder_t
|
|
||||||
{
|
|
||||||
ChiakiLog *log;
|
|
||||||
ChiakiAudioHeader audio_header;
|
|
||||||
|
|
||||||
ChiakiMutex codec_mutex;
|
|
||||||
struct AMediaCodec *codec;
|
|
||||||
uint64_t timestamp_cur;
|
|
||||||
ChiakiThread output_thread;
|
|
||||||
|
|
||||||
AndroidChiakiAudioDecoderSettingsCallback settings_cb;
|
|
||||||
AndroidChiakiAudioDecoderFrameCallback frame_cb;
|
|
||||||
void *cb_user;
|
|
||||||
} AndroidChiakiAudioDecoder;
|
|
||||||
|
|
||||||
ChiakiErrorCode android_chiaki_audio_decoder_init(AndroidChiakiAudioDecoder *decoder, ChiakiLog *log);
|
|
||||||
void android_chiaki_audio_decoder_fini(AndroidChiakiAudioDecoder *decoder);
|
|
||||||
void android_chiaki_audio_decoder_get_sink(AndroidChiakiAudioDecoder *decoder, ChiakiAudioSink *sink);
|
|
||||||
|
|
||||||
static inline void android_chiaki_audio_decoder_set_cb(AndroidChiakiAudioDecoder *decoder, AndroidChiakiAudioDecoderSettingsCallback settings_cb, AndroidChiakiAudioDecoderFrameCallback frame_cb, void *user)
|
|
||||||
{
|
|
||||||
decoder->settings_cb = settings_cb;
|
|
||||||
decoder->frame_cb = frame_cb;
|
|
||||||
decoder->cb_user = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1,124 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#include "audio-output.h"
|
|
||||||
|
|
||||||
#include "circular-buf.hpp"
|
|
||||||
|
|
||||||
#include <chiaki/log.h>
|
|
||||||
#include <chiaki/thread.h>
|
|
||||||
|
|
||||||
#include <oboe/Oboe.h>
|
|
||||||
|
|
||||||
#define BUFFER_CHUNK_SIZE 1024
|
|
||||||
#define BUFFER_CHUNKS_COUNT 32
|
|
||||||
|
|
||||||
using AudioBuffer = CircularBuffer<BUFFER_CHUNKS_COUNT, BUFFER_CHUNK_SIZE>;
|
|
||||||
|
|
||||||
class AudioOutput;
|
|
||||||
|
|
||||||
class AudioOutputCallback: public oboe::AudioStreamCallback
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
AudioOutput *audio_output;
|
|
||||||
|
|
||||||
public:
|
|
||||||
AudioOutputCallback(AudioOutput *audio_output) : audio_output(audio_output) {}
|
|
||||||
oboe::DataCallbackResult onAudioReady(oboe::AudioStream *stream, void *audioData, int32_t numFrames) override;
|
|
||||||
void onErrorBeforeClose(oboe::AudioStream *stream, oboe::Result error) override;
|
|
||||||
void onErrorAfterClose(oboe::AudioStream *stream, oboe::Result error) override;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct AudioOutput
|
|
||||||
{
|
|
||||||
ChiakiLog *log;
|
|
||||||
oboe::ManagedStream stream;
|
|
||||||
AudioOutputCallback stream_callback;
|
|
||||||
AudioBuffer buf;
|
|
||||||
|
|
||||||
AudioOutput() : stream_callback(this) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
extern "C" void *android_chiaki_audio_output_new(ChiakiLog *log)
|
|
||||||
{
|
|
||||||
auto r = new AudioOutput();
|
|
||||||
r->log = log;
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void android_chiaki_audio_output_free(void *audio_output)
|
|
||||||
{
|
|
||||||
if(!audio_output)
|
|
||||||
return;
|
|
||||||
auto ao = reinterpret_cast<AudioOutput *>(audio_output);
|
|
||||||
ao->stream = nullptr;
|
|
||||||
delete ao;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void android_chiaki_audio_output_settings(uint32_t channels, uint32_t rate, void *audio_output)
|
|
||||||
{
|
|
||||||
auto ao = reinterpret_cast<AudioOutput *>(audio_output);
|
|
||||||
|
|
||||||
oboe::AudioStreamBuilder builder;
|
|
||||||
builder.setPerformanceMode(oboe::PerformanceMode::LowLatency)
|
|
||||||
->setSharingMode(oboe::SharingMode::Exclusive)
|
|
||||||
->setFormat(oboe::AudioFormat::I16)
|
|
||||||
->setChannelCount(channels)
|
|
||||||
->setSampleRate(rate)
|
|
||||||
->setCallback(&ao->stream_callback);
|
|
||||||
|
|
||||||
auto result = builder.openManagedStream(ao->stream);
|
|
||||||
if(result == oboe::Result::OK)
|
|
||||||
CHIAKI_LOGI(ao->log, "Audio Output opened Oboe stream");
|
|
||||||
else
|
|
||||||
CHIAKI_LOGE(ao->log, "Audio Output failed to open Oboe stream: %s", oboe::convertToText(result));
|
|
||||||
|
|
||||||
result = ao->stream->start();
|
|
||||||
if(result == oboe::Result::OK)
|
|
||||||
CHIAKI_LOGI(ao->log, "Audio Output started Oboe stream");
|
|
||||||
else
|
|
||||||
CHIAKI_LOGE(ao->log, "Audio Output failed to start Oboe stream: %s", oboe::convertToText(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void android_chiaki_audio_output_frame(int16_t *buf, size_t samples_count, void *audio_output)
|
|
||||||
{
|
|
||||||
auto ao = reinterpret_cast<AudioOutput *>(audio_output);
|
|
||||||
|
|
||||||
size_t buf_size = samples_count * sizeof(int16_t);
|
|
||||||
size_t pushed = ao->buf.Push(reinterpret_cast<uint8_t *>(buf), buf_size);
|
|
||||||
if(pushed < buf_size)
|
|
||||||
CHIAKI_LOGW(ao->log, "Audio Output Buffer Overflow!");
|
|
||||||
}
|
|
||||||
|
|
||||||
oboe::DataCallbackResult AudioOutputCallback::onAudioReady(oboe::AudioStream *stream, void *audio_data, int32_t num_frames)
|
|
||||||
{
|
|
||||||
if(stream->getFormat() != oboe::AudioFormat::I16)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(audio_output->log, "Oboe stream has invalid format in callback");
|
|
||||||
return oboe::DataCallbackResult::Stop;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t bytes_per_frame = stream->getBytesPerFrame();
|
|
||||||
size_t buf_size_requested = static_cast<size_t>(bytes_per_frame * num_frames);
|
|
||||||
auto buf = reinterpret_cast<uint8_t *>(audio_data);
|
|
||||||
|
|
||||||
size_t buf_size_delivered = audio_output->buf.Pop(buf, buf_size_requested);
|
|
||||||
//CHIAKI_LOGW(audio_output->log, "Delivered %llu", (unsigned long long)buf_size_delivered);
|
|
||||||
|
|
||||||
if(buf_size_delivered < buf_size_requested)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGV(audio_output->log, "Audio Output Buffer Underflow!");
|
|
||||||
memset(buf + buf_size_delivered, 0, buf_size_requested - buf_size_delivered);
|
|
||||||
}
|
|
||||||
|
|
||||||
return oboe::DataCallbackResult::Continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioOutputCallback::onErrorBeforeClose(oboe::AudioStream *stream, oboe::Result error)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(audio_output->log, "Oboe reported error before close: %s", oboe::convertToText(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioOutputCallback::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result error)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(audio_output->log, "Oboe reported error after close: %s", oboe::convertToText(error));
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#ifndef CHIAKI_JNI_AUDIO_OUTPUT_H
|
|
||||||
#define CHIAKI_JNI_AUDIO_OUTPUT_H
|
|
||||||
|
|
||||||
#include <chiaki/log.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void *android_chiaki_audio_output_new(ChiakiLog *log);
|
|
||||||
void android_chiaki_audio_output_free(void *audio_output);
|
|
||||||
void android_chiaki_audio_output_settings(uint32_t channels, uint32_t rate, void *audio_output);
|
|
||||||
void android_chiaki_audio_output_frame(int16_t *buf, size_t samples_count, void *audio_output);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif //CHIAKI_JNI_AUDIO_OUTPUT_H
|
|
|
@ -1,834 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
|
|
||||||
#include <android/log.h>
|
|
||||||
|
|
||||||
#include <chiaki/common.h>
|
|
||||||
#include <chiaki/log.h>
|
|
||||||
#include <chiaki/session.h>
|
|
||||||
#include <chiaki/discoveryservice.h>
|
|
||||||
#include <chiaki/regist.h>
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
#include <linux/in.h>
|
|
||||||
#include <linux/in6.h>
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
|
|
||||||
#include "video-decoder.h"
|
|
||||||
#include "audio-decoder.h"
|
|
||||||
#include "audio-output.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include "chiaki-jni.h"
|
|
||||||
|
|
||||||
static char *strdup_jni(const char *str)
|
|
||||||
{
|
|
||||||
if(!str)
|
|
||||||
return NULL;
|
|
||||||
char *r = strdup(str);
|
|
||||||
if(!r)
|
|
||||||
return NULL;
|
|
||||||
for(char *c=r; *c; c++)
|
|
||||||
{
|
|
||||||
if(*c & (1 << 7))
|
|
||||||
*c = '?';
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobject jnistr_from_ascii(JNIEnv *env, const char *str)
|
|
||||||
{
|
|
||||||
if(!str)
|
|
||||||
return NULL;
|
|
||||||
char *s = strdup_jni(str);
|
|
||||||
if(!s)
|
|
||||||
return NULL;
|
|
||||||
jobject r = E->NewStringUTF(env, s);
|
|
||||||
free(s);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
static jbyteArray jnibytearray_create(JNIEnv *env, const uint8_t *buf, size_t buf_size)
|
|
||||||
{
|
|
||||||
jbyteArray r = E->NewByteArray(env, buf_size);
|
|
||||||
E->SetByteArrayRegion(env, r, 0, buf_size, (const jbyte *)buf);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
static jobject get_kotlin_global_object(JNIEnv *env, const char *id)
|
|
||||||
{
|
|
||||||
size_t idlen = strlen(id);
|
|
||||||
char *sig = malloc(idlen + 3);
|
|
||||||
if(!sig)
|
|
||||||
return NULL;
|
|
||||||
sig[0] = 'L';
|
|
||||||
memcpy(sig + 1, id, idlen);
|
|
||||||
sig[1 + idlen] = ';';
|
|
||||||
sig[1 + idlen + 1] = '\0';
|
|
||||||
jclass cls = E->FindClass(env, id);
|
|
||||||
jfieldID field_id = E->GetStaticFieldID(env, cls, "INSTANCE", sig);
|
|
||||||
jobject r = E->GetStaticObjectField(env, cls, field_id);
|
|
||||||
free(sig);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ChiakiLog global_log;
|
|
||||||
JavaVM *global_vm;
|
|
||||||
|
|
||||||
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
|
|
||||||
{
|
|
||||||
global_vm = vm;
|
|
||||||
|
|
||||||
android_chiaki_file_log_init(&global_log, CHIAKI_LOG_ALL & ~CHIAKI_LOG_VERBOSE, NULL);
|
|
||||||
CHIAKI_LOGI(&global_log, "Loading Chiaki Library");
|
|
||||||
ChiakiErrorCode err = chiaki_lib_init();
|
|
||||||
CHIAKI_LOGI(&global_log, "Chiaki Library Init Result: %s\n", chiaki_error_string(err));
|
|
||||||
return JNI_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEnv *attach_thread_jni()
|
|
||||||
{
|
|
||||||
JNIEnv *env;
|
|
||||||
int r = (*global_vm)->GetEnv(global_vm, (void **)&env, JNI_VERSION);
|
|
||||||
if(r == JNI_OK)
|
|
||||||
return env;
|
|
||||||
|
|
||||||
if((*global_vm)->AttachCurrentThread(global_vm, &env, NULL) == 0)
|
|
||||||
return env;
|
|
||||||
|
|
||||||
CHIAKI_LOGE(&global_log, "Failed to get JNIEnv from JavaVM or attach");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL JNI_FCN(errorCodeToString)(JNIEnv *env, jobject obj, jint value)
|
|
||||||
{
|
|
||||||
return E->NewStringUTF(env, chiaki_error_string((ChiakiErrorCode)value));
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL JNI_FCN(quitReasonToString)(JNIEnv *env, jobject obj, jint value)
|
|
||||||
{
|
|
||||||
return E->NewStringUTF(env, chiaki_quit_reason_string((ChiakiQuitReason)value));
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL JNI_FCN(quitReasonIsError)(JNIEnv *env, jobject obj, jint value)
|
|
||||||
{
|
|
||||||
return chiaki_quit_reason_is_error(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jobject JNICALL JNI_FCN(videoProfilePreset)(JNIEnv *env, jobject obj, jint resolution_preset, jint fps_preset, jobject codec)
|
|
||||||
{
|
|
||||||
ChiakiConnectVideoProfile profile = { 0 };
|
|
||||||
chiaki_connect_video_profile_preset(&profile, (ChiakiVideoResolutionPreset)resolution_preset, (ChiakiVideoFPSPreset)fps_preset);
|
|
||||||
jclass profile_class = E->FindClass(env, BASE_PACKAGE"/ConnectVideoProfile");
|
|
||||||
jmethodID profile_ctor = E->GetMethodID(env, profile_class, "<init>", "(IIIIL"BASE_PACKAGE"/Codec;)V");
|
|
||||||
return E->NewObject(env, profile_class, profile_ctor, profile.width, profile.height, profile.max_fps, profile.bitrate, codec);
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef struct android_chiaki_session_t
|
|
||||||
{
|
|
||||||
ChiakiSession session;
|
|
||||||
ChiakiLog *log;
|
|
||||||
jobject java_session;
|
|
||||||
jclass java_session_class;
|
|
||||||
jmethodID java_session_event_connected_meth;
|
|
||||||
jmethodID java_session_event_login_pin_request_meth;
|
|
||||||
jmethodID java_session_event_quit_meth;
|
|
||||||
jmethodID java_session_event_rumble_meth;
|
|
||||||
jfieldID java_controller_state_buttons;
|
|
||||||
jfieldID java_controller_state_l2_state;
|
|
||||||
jfieldID java_controller_state_r2_state;
|
|
||||||
jfieldID java_controller_state_left_x;
|
|
||||||
jfieldID java_controller_state_left_y;
|
|
||||||
jfieldID java_controller_state_right_x;
|
|
||||||
jfieldID java_controller_state_right_y;
|
|
||||||
jfieldID java_controller_state_touches;
|
|
||||||
jfieldID java_controller_state_gyro_x;
|
|
||||||
jfieldID java_controller_state_gyro_y;
|
|
||||||
jfieldID java_controller_state_gyro_z;
|
|
||||||
jfieldID java_controller_state_accel_x;
|
|
||||||
jfieldID java_controller_state_accel_y;
|
|
||||||
jfieldID java_controller_state_accel_z;
|
|
||||||
jfieldID java_controller_state_orient_x;
|
|
||||||
jfieldID java_controller_state_orient_y;
|
|
||||||
jfieldID java_controller_state_orient_z;
|
|
||||||
jfieldID java_controller_state_orient_w;
|
|
||||||
jfieldID java_controller_touch_x;
|
|
||||||
jfieldID java_controller_touch_y;
|
|
||||||
jfieldID java_controller_touch_id;
|
|
||||||
|
|
||||||
AndroidChiakiVideoDecoder video_decoder;
|
|
||||||
AndroidChiakiAudioDecoder audio_decoder;
|
|
||||||
void *audio_output;
|
|
||||||
} AndroidChiakiSession;
|
|
||||||
|
|
||||||
static void android_chiaki_event_cb(ChiakiEvent *event, void *user)
|
|
||||||
{
|
|
||||||
AndroidChiakiSession *session = user;
|
|
||||||
|
|
||||||
JNIEnv *env = attach_thread_jni();
|
|
||||||
if(!env)
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch(event->type)
|
|
||||||
{
|
|
||||||
case CHIAKI_EVENT_CONNECTED:
|
|
||||||
E->CallVoidMethod(env, session->java_session,
|
|
||||||
session->java_session_event_connected_meth);
|
|
||||||
break;
|
|
||||||
case CHIAKI_EVENT_LOGIN_PIN_REQUEST:
|
|
||||||
E->CallVoidMethod(env, session->java_session,
|
|
||||||
session->java_session_event_login_pin_request_meth,
|
|
||||||
(jboolean)event->login_pin_request.pin_incorrect);
|
|
||||||
break;
|
|
||||||
case CHIAKI_EVENT_QUIT:
|
|
||||||
{
|
|
||||||
char *reason_str = strdup_jni(event->quit.reason_str);
|
|
||||||
jstring reason_str_java = reason_str ? E->NewStringUTF(env, reason_str) : NULL;
|
|
||||||
E->CallVoidMethod(env, session->java_session,
|
|
||||||
session->java_session_event_quit_meth,
|
|
||||||
(jint)event->quit.reason,
|
|
||||||
reason_str_java);
|
|
||||||
if(reason_str_java)
|
|
||||||
E->DeleteLocalRef(env, reason_str_java);
|
|
||||||
free(reason_str);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CHIAKI_EVENT_RUMBLE:
|
|
||||||
E->CallVoidMethod(env, session->java_session,
|
|
||||||
session->java_session_event_rumble_meth,
|
|
||||||
(jint)event->rumble.left,
|
|
||||||
(jint)event->rumble.right);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*global_vm)->DetachCurrentThread(global_vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject result, jobject connect_info_obj, jstring log_file_str, jboolean log_verbose, jobject java_session)
|
|
||||||
{
|
|
||||||
AndroidChiakiSession *session = NULL;
|
|
||||||
ChiakiLog *log = malloc(sizeof(ChiakiLog));
|
|
||||||
const char *log_file = log_file_str ? E->GetStringUTFChars(env, log_file_str, NULL) : NULL;
|
|
||||||
android_chiaki_file_log_init(log, log_verbose ? CHIAKI_LOG_ALL : (CHIAKI_LOG_ALL & ~CHIAKI_LOG_VERBOSE), log_file);
|
|
||||||
if(log_file)
|
|
||||||
E->ReleaseStringUTFChars(env, log_file_str, log_file);
|
|
||||||
|
|
||||||
ChiakiErrorCode err = CHIAKI_ERR_SUCCESS;
|
|
||||||
char *host_str = NULL;
|
|
||||||
|
|
||||||
jclass result_class = E->GetObjectClass(env, result);
|
|
||||||
|
|
||||||
jclass connect_info_class = E->GetObjectClass(env, connect_info_obj);
|
|
||||||
jboolean ps5 = E->GetBooleanField(env, connect_info_obj, E->GetFieldID(env, connect_info_class, "ps5", "Z"));
|
|
||||||
jstring host_string = E->GetObjectField(env, connect_info_obj, E->GetFieldID(env, connect_info_class, "host", "Ljava/lang/String;"));
|
|
||||||
jbyteArray regist_key_array = E->GetObjectField(env, connect_info_obj, E->GetFieldID(env, connect_info_class, "registKey", "[B"));
|
|
||||||
jbyteArray morning_array = E->GetObjectField(env, connect_info_obj, E->GetFieldID(env, connect_info_class, "morning", "[B"));
|
|
||||||
jobject connect_video_profile_obj = E->GetObjectField(env, connect_info_obj, E->GetFieldID(env, connect_info_class, "videoProfile", "L"BASE_PACKAGE"/ConnectVideoProfile;"));
|
|
||||||
jclass connect_video_profile_class = E->GetObjectClass(env, connect_video_profile_obj);
|
|
||||||
|
|
||||||
ChiakiConnectInfo connect_info = { 0 };
|
|
||||||
connect_info.ps5 = ps5;
|
|
||||||
|
|
||||||
const char *str_borrow = E->GetStringUTFChars(env, host_string, NULL);
|
|
||||||
connect_info.host = host_str = strdup(str_borrow);
|
|
||||||
E->ReleaseStringUTFChars(env, host_string, str_borrow);
|
|
||||||
if(!connect_info.host)
|
|
||||||
{
|
|
||||||
err = CHIAKI_ERR_MEMORY;
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(E->GetArrayLength(env, regist_key_array) != sizeof(connect_info.regist_key))
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(log, "Regist Key passed from Java has invalid length");
|
|
||||||
err = CHIAKI_ERR_INVALID_DATA;
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
jbyte *bytes = E->GetByteArrayElements(env, regist_key_array, NULL);
|
|
||||||
memcpy(connect_info.regist_key, bytes, sizeof(connect_info.regist_key));
|
|
||||||
E->ReleaseByteArrayElements(env, regist_key_array, bytes, JNI_ABORT);
|
|
||||||
|
|
||||||
if(E->GetArrayLength(env, morning_array) != sizeof(connect_info.morning))
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(log, "Morning passed from Java has invalid length");
|
|
||||||
err = CHIAKI_ERR_INVALID_DATA;
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
bytes = E->GetByteArrayElements(env, morning_array, NULL);
|
|
||||||
memcpy(connect_info.morning, bytes, sizeof(connect_info.morning));
|
|
||||||
E->ReleaseByteArrayElements(env, morning_array, bytes, JNI_ABORT);
|
|
||||||
|
|
||||||
connect_info.video_profile.width = (unsigned int)E->GetIntField(env, connect_video_profile_obj, E->GetFieldID(env, connect_video_profile_class, "width", "I"));
|
|
||||||
connect_info.video_profile.height = (unsigned int)E->GetIntField(env, connect_video_profile_obj, E->GetFieldID(env, connect_video_profile_class, "height", "I"));
|
|
||||||
connect_info.video_profile.max_fps = (unsigned int)E->GetIntField(env, connect_video_profile_obj, E->GetFieldID(env, connect_video_profile_class, "maxFPS", "I"));
|
|
||||||
connect_info.video_profile.bitrate = (unsigned int)E->GetIntField(env, connect_video_profile_obj, E->GetFieldID(env, connect_video_profile_class, "bitrate", "I"));
|
|
||||||
|
|
||||||
jobject codec_obj = E->GetObjectField(env, connect_video_profile_obj, E->GetFieldID(env, connect_video_profile_class, "codec", "L"BASE_PACKAGE"/Codec;"));
|
|
||||||
jclass codec_class = E->GetObjectClass(env, codec_obj);
|
|
||||||
jint target_value = E->GetIntField(env, codec_obj, E->GetFieldID(env, codec_class, "value", "I"));
|
|
||||||
connect_info.video_profile.codec = (ChiakiCodec)target_value;
|
|
||||||
|
|
||||||
connect_info.video_profile_auto_downgrade = true;
|
|
||||||
|
|
||||||
session = CHIAKI_NEW(AndroidChiakiSession);
|
|
||||||
if(!session)
|
|
||||||
{
|
|
||||||
err = CHIAKI_ERR_MEMORY;
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
memset(session, 0, sizeof(AndroidChiakiSession));
|
|
||||||
session->log = log;
|
|
||||||
err = android_chiaki_video_decoder_init(&session->video_decoder, log, connect_info.video_profile.width, connect_info.video_profile.height,
|
|
||||||
connect_info.ps5 ? connect_info.video_profile.codec : CHIAKI_CODEC_H264);
|
|
||||||
if(err != CHIAKI_ERR_SUCCESS)
|
|
||||||
{
|
|
||||||
free(session);
|
|
||||||
session = NULL;
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = android_chiaki_audio_decoder_init(&session->audio_decoder, log);
|
|
||||||
if(err != CHIAKI_ERR_SUCCESS)
|
|
||||||
{
|
|
||||||
android_chiaki_video_decoder_fini(&session->video_decoder);
|
|
||||||
free(session);
|
|
||||||
session = NULL;
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
session->audio_output = android_chiaki_audio_output_new(log);
|
|
||||||
|
|
||||||
android_chiaki_audio_decoder_set_cb(&session->audio_decoder, android_chiaki_audio_output_settings, android_chiaki_audio_output_frame, session->audio_output);
|
|
||||||
|
|
||||||
err = chiaki_session_init(&session->session, &connect_info, log);
|
|
||||||
if(err != CHIAKI_ERR_SUCCESS)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(log, "JNI ChiakiSession failed to init");
|
|
||||||
android_chiaki_video_decoder_fini(&session->video_decoder);
|
|
||||||
android_chiaki_audio_decoder_fini(&session->audio_decoder);
|
|
||||||
android_chiaki_audio_output_free(session->audio_output);
|
|
||||||
free(session);
|
|
||||||
session = NULL;
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
session->java_session = E->NewGlobalRef(env, java_session);
|
|
||||||
session->java_session_class = E->NewGlobalRef(env, E->GetObjectClass(env, session->java_session));
|
|
||||||
session->java_session_event_connected_meth = E->GetMethodID(env, session->java_session_class, "eventConnected", "()V");
|
|
||||||
session->java_session_event_login_pin_request_meth = E->GetMethodID(env, session->java_session_class, "eventLoginPinRequest", "(Z)V");
|
|
||||||
session->java_session_event_quit_meth = E->GetMethodID(env, session->java_session_class, "eventQuit", "(ILjava/lang/String;)V");
|
|
||||||
session->java_session_event_rumble_meth = E->GetMethodID(env, session->java_session_class, "eventRumble", "(II)V");
|
|
||||||
|
|
||||||
jclass controller_state_class = E->FindClass(env, BASE_PACKAGE"/ControllerState");
|
|
||||||
session->java_controller_state_buttons = E->GetFieldID(env, controller_state_class, "buttons", "I");
|
|
||||||
session->java_controller_state_l2_state = E->GetFieldID(env, controller_state_class, "l2State", "B");
|
|
||||||
session->java_controller_state_r2_state = E->GetFieldID(env, controller_state_class, "r2State", "B");
|
|
||||||
session->java_controller_state_left_x = E->GetFieldID(env, controller_state_class, "leftX", "S");
|
|
||||||
session->java_controller_state_left_y = E->GetFieldID(env, controller_state_class, "leftY", "S");
|
|
||||||
session->java_controller_state_right_x = E->GetFieldID(env, controller_state_class, "rightX", "S");
|
|
||||||
session->java_controller_state_right_y = E->GetFieldID(env, controller_state_class, "rightY", "S");
|
|
||||||
session->java_controller_state_touches = E->GetFieldID(env, controller_state_class, "touches", "[L"BASE_PACKAGE"/ControllerTouch;");
|
|
||||||
session->java_controller_state_gyro_x = E->GetFieldID(env, controller_state_class, "gyroX", "F");
|
|
||||||
session->java_controller_state_gyro_y = E->GetFieldID(env, controller_state_class, "gyroY", "F");
|
|
||||||
session->java_controller_state_gyro_z = E->GetFieldID(env, controller_state_class, "gyroZ", "F");
|
|
||||||
session->java_controller_state_accel_x = E->GetFieldID(env, controller_state_class, "accelX", "F");
|
|
||||||
session->java_controller_state_accel_y = E->GetFieldID(env, controller_state_class, "accelY", "F");
|
|
||||||
session->java_controller_state_accel_z = E->GetFieldID(env, controller_state_class, "accelZ", "F");
|
|
||||||
session->java_controller_state_orient_x = E->GetFieldID(env, controller_state_class, "orientX", "F");
|
|
||||||
session->java_controller_state_orient_y = E->GetFieldID(env, controller_state_class, "orientY", "F");
|
|
||||||
session->java_controller_state_orient_z = E->GetFieldID(env, controller_state_class, "orientZ", "F");
|
|
||||||
session->java_controller_state_orient_w = E->GetFieldID(env, controller_state_class, "orientW", "F");
|
|
||||||
|
|
||||||
jclass controller_touch_class = E->FindClass(env, BASE_PACKAGE"/ControllerTouch");
|
|
||||||
session->java_controller_touch_x = E->GetFieldID(env, controller_touch_class, "x", "S");
|
|
||||||
session->java_controller_touch_y = E->GetFieldID(env, controller_touch_class, "y", "S");
|
|
||||||
session->java_controller_touch_id = E->GetFieldID(env, controller_touch_class, "id", "B");
|
|
||||||
|
|
||||||
chiaki_session_set_event_cb(&session->session, android_chiaki_event_cb, session);
|
|
||||||
chiaki_session_set_video_sample_cb(&session->session, android_chiaki_video_decoder_video_sample, &session->video_decoder);
|
|
||||||
|
|
||||||
ChiakiAudioSink audio_sink;
|
|
||||||
android_chiaki_audio_decoder_get_sink(&session->audio_decoder, &audio_sink);
|
|
||||||
chiaki_session_set_audio_sink(&session->session, &audio_sink);
|
|
||||||
|
|
||||||
beach:
|
|
||||||
if(!session && log)
|
|
||||||
{
|
|
||||||
android_chiaki_file_log_fini(log);
|
|
||||||
free(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
free(host_str);
|
|
||||||
E->SetIntField(env, result, E->GetFieldID(env, result_class, "errorCode", "I"), (jint)err);
|
|
||||||
E->SetLongField(env, result, E->GetFieldID(env, result_class, "ptr", "J"), (jlong)session);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JNI_FCN(sessionFree)(JNIEnv *env, jobject obj, jlong ptr)
|
|
||||||
{
|
|
||||||
AndroidChiakiSession *session = (AndroidChiakiSession *)ptr;
|
|
||||||
if(!session)
|
|
||||||
return;
|
|
||||||
CHIAKI_LOGI(session->log, "Shutting down JNI Session");
|
|
||||||
chiaki_session_fini(&session->session);
|
|
||||||
android_chiaki_video_decoder_fini(&session->video_decoder);
|
|
||||||
android_chiaki_audio_decoder_fini(&session->audio_decoder);
|
|
||||||
android_chiaki_audio_output_free(session->audio_output);
|
|
||||||
E->DeleteGlobalRef(env, session->java_session);
|
|
||||||
E->DeleteGlobalRef(env, session->java_session_class);
|
|
||||||
CHIAKI_LOGI(session->log, "JNI Session has quit");
|
|
||||||
android_chiaki_file_log_fini(session->log);
|
|
||||||
free(session->log);
|
|
||||||
free(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL JNI_FCN(sessionStart)(JNIEnv *env, jobject obj, jlong ptr)
|
|
||||||
{
|
|
||||||
AndroidChiakiSession *session = (AndroidChiakiSession *)ptr;
|
|
||||||
CHIAKI_LOGI(session->log, "Start JNI Session");
|
|
||||||
return chiaki_session_start(&session->session);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL JNI_FCN(sessionStop)(JNIEnv *env, jobject obj, jlong ptr)
|
|
||||||
{
|
|
||||||
AndroidChiakiSession *session = (AndroidChiakiSession *)ptr;
|
|
||||||
CHIAKI_LOGI(session->log, "Stop JNI Session");
|
|
||||||
return chiaki_session_stop(&session->session);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL JNI_FCN(sessionJoin)(JNIEnv *env, jobject obj, jlong ptr)
|
|
||||||
{
|
|
||||||
AndroidChiakiSession *session = (AndroidChiakiSession *)ptr;
|
|
||||||
CHIAKI_LOGI(session->log, "Join JNI Session");
|
|
||||||
return chiaki_session_join(&session->session);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JNI_FCN(sessionSetSurface)(JNIEnv *env, jobject obj, jlong ptr, jobject surface)
|
|
||||||
{
|
|
||||||
AndroidChiakiSession *session = (AndroidChiakiSession *)ptr;
|
|
||||||
android_chiaki_video_decoder_set_surface(&session->video_decoder, env, surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JNI_FCN(sessionSetControllerState)(JNIEnv *env, jobject obj, jlong ptr, jobject controller_state_java)
|
|
||||||
{
|
|
||||||
AndroidChiakiSession *session = (AndroidChiakiSession *)ptr;
|
|
||||||
ChiakiControllerState controller_state;
|
|
||||||
chiaki_controller_state_set_idle(&controller_state);
|
|
||||||
controller_state.buttons = (uint32_t)E->GetIntField(env, controller_state_java, session->java_controller_state_buttons);
|
|
||||||
controller_state.l2_state = (uint8_t)E->GetByteField(env, controller_state_java, session->java_controller_state_l2_state);
|
|
||||||
controller_state.r2_state = (uint8_t)E->GetByteField(env, controller_state_java, session->java_controller_state_r2_state);
|
|
||||||
controller_state.left_x = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_left_x);
|
|
||||||
controller_state.left_y = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_left_y);
|
|
||||||
controller_state.right_x = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_right_x);
|
|
||||||
controller_state.right_y = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_right_y);
|
|
||||||
jobjectArray touch_array = E->GetObjectField(env, controller_state_java, session->java_controller_state_touches);
|
|
||||||
size_t touch_array_len = (size_t)E->GetArrayLength(env, touch_array);
|
|
||||||
for(size_t i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++)
|
|
||||||
{
|
|
||||||
if(i < touch_array_len)
|
|
||||||
{
|
|
||||||
jobject touch = E->GetObjectArrayElement(env, touch_array, i);
|
|
||||||
controller_state.touches[i].x = (uint16_t)E->GetShortField(env, touch, session->java_controller_touch_x);
|
|
||||||
controller_state.touches[i].y = (uint16_t)E->GetShortField(env, touch, session->java_controller_touch_y);
|
|
||||||
controller_state.touches[i].id = (int8_t)E->GetByteField(env, touch, session->java_controller_touch_id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
controller_state.touches[i].x = 0;
|
|
||||||
controller_state.touches[i].y = 0;
|
|
||||||
controller_state.touches[i].id = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
controller_state.gyro_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_x);
|
|
||||||
controller_state.gyro_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_y);
|
|
||||||
controller_state.gyro_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_z);
|
|
||||||
controller_state.accel_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_x);
|
|
||||||
controller_state.accel_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_y);
|
|
||||||
controller_state.accel_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_z);
|
|
||||||
controller_state.orient_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_x);
|
|
||||||
controller_state.orient_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_y);
|
|
||||||
controller_state.orient_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_z);
|
|
||||||
controller_state.orient_w = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_w);
|
|
||||||
chiaki_session_set_controller_state(&session->session, &controller_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JNI_FCN(sessionSetLoginPin)(JNIEnv *env, jobject obj, jlong ptr, jstring pin_java)
|
|
||||||
{
|
|
||||||
AndroidChiakiSession *session = (AndroidChiakiSession *)ptr;
|
|
||||||
const char *pin = E->GetStringUTFChars(env, pin_java, NULL);
|
|
||||||
chiaki_session_set_login_pin(&session->session, (const uint8_t *)pin, strlen(pin));
|
|
||||||
E->ReleaseStringUTFChars(env, pin_java, pin);
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef struct android_discovery_service_t
|
|
||||||
{
|
|
||||||
ChiakiDiscoveryService service;
|
|
||||||
jobject java_service;
|
|
||||||
jclass java_service_class;
|
|
||||||
jmethodID java_service_hosts_updated_meth;
|
|
||||||
|
|
||||||
jclass host_class;
|
|
||||||
jmethodID host_ctor;
|
|
||||||
jobject host_state_unknown;
|
|
||||||
jobject host_state_ready;
|
|
||||||
jobject host_state_standby;
|
|
||||||
} AndroidDiscoveryService;
|
|
||||||
|
|
||||||
static void android_discovery_service_cb(ChiakiDiscoveryHost *hosts, size_t hosts_count, void *user)
|
|
||||||
{
|
|
||||||
AndroidDiscoveryService *service = user;
|
|
||||||
|
|
||||||
CHIAKI_LOGI(&global_log, "JNI Discovery Callback got %llu hosts", (unsigned long long)hosts_count);
|
|
||||||
|
|
||||||
JNIEnv *env = attach_thread_jni();
|
|
||||||
if(!env)
|
|
||||||
return;
|
|
||||||
|
|
||||||
jobjectArray r = E->NewObjectArray(env, hosts_count, service->host_class, NULL);
|
|
||||||
|
|
||||||
for(size_t i=0; i<hosts_count; i++)
|
|
||||||
{
|
|
||||||
jobject state;
|
|
||||||
ChiakiDiscoveryHost *host = hosts + i;
|
|
||||||
switch(host->state)
|
|
||||||
{
|
|
||||||
case CHIAKI_DISCOVERY_HOST_STATE_STANDBY:
|
|
||||||
state = service->host_state_standby;
|
|
||||||
break;
|
|
||||||
case CHIAKI_DISCOVERY_HOST_STATE_READY:
|
|
||||||
state = service->host_state_ready;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
state = service->host_state_unknown;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobject o = E->NewObject(env, service->host_class, service->host_ctor,
|
|
||||||
state,
|
|
||||||
host->host_request_port,
|
|
||||||
jnistr_from_ascii(env, host->host_addr),
|
|
||||||
jnistr_from_ascii(env, host->system_version),
|
|
||||||
jnistr_from_ascii(env, host->device_discovery_protocol_version),
|
|
||||||
jnistr_from_ascii(env, host->host_name),
|
|
||||||
jnistr_from_ascii(env, host->host_type),
|
|
||||||
jnistr_from_ascii(env, host->host_id),
|
|
||||||
jnistr_from_ascii(env, host->running_app_titleid),
|
|
||||||
jnistr_from_ascii(env, host->running_app_name));
|
|
||||||
|
|
||||||
E->SetObjectArrayElement(env, r, i, o);
|
|
||||||
}
|
|
||||||
|
|
||||||
E->CallVoidMethod(env, service->java_service, service->java_service_hosts_updated_meth, r);
|
|
||||||
|
|
||||||
(*global_vm)->DetachCurrentThread(global_vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ChiakiErrorCode sockaddr_from_java(JNIEnv *env, jobject /*InetSocketAddress*/ sockaddr_obj, struct sockaddr **addr, size_t *addr_size)
|
|
||||||
{
|
|
||||||
jclass sockaddr_class = E->GetObjectClass(env, sockaddr_obj);
|
|
||||||
uint16_t port = (uint16_t)E->CallIntMethod(env, sockaddr_obj, E->GetMethodID(env, sockaddr_class, "getPort", "()I"));
|
|
||||||
jobject addr_obj = E->CallObjectMethod(env, sockaddr_obj, E->GetMethodID(env, sockaddr_class, "getAddress", "()Ljava/net/InetAddress;"));
|
|
||||||
jclass addr_class = E->GetObjectClass(env, addr_obj);
|
|
||||||
jbyteArray addr_byte_array = E->CallObjectMethod(env, addr_obj, E->GetMethodID(env, addr_class, "getAddress", "()[B"));
|
|
||||||
jsize addr_byte_array_len = E->GetArrayLength(env, addr_byte_array);
|
|
||||||
|
|
||||||
if(addr_byte_array_len == 4)
|
|
||||||
{
|
|
||||||
struct sockaddr_in *inaddr = CHIAKI_NEW(struct sockaddr_in);
|
|
||||||
if(!inaddr)
|
|
||||||
return CHIAKI_ERR_MEMORY;
|
|
||||||
memset(inaddr, 0, sizeof(*inaddr));
|
|
||||||
inaddr->sin_family = AF_INET;
|
|
||||||
jbyte *bytes = E->GetByteArrayElements(env, addr_byte_array, NULL);
|
|
||||||
memcpy(&inaddr->sin_addr.s_addr, bytes, sizeof(inaddr->sin_addr.s_addr));
|
|
||||||
E->ReleaseByteArrayElements(env, addr_byte_array, bytes, JNI_ABORT);
|
|
||||||
inaddr->sin_port = htons(port);
|
|
||||||
|
|
||||||
*addr = (struct sockaddr *)inaddr;
|
|
||||||
*addr_size = sizeof(*inaddr);
|
|
||||||
}
|
|
||||||
else if(addr_byte_array_len == 0x10)
|
|
||||||
{
|
|
||||||
struct sockaddr_in6 *inaddr6 = CHIAKI_NEW(struct sockaddr_in6);
|
|
||||||
if(!inaddr6)
|
|
||||||
return CHIAKI_ERR_MEMORY;
|
|
||||||
memset(inaddr6, 0, sizeof(*inaddr6));
|
|
||||||
inaddr6->sin6_family = AF_INET6;
|
|
||||||
jbyte *bytes = E->GetByteArrayElements(env, addr_byte_array, NULL);
|
|
||||||
memcpy(&inaddr6->sin6_addr.in6_u, bytes, sizeof(inaddr6->sin6_addr.in6_u));
|
|
||||||
E->ReleaseByteArrayElements(env, addr_byte_array, bytes, JNI_ABORT);
|
|
||||||
inaddr6->sin6_port = htons(port);
|
|
||||||
|
|
||||||
*addr = (struct sockaddr *)inaddr6;
|
|
||||||
*addr_size = sizeof(*inaddr6);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return CHIAKI_ERR_INVALID_DATA;
|
|
||||||
|
|
||||||
return CHIAKI_ERR_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JNI_FCN(discoveryServiceCreate)(JNIEnv *env, jobject obj, jobject result, jobject options_obj, jobject java_service)
|
|
||||||
{
|
|
||||||
jclass result_class = E->GetObjectClass(env, result);
|
|
||||||
ChiakiErrorCode err = CHIAKI_ERR_SUCCESS;
|
|
||||||
ChiakiDiscoveryServiceOptions options = { 0 };
|
|
||||||
|
|
||||||
AndroidDiscoveryService *service = CHIAKI_NEW(AndroidDiscoveryService);
|
|
||||||
if(!service)
|
|
||||||
{
|
|
||||||
err = CHIAKI_ERR_MEMORY;
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
jclass options_class = E->GetObjectClass(env, options_obj);
|
|
||||||
|
|
||||||
options.hosts_max = (size_t)E->GetLongField(env, options_obj, E->GetFieldID(env, options_class, "hostsMax", "J"));
|
|
||||||
options.host_drop_pings = (uint64_t)E->GetLongField(env, options_obj, E->GetFieldID(env, options_class, "hostDropPings", "J"));
|
|
||||||
options.ping_ms = (uint64_t)E->GetLongField(env, options_obj, E->GetFieldID(env, options_class, "pingMs", "J"));
|
|
||||||
options.cb = android_discovery_service_cb;
|
|
||||||
options.cb_user = service;
|
|
||||||
|
|
||||||
err = sockaddr_from_java(env, E->GetObjectField(env, options_obj, E->GetFieldID(env, options_class, "sendAddr", "Ljava/net/InetSocketAddress;")), &options.send_addr, &options.send_addr_size);
|
|
||||||
if(err != CHIAKI_ERR_SUCCESS)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(&global_log, "Failed to get sockaddr from InetSocketAddress");
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
service->java_service = E->NewGlobalRef(env, java_service);
|
|
||||||
service->java_service_class = E->GetObjectClass(env, service->java_service);
|
|
||||||
service->java_service_hosts_updated_meth = E->GetMethodID(env, service->java_service_class, "hostsUpdated", "([L"BASE_PACKAGE"/DiscoveryHost;)V");
|
|
||||||
|
|
||||||
service->host_class = E->NewGlobalRef(env, E->FindClass(env, BASE_PACKAGE"/DiscoveryHost"));
|
|
||||||
service->host_ctor = E->GetMethodID(env, service->host_class, "<init>", "("
|
|
||||||
"L"BASE_PACKAGE"/DiscoveryHost$State;"
|
|
||||||
"S" // hostRequestPort: UShort
|
|
||||||
"Ljava/lang/String;" // hostAddr: String?,
|
|
||||||
"Ljava/lang/String;" // systemVersion: String?,
|
|
||||||
"Ljava/lang/String;" // deviceDiscoveryProtocolVersion: String?,
|
|
||||||
"Ljava/lang/String;" // hostName: String?,
|
|
||||||
"Ljava/lang/String;" // hostType: String?,
|
|
||||||
"Ljava/lang/String;" // hostId: String?,
|
|
||||||
"Ljava/lang/String;" // runningAppTitleid: String?,
|
|
||||||
"Ljava/lang/String;" // runningAppName: String?
|
|
||||||
")V");
|
|
||||||
|
|
||||||
jclass host_state_class = E->FindClass(env, BASE_PACKAGE"/DiscoveryHost$State");
|
|
||||||
service->host_state_unknown = E->NewGlobalRef(env, E->GetStaticObjectField(env, host_state_class, E->GetStaticFieldID(env, host_state_class, "UNKNOWN", "L"BASE_PACKAGE"/DiscoveryHost$State;")));
|
|
||||||
service->host_state_standby = E->NewGlobalRef(env, E->GetStaticObjectField(env, host_state_class, E->GetStaticFieldID(env, host_state_class, "STANDBY", "L"BASE_PACKAGE"/DiscoveryHost$State;")));
|
|
||||||
service->host_state_ready = E->NewGlobalRef(env, E->GetStaticObjectField(env, host_state_class, E->GetStaticFieldID(env, host_state_class, "READY", "L"BASE_PACKAGE"/DiscoveryHost$State;")));
|
|
||||||
|
|
||||||
|
|
||||||
err = chiaki_discovery_service_init(&service->service, &options, &global_log);
|
|
||||||
if(err != CHIAKI_ERR_SUCCESS)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(&global_log, "Failed to create discovery service (JNI)");
|
|
||||||
E->DeleteGlobalRef(env, service->java_service);
|
|
||||||
E->DeleteGlobalRef(env, service->host_state_unknown);
|
|
||||||
E->DeleteGlobalRef(env, service->host_state_standby);
|
|
||||||
E->DeleteGlobalRef(env, service->host_state_ready);
|
|
||||||
E->DeleteGlobalRef(env, service->host_class);
|
|
||||||
free(service);
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
beach:
|
|
||||||
free(options.send_addr);
|
|
||||||
E->SetIntField(env, result, E->GetFieldID(env, result_class, "errorCode", "I"), (jint)err);
|
|
||||||
E->SetLongField(env, result, E->GetFieldID(env, result_class, "ptr", "J"), (jlong)service);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JNI_FCN(discoveryServiceFree)(JNIEnv *env, jobject obj, jlong ptr)
|
|
||||||
{
|
|
||||||
AndroidDiscoveryService *service = (AndroidDiscoveryService *)ptr;
|
|
||||||
if(!service)
|
|
||||||
return;
|
|
||||||
chiaki_discovery_service_fini(&service->service);
|
|
||||||
E->DeleteGlobalRef(env, service->java_service);
|
|
||||||
E->DeleteGlobalRef(env, service->host_state_unknown);
|
|
||||||
E->DeleteGlobalRef(env, service->host_state_standby);
|
|
||||||
E->DeleteGlobalRef(env, service->host_state_ready);
|
|
||||||
E->DeleteGlobalRef(env, service->host_class);
|
|
||||||
free(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL JNI_FCN(discoveryServiceWakeup)(JNIEnv *env, jobject obj, jlong ptr, jstring host_string, jlong user_credential, jboolean ps5)
|
|
||||||
{
|
|
||||||
AndroidDiscoveryService *service = (AndroidDiscoveryService *)ptr;
|
|
||||||
const char *host = E->GetStringUTFChars(env, host_string, NULL);
|
|
||||||
ChiakiErrorCode r = chiaki_discovery_wakeup(&global_log, service ? &service->service.discovery : NULL, host, (uint64_t)user_credential, ps5);
|
|
||||||
E->ReleaseStringUTFChars(env, host_string, host);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
typedef struct android_chiaki_regist_t
|
|
||||||
{
|
|
||||||
AndroidChiakiJNILog log;
|
|
||||||
ChiakiRegist regist;
|
|
||||||
|
|
||||||
jobject java_regist;
|
|
||||||
jmethodID java_regist_event_meth;
|
|
||||||
|
|
||||||
jclass java_target_class;
|
|
||||||
|
|
||||||
jobject java_regist_event_canceled;
|
|
||||||
jobject java_regist_event_failed;
|
|
||||||
jclass java_regist_event_success_class;
|
|
||||||
jmethodID java_regist_event_success_ctor;
|
|
||||||
|
|
||||||
jclass java_regist_host_class;
|
|
||||||
jmethodID java_regist_host_ctor;
|
|
||||||
} AndroidChiakiRegist;
|
|
||||||
|
|
||||||
static jobject create_jni_target(JNIEnv *env, jclass target_class, ChiakiTarget target)
|
|
||||||
{
|
|
||||||
jmethodID meth = E->GetStaticMethodID(env, target_class, "fromValue", "(I)L"BASE_PACKAGE"/Target;");
|
|
||||||
return E->CallStaticObjectMethod(env, target_class, meth, (jint)target);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void android_chiaki_regist_cb(ChiakiRegistEvent *event, void *user)
|
|
||||||
{
|
|
||||||
AndroidChiakiRegist *regist = user;
|
|
||||||
|
|
||||||
JNIEnv *env = attach_thread_jni();
|
|
||||||
if(!env)
|
|
||||||
return;
|
|
||||||
|
|
||||||
jobject java_event = NULL;
|
|
||||||
switch(event->type)
|
|
||||||
{
|
|
||||||
case CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED:
|
|
||||||
java_event = regist->java_regist_event_canceled;
|
|
||||||
break;
|
|
||||||
case CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED:
|
|
||||||
java_event = regist->java_regist_event_failed;
|
|
||||||
break;
|
|
||||||
case CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS:
|
|
||||||
{
|
|
||||||
ChiakiRegisteredHost *host = event->registered_host;
|
|
||||||
jobject java_host = E->NewObject(env, regist->java_regist_host_class, regist->java_regist_host_ctor,
|
|
||||||
create_jni_target(env, regist->java_target_class, host->target),
|
|
||||||
jnistr_from_ascii(env, host->ap_ssid),
|
|
||||||
jnistr_from_ascii(env, host->ap_bssid),
|
|
||||||
jnistr_from_ascii(env, host->ap_key),
|
|
||||||
jnistr_from_ascii(env, host->ap_name),
|
|
||||||
jnibytearray_create(env, host->server_mac, sizeof(host->server_mac)),
|
|
||||||
jnistr_from_ascii(env, host->server_nickname),
|
|
||||||
jnibytearray_create(env, (const uint8_t *)host->rp_regist_key, sizeof(host->rp_regist_key)),
|
|
||||||
(jint)host->rp_key_type,
|
|
||||||
jnibytearray_create(env, host->rp_key, sizeof(host->rp_key)));
|
|
||||||
java_event = E->NewObject(env, regist->java_regist_event_success_class, regist->java_regist_event_success_ctor, java_host);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(java_event)
|
|
||||||
E->CallVoidMethod(env, regist->java_regist, regist->java_regist_event_meth, java_event);
|
|
||||||
|
|
||||||
(*global_vm)->DetachCurrentThread(global_vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void android_chiaki_regist_fini_partial(JNIEnv *env, AndroidChiakiRegist *regist)
|
|
||||||
{
|
|
||||||
android_chiaki_jni_log_fini(®ist->log, env);
|
|
||||||
E->DeleteGlobalRef(env, regist->java_regist);
|
|
||||||
E->DeleteGlobalRef(env, regist->java_target_class);
|
|
||||||
E->DeleteGlobalRef(env, regist->java_regist_event_canceled);
|
|
||||||
E->DeleteGlobalRef(env, regist->java_regist_event_failed);
|
|
||||||
E->DeleteGlobalRef(env, regist->java_regist_event_success_class);
|
|
||||||
E->DeleteGlobalRef(env, regist->java_regist_host_class);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JNI_FCN(registStart)(JNIEnv *env, jobject obj, jobject result, jobject regist_info_obj, jobject log_obj, jobject java_regist)
|
|
||||||
{
|
|
||||||
jclass result_class = E->GetObjectClass(env, result);
|
|
||||||
ChiakiErrorCode err = CHIAKI_ERR_SUCCESS;
|
|
||||||
AndroidChiakiRegist *regist = CHIAKI_NEW(AndroidChiakiRegist);
|
|
||||||
if(!regist)
|
|
||||||
{
|
|
||||||
err = CHIAKI_ERR_MEMORY;
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
android_chiaki_jni_log_init(®ist->log, env, log_obj);
|
|
||||||
|
|
||||||
regist->java_regist = E->NewGlobalRef(env, java_regist);
|
|
||||||
regist->java_regist_event_meth = E->GetMethodID(env, E->GetObjectClass(env, regist->java_regist), "event", "(L"BASE_PACKAGE"/RegistEvent;)V");
|
|
||||||
|
|
||||||
regist->java_target_class = E->NewGlobalRef(env, E->FindClass(env, BASE_PACKAGE"/Target"));
|
|
||||||
|
|
||||||
regist->java_regist_event_canceled = E->NewGlobalRef(env, get_kotlin_global_object(env, BASE_PACKAGE"/RegistEventCanceled"));
|
|
||||||
regist->java_regist_event_failed = E->NewGlobalRef(env, get_kotlin_global_object(env, BASE_PACKAGE"/RegistEventFailed"));
|
|
||||||
regist->java_regist_event_success_class = E->NewGlobalRef(env, E->FindClass(env, BASE_PACKAGE"/RegistEventSuccess"));
|
|
||||||
regist->java_regist_event_success_ctor = E->GetMethodID(env, regist->java_regist_event_success_class, "<init>", "(L"BASE_PACKAGE"/RegistHost;)V");
|
|
||||||
|
|
||||||
regist->java_regist_host_class = E->NewGlobalRef(env, E->FindClass(env, BASE_PACKAGE"/RegistHost"));
|
|
||||||
regist->java_regist_host_ctor = E->GetMethodID(env, regist->java_regist_host_class, "<init>", "("
|
|
||||||
"L"BASE_PACKAGE"/Target;" // target: Target
|
|
||||||
"Ljava/lang/String;" // apSsid: String
|
|
||||||
"Ljava/lang/String;" // apBssid: String
|
|
||||||
"Ljava/lang/String;" // apKey: String
|
|
||||||
"Ljava/lang/String;" // apName: String
|
|
||||||
"[B" // serverMac: ByteArray
|
|
||||||
"Ljava/lang/String;" // serverNickname: String
|
|
||||||
"[B" // rpRegistKey: ByteArray
|
|
||||||
"I" // rpKeyType: UInt
|
|
||||||
"[B" // rpKey: ByteArray
|
|
||||||
")V");
|
|
||||||
|
|
||||||
jclass regist_info_class = E->GetObjectClass(env, regist_info_obj);
|
|
||||||
|
|
||||||
jobject target_obj = E->GetObjectField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "target", "L"BASE_PACKAGE"/Target;"));
|
|
||||||
jclass target_class = E->GetObjectClass(env, target_obj);
|
|
||||||
jint target_value = E->GetIntField(env, target_obj, E->GetFieldID(env, target_class, "value", "I"));
|
|
||||||
|
|
||||||
jstring host_string = E->GetObjectField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "host", "Ljava/lang/String;"));
|
|
||||||
jboolean broadcast = E->GetBooleanField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "broadcast", "Z"));
|
|
||||||
jstring psn_online_id_string = E->GetObjectField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "psnOnlineId", "Ljava/lang/String;"));
|
|
||||||
jbyteArray psn_account_id_array = E->GetObjectField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "psnAccountId", "[B"));
|
|
||||||
jint pin = E->GetIntField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "pin", "I"));
|
|
||||||
|
|
||||||
ChiakiRegistInfo regist_info = { 0 };
|
|
||||||
regist_info.target = (ChiakiTarget)target_value;
|
|
||||||
regist_info.host = E->GetStringUTFChars(env, host_string, NULL);
|
|
||||||
regist_info.broadcast = broadcast;
|
|
||||||
if(psn_online_id_string)
|
|
||||||
regist_info.psn_online_id = E->GetStringUTFChars(env, psn_online_id_string, NULL);
|
|
||||||
if(psn_account_id_array && E->GetArrayLength(env, psn_account_id_array) == sizeof(regist_info.psn_account_id))
|
|
||||||
E->GetByteArrayRegion(env, psn_account_id_array, 0, sizeof(regist_info.psn_account_id), (jbyte *)regist_info.psn_account_id);
|
|
||||||
regist_info.pin = (uint32_t)pin;
|
|
||||||
|
|
||||||
err = chiaki_regist_start(®ist->regist, ®ist->log.log, ®ist_info, android_chiaki_regist_cb, regist);
|
|
||||||
|
|
||||||
E->ReleaseStringUTFChars(env, host_string, regist_info.host);
|
|
||||||
if(regist_info.psn_online_id)
|
|
||||||
E->ReleaseStringUTFChars(env, psn_online_id_string, regist_info.psn_online_id);
|
|
||||||
|
|
||||||
if(err != CHIAKI_ERR_SUCCESS)
|
|
||||||
{
|
|
||||||
android_chiaki_regist_fini_partial(env, regist);
|
|
||||||
free(regist);
|
|
||||||
regist = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
beach:
|
|
||||||
E->SetIntField(env, result, E->GetFieldID(env, result_class, "errorCode", "I"), (jint)err);
|
|
||||||
E->SetLongField(env, result, E->GetFieldID(env, result_class, "ptr", "J"), (jlong)regist);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JNI_FCN(registStop)(JNIEnv *env, jobject obj, jlong ptr)
|
|
||||||
{
|
|
||||||
AndroidChiakiRegist *regist = (AndroidChiakiRegist *)ptr;
|
|
||||||
chiaki_regist_stop(®ist->regist);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JNI_FCN(registFree)(JNIEnv *env, jobject obj, jlong ptr)
|
|
||||||
{
|
|
||||||
AndroidChiakiRegist *regist = (AndroidChiakiRegist *)ptr;
|
|
||||||
chiaki_regist_fini(®ist->regist);
|
|
||||||
android_chiaki_regist_fini_partial(env, regist);
|
|
||||||
free(regist);
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#ifndef CHIAKI_JNI_H
|
|
||||||
#define CHIAKI_JNI_H
|
|
||||||
|
|
||||||
#define JNI_VERSION JNI_VERSION_1_6
|
|
||||||
|
|
||||||
#define BASE_PACKAGE "com/metallic/chiaki/lib"
|
|
||||||
#define JNI_FCN(name) Java_com_metallic_chiaki_lib_ChiakiNative_##name
|
|
||||||
|
|
||||||
#define E (*env)
|
|
||||||
|
|
||||||
JNIEnv *attach_thread_jni();
|
|
||||||
jobject jnistr_from_ascii(JNIEnv *env, const char *str);
|
|
||||||
|
|
||||||
extern JavaVM *global_vm;
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1,138 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#ifndef CHIAKI_JNI_CIRCULARBUF_HPP
|
|
||||||
#define CHIAKI_JNI_CIRCULARBUF_HPP
|
|
||||||
|
|
||||||
#include "circular-fifo.hpp"
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
#include <assert.h>
|
|
||||||
|
|
||||||
#include <android/log.h>
|
|
||||||
|
|
||||||
template<size_t ChunksCount, size_t ChunkSize>
|
|
||||||
class CircularBuffer
|
|
||||||
{
|
|
||||||
static_assert(ChunksCount > 0, "ChunksCount > 0");
|
|
||||||
static_assert(ChunkSize > 0, "ChunkSize > 0");
|
|
||||||
|
|
||||||
private:
|
|
||||||
using Queue = memory_relaxed_aquire_release::CircularFifo<uint8_t *, ChunksCount>;
|
|
||||||
Queue full_queue;
|
|
||||||
Queue free_queue;
|
|
||||||
uint8_t *buffer;
|
|
||||||
|
|
||||||
uint8_t *push_chunk;
|
|
||||||
size_t push_chunk_size; // written bytes from the start of the chunk
|
|
||||||
|
|
||||||
uint8_t *pop_chunk;
|
|
||||||
size_t pop_chunk_size; // remaining bytes until the end of the chunk
|
|
||||||
|
|
||||||
void FlushChunks()
|
|
||||||
{
|
|
||||||
for(size_t i=1; i<ChunksCount; i++)
|
|
||||||
free_queue.push(buffer + i * ChunkSize);
|
|
||||||
|
|
||||||
push_chunk = buffer;
|
|
||||||
push_chunk_size = 0;
|
|
||||||
|
|
||||||
pop_chunk = nullptr;
|
|
||||||
pop_chunk_size = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
CircularBuffer() : buffer(new uint8_t[ChunksCount * ChunkSize])
|
|
||||||
{
|
|
||||||
FlushChunks();
|
|
||||||
}
|
|
||||||
|
|
||||||
~CircularBuffer()
|
|
||||||
{
|
|
||||||
delete [] buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the entire Buffer.
|
|
||||||
* WARNING: Not thread-safe at all! Call only when no producer and consumer is running.
|
|
||||||
*/
|
|
||||||
void Flush()
|
|
||||||
{
|
|
||||||
full_queue = Queue();
|
|
||||||
free_queue = Queue();
|
|
||||||
FlushChunks();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bytes that were pushed
|
|
||||||
*/
|
|
||||||
size_t Push(uint8_t *buf, size_t buf_size)
|
|
||||||
{
|
|
||||||
size_t pushed = 0;
|
|
||||||
while(pushed < buf_size)
|
|
||||||
{
|
|
||||||
if(!push_chunk)
|
|
||||||
{
|
|
||||||
if(!free_queue.pop(push_chunk))
|
|
||||||
push_chunk = nullptr;
|
|
||||||
if(!push_chunk)
|
|
||||||
return pushed;
|
|
||||||
push_chunk_size = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t to_push = buf_size - pushed;
|
|
||||||
size_t remaining_space = ChunkSize - push_chunk_size;
|
|
||||||
if(to_push > remaining_space)
|
|
||||||
to_push = remaining_space;
|
|
||||||
memcpy(push_chunk + push_chunk_size, buf + pushed, to_push);
|
|
||||||
pushed += to_push;
|
|
||||||
push_chunk_size += to_push;
|
|
||||||
|
|
||||||
if(push_chunk_size == ChunkSize)
|
|
||||||
{
|
|
||||||
bool success = full_queue.push(push_chunk);
|
|
||||||
assert(success); // We should have made the queues big enough
|
|
||||||
push_chunk = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pushed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bytes that were popped
|
|
||||||
*/
|
|
||||||
size_t Pop(uint8_t *buf, size_t buf_size)
|
|
||||||
{
|
|
||||||
size_t popped = 0;
|
|
||||||
while(popped < buf_size)
|
|
||||||
{
|
|
||||||
if(!pop_chunk)
|
|
||||||
{
|
|
||||||
if(!full_queue.pop(pop_chunk))
|
|
||||||
pop_chunk = nullptr;
|
|
||||||
if(!pop_chunk)
|
|
||||||
return popped;
|
|
||||||
pop_chunk_size = ChunkSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t to_pop = buf_size - popped;
|
|
||||||
if(to_pop > pop_chunk_size)
|
|
||||||
to_pop = pop_chunk_size;
|
|
||||||
memcpy(buf + popped, pop_chunk + (ChunkSize - pop_chunk_size), to_pop);
|
|
||||||
popped += to_pop;
|
|
||||||
pop_chunk_size -= to_pop;
|
|
||||||
|
|
||||||
if(pop_chunk_size == 0)
|
|
||||||
{
|
|
||||||
bool success = free_queue.push(pop_chunk);
|
|
||||||
assert(success);// We should have made the queues big enough
|
|
||||||
pop_chunk = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return popped;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#endif //CHIAKI_JNI_CIRCULARBUF_HPP
|
|
|
@ -1,108 +0,0 @@
|
||||||
/*
|
|
||||||
* Not any company's property but Public-Domain
|
|
||||||
* Do with source-code as you will. No requirement to keep this
|
|
||||||
* header if need to use it/change it/ or do whatever with it
|
|
||||||
*
|
|
||||||
* Note that there is No guarantee that this code will work
|
|
||||||
* and I take no responsibility for this code and any problems you
|
|
||||||
* might get if using it.
|
|
||||||
*
|
|
||||||
* Code & platform dependent issues with it was originally
|
|
||||||
* published at http://www.kjellkod.cc/threadsafecircularqueue
|
|
||||||
* 2012-16-19 @author Kjell Hedström, hedstrom@kjellkod.cc */
|
|
||||||
|
|
||||||
// should be mentioned the thinking of what goes where
|
|
||||||
// it is a "controversy" whether what is tail and what is head
|
|
||||||
// http://en.wikipedia.org/wiki/FIFO#Head_or_tail_first
|
|
||||||
|
|
||||||
#ifndef CIRCULARFIFO_AQUIRE_RELEASE_H_
|
|
||||||
#define CIRCULARFIFO_AQUIRE_RELEASE_H_
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <cstddef>
|
|
||||||
namespace memory_relaxed_aquire_release {
|
|
||||||
template<typename Element, size_t Size>
|
|
||||||
class CircularFifo{
|
|
||||||
public:
|
|
||||||
enum { Capacity = Size+1 };
|
|
||||||
|
|
||||||
CircularFifo() : _tail(0), _head(0){}
|
|
||||||
virtual ~CircularFifo() {}
|
|
||||||
|
|
||||||
bool push(const Element& item); // pushByMOve?
|
|
||||||
bool pop(Element& item);
|
|
||||||
|
|
||||||
bool wasEmpty() const;
|
|
||||||
bool wasFull() const;
|
|
||||||
bool isLockFree() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
size_t increment(size_t idx) const;
|
|
||||||
|
|
||||||
std::atomic <size_t> _tail; // tail(input) index
|
|
||||||
Element _array[Capacity];
|
|
||||||
std::atomic<size_t> _head; // head(output) index
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename Element, size_t Size>
|
|
||||||
bool CircularFifo<Element, Size>::push(const Element& item)
|
|
||||||
{
|
|
||||||
const auto current_tail = _tail.load(std::memory_order_relaxed);
|
|
||||||
const auto next_tail = increment(current_tail);
|
|
||||||
if(next_tail != _head.load(std::memory_order_acquire))
|
|
||||||
{
|
|
||||||
_array[current_tail] = item;
|
|
||||||
_tail.store(next_tail, std::memory_order_release);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false; // full queue
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Pop by Consumer can only update the head (load with relaxed, store with release)
|
|
||||||
// the tail must be accessed with at least aquire
|
|
||||||
template<typename Element, size_t Size>
|
|
||||||
bool CircularFifo<Element, Size>::pop(Element& item)
|
|
||||||
{
|
|
||||||
const auto current_head = _head.load(std::memory_order_relaxed);
|
|
||||||
if(current_head == _tail.load(std::memory_order_acquire))
|
|
||||||
return false; // empty queue
|
|
||||||
|
|
||||||
item = _array[current_head];
|
|
||||||
_head.store(increment(current_head), std::memory_order_release);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Element, size_t Size>
|
|
||||||
bool CircularFifo<Element, Size>::wasEmpty() const
|
|
||||||
{
|
|
||||||
// snapshot with acceptance of that this comparison operation is not atomic
|
|
||||||
return (_head.load() == _tail.load());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// snapshot with acceptance that this comparison is not atomic
|
|
||||||
template<typename Element, size_t Size>
|
|
||||||
bool CircularFifo<Element, Size>::wasFull() const
|
|
||||||
{
|
|
||||||
const auto next_tail = increment(_tail.load()); // aquire, we dont know who call
|
|
||||||
return (next_tail == _head.load());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
template<typename Element, size_t Size>
|
|
||||||
bool CircularFifo<Element, Size>::isLockFree() const
|
|
||||||
{
|
|
||||||
return (_tail.is_lock_free() && _head.is_lock_free());
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Element, size_t Size>
|
|
||||||
size_t CircularFifo<Element, Size>::increment(size_t idx) const
|
|
||||||
{
|
|
||||||
return (idx + 1) % Capacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // memory_relaxed_aquire_release
|
|
||||||
#endif /* CIRCULARFIFO_AQUIRE_RELEASE_H_ */
|
|
|
@ -1,131 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#include "log.h"
|
|
||||||
|
|
||||||
#include <android/log.h>
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <jni.h>
|
|
||||||
|
|
||||||
#include "chiaki-jni.h"
|
|
||||||
|
|
||||||
#define LOG_TAG "Chiaki"
|
|
||||||
|
|
||||||
void log_cb_android(ChiakiLogLevel level, const char *msg, void *user)
|
|
||||||
{
|
|
||||||
int prio;
|
|
||||||
switch(level)
|
|
||||||
{
|
|
||||||
case CHIAKI_LOG_DEBUG:
|
|
||||||
prio = ANDROID_LOG_DEBUG;
|
|
||||||
break;
|
|
||||||
case CHIAKI_LOG_VERBOSE:
|
|
||||||
prio = ANDROID_LOG_VERBOSE;
|
|
||||||
break;
|
|
||||||
case CHIAKI_LOG_INFO:
|
|
||||||
prio = ANDROID_LOG_INFO;
|
|
||||||
break;
|
|
||||||
case CHIAKI_LOG_WARNING:
|
|
||||||
prio = ANDROID_LOG_ERROR;
|
|
||||||
break;
|
|
||||||
case CHIAKI_LOG_ERROR:
|
|
||||||
prio = ANDROID_LOG_ERROR;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
prio = ANDROID_LOG_INFO;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
__android_log_write(prio, LOG_TAG, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void log_cb_android_file(ChiakiLogLevel level, const char *msg, void *user)
|
|
||||||
{
|
|
||||||
log_cb_android(level, msg, user);
|
|
||||||
FILE *f = user;
|
|
||||||
if(!f)
|
|
||||||
return;
|
|
||||||
switch(level)
|
|
||||||
{
|
|
||||||
case CHIAKI_LOG_DEBUG:
|
|
||||||
fwrite("[D] ", 4, 1, f);
|
|
||||||
break;
|
|
||||||
case CHIAKI_LOG_VERBOSE:
|
|
||||||
fwrite("[V] ", 4, 1, f);
|
|
||||||
break;
|
|
||||||
case CHIAKI_LOG_INFO:
|
|
||||||
fwrite("[I] ", 4, 1, f);
|
|
||||||
break;
|
|
||||||
case CHIAKI_LOG_WARNING:
|
|
||||||
fwrite("[W] ", 4, 1, f);
|
|
||||||
break;
|
|
||||||
case CHIAKI_LOG_ERROR:
|
|
||||||
fwrite("[E] ", 4, 1, f);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
fwrite("[?] ", 4, 1, f);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
fwrite(msg, strlen(msg), 1, f);
|
|
||||||
fwrite("\n", 1, 1, f);
|
|
||||||
}
|
|
||||||
|
|
||||||
ChiakiErrorCode android_chiaki_file_log_init(ChiakiLog *log, uint32_t level, const char *file)
|
|
||||||
{
|
|
||||||
chiaki_log_init(log, level, log_cb_android, NULL);
|
|
||||||
if(file)
|
|
||||||
{
|
|
||||||
FILE *f = fopen(file, "w+");
|
|
||||||
if(!f)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(log, "Failed to open log file %s for writing: %s", file, strerror(errno));
|
|
||||||
return CHIAKI_ERR_UNKNOWN;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
log->user = f;
|
|
||||||
log->cb = log_cb_android_file;
|
|
||||||
CHIAKI_LOGI(log, "Logging to file %s", file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return CHIAKI_ERR_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
void android_chiaki_file_log_fini(ChiakiLog *log)
|
|
||||||
{
|
|
||||||
if(log->user)
|
|
||||||
{
|
|
||||||
FILE *f = log->user;
|
|
||||||
fclose(f);
|
|
||||||
log->user = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static void android_chiaki_log_cb(ChiakiLogLevel level, const char *msg, void *user)
|
|
||||||
{
|
|
||||||
log_cb_android(level, msg, NULL);
|
|
||||||
|
|
||||||
AndroidChiakiJNILog *log = user;
|
|
||||||
JNIEnv *env = attach_thread_jni();
|
|
||||||
if(!env)
|
|
||||||
return;
|
|
||||||
E->CallVoidMethod(env, log->java_log, log->java_log_meth, (jint)level, jnistr_from_ascii(env, msg));
|
|
||||||
(*global_vm)->DetachCurrentThread(global_vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
void android_chiaki_jni_log_init(AndroidChiakiJNILog *log, JNIEnv *env, jobject java_log)
|
|
||||||
{
|
|
||||||
log->java_log = E->NewGlobalRef(env, java_log);
|
|
||||||
jclass log_class = E->GetObjectClass(env, log->java_log);
|
|
||||||
log->java_log_meth = E->GetMethodID(env, log_class, "log", "(ILjava/lang/String;)V");
|
|
||||||
log->log.level_mask = (uint32_t)E->GetIntField(env, log->java_log, E->GetFieldID(env, log_class, "levelMask", "I"));
|
|
||||||
log->log.cb = android_chiaki_log_cb;
|
|
||||||
log->log.user = log;
|
|
||||||
}
|
|
||||||
|
|
||||||
void android_chiaki_jni_log_fini(AndroidChiakiJNILog *log, JNIEnv *env)
|
|
||||||
{
|
|
||||||
E->DeleteGlobalRef(env, log->java_log);
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#ifndef CHIAKI_JNI_LOG_H
|
|
||||||
#define CHIAKI_JNI_LOG_H
|
|
||||||
|
|
||||||
#include <chiaki/log.h>
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
|
|
||||||
typedef struct android_jni_chiaki_log_t
|
|
||||||
{
|
|
||||||
jobject java_log;
|
|
||||||
jmethodID java_log_meth;
|
|
||||||
ChiakiLog log;
|
|
||||||
} AndroidChiakiJNILog;
|
|
||||||
|
|
||||||
ChiakiErrorCode android_chiaki_file_log_init(ChiakiLog *log, uint32_t level, const char *file);
|
|
||||||
void android_chiaki_file_log_fini(ChiakiLog *log);
|
|
||||||
|
|
||||||
void android_chiaki_jni_log_init(AndroidChiakiJNILog *log, JNIEnv *env, jobject java_log);
|
|
||||||
void android_chiaki_jni_log_fini(AndroidChiakiJNILog *log, JNIEnv *env);
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 8740d0fc321a55489dbbf6067298201b7d2e106d
|
|
|
@ -1,221 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#include "video-decoder.h"
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
|
|
||||||
#include <media/NdkMediaCodec.h>
|
|
||||||
#include <media/NdkMediaFormat.h>
|
|
||||||
#include <android/native_window_jni.h>
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define INPUT_BUFFER_TIMEOUT_MS 10
|
|
||||||
|
|
||||||
static void *android_chiaki_video_decoder_output_thread_func(void *user);
|
|
||||||
|
|
||||||
ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *decoder, ChiakiLog *log, int32_t target_width, int32_t target_height, ChiakiCodec codec)
|
|
||||||
{
|
|
||||||
decoder->log = log;
|
|
||||||
decoder->codec = NULL;
|
|
||||||
decoder->timestamp_cur = 0;
|
|
||||||
decoder->target_width = target_width;
|
|
||||||
decoder->target_height = target_height;
|
|
||||||
decoder->target_codec = codec;
|
|
||||||
decoder->shutdown_output = false;
|
|
||||||
return chiaki_mutex_init(&decoder->codec_mutex, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void kill_decoder(AndroidChiakiVideoDecoder *decoder)
|
|
||||||
{
|
|
||||||
chiaki_mutex_lock(&decoder->codec_mutex);
|
|
||||||
decoder->shutdown_output = true;
|
|
||||||
ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, 1000);
|
|
||||||
if(codec_buf_index >= 0)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer");
|
|
||||||
AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, 0, decoder->timestamp_cur++, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);
|
|
||||||
AMediaCodec_stop(decoder->codec);
|
|
||||||
chiaki_mutex_unlock(&decoder->codec_mutex);
|
|
||||||
chiaki_thread_join(&decoder->output_thread, NULL);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "Failed to get input buffer for shutting down Video Decoder!");
|
|
||||||
AMediaCodec_stop(decoder->codec);
|
|
||||||
chiaki_mutex_unlock(&decoder->codec_mutex);
|
|
||||||
}
|
|
||||||
AMediaCodec_delete(decoder->codec);
|
|
||||||
decoder->codec = NULL;
|
|
||||||
decoder->shutdown_output = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder)
|
|
||||||
{
|
|
||||||
if(decoder->codec)
|
|
||||||
kill_decoder(decoder);
|
|
||||||
chiaki_mutex_fini(&decoder->codec_mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void android_chiaki_video_decoder_set_surface(AndroidChiakiVideoDecoder *decoder, JNIEnv *env, jobject surface)
|
|
||||||
{
|
|
||||||
chiaki_mutex_lock(&decoder->codec_mutex);
|
|
||||||
|
|
||||||
if(!surface)
|
|
||||||
{
|
|
||||||
if(decoder->codec)
|
|
||||||
{
|
|
||||||
kill_decoder(decoder);
|
|
||||||
CHIAKI_LOGI(decoder->log, "Decoder shut down after surface was removed");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(decoder->codec)
|
|
||||||
{
|
|
||||||
#if __ANDROID_API__ >= 23
|
|
||||||
CHIAKI_LOGI(decoder->log, "Video decoder already initialized, swapping surface");
|
|
||||||
ANativeWindow *new_window = surface ? ANativeWindow_fromSurface(env, surface) : NULL;
|
|
||||||
AMediaCodec_setOutputSurface(decoder->codec, new_window);
|
|
||||||
ANativeWindow_release(decoder->window);
|
|
||||||
decoder->window = new_window;
|
|
||||||
#else
|
|
||||||
CHIAKI_LOGE(decoder->log, "Video Decoder already initialized");
|
|
||||||
#endif
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder->window = ANativeWindow_fromSurface(env, surface);
|
|
||||||
|
|
||||||
const char *mime = chiaki_codec_is_h265(decoder->target_codec) ? "video/hevc" : "video/avc";
|
|
||||||
CHIAKI_LOGI(decoder->log, "Initializing decoder with mime %s", mime);
|
|
||||||
|
|
||||||
decoder->codec = AMediaCodec_createDecoderByType(mime);
|
|
||||||
if(!decoder->codec)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "Failed to create AMediaCodec for mime type %s", mime);
|
|
||||||
goto error_surface;
|
|
||||||
}
|
|
||||||
|
|
||||||
AMediaFormat *format = AMediaFormat_new();
|
|
||||||
AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, mime);
|
|
||||||
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, decoder->target_width);
|
|
||||||
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, decoder->target_height);
|
|
||||||
|
|
||||||
media_status_t r = AMediaCodec_configure(decoder->codec, format, decoder->window, NULL, 0);
|
|
||||||
if(r != AMEDIA_OK)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "AMediaCodec_configure() failed: %d", (int)r);
|
|
||||||
AMediaFormat_delete(format);
|
|
||||||
goto error_codec;
|
|
||||||
}
|
|
||||||
|
|
||||||
r = AMediaCodec_start(decoder->codec);
|
|
||||||
AMediaFormat_delete(format);
|
|
||||||
if(r != AMEDIA_OK)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "AMediaCodec_start() failed: %d", (int)r);
|
|
||||||
goto error_codec;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChiakiErrorCode err = chiaki_thread_create(&decoder->output_thread, android_chiaki_video_decoder_output_thread_func, decoder);
|
|
||||||
if(err != CHIAKI_ERR_SUCCESS)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "Failed to create output thread for AMediaCodec");
|
|
||||||
goto error_codec;
|
|
||||||
}
|
|
||||||
|
|
||||||
goto beach;
|
|
||||||
|
|
||||||
error_codec:
|
|
||||||
AMediaCodec_delete(decoder->codec);
|
|
||||||
decoder->codec = NULL;
|
|
||||||
|
|
||||||
error_surface:
|
|
||||||
ANativeWindow_release(decoder->window);
|
|
||||||
decoder->window = NULL;
|
|
||||||
|
|
||||||
beach:
|
|
||||||
chiaki_mutex_unlock(&decoder->codec_mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool android_chiaki_video_decoder_video_sample(uint8_t *buf, size_t buf_size, void *user)
|
|
||||||
{
|
|
||||||
bool r = true;
|
|
||||||
AndroidChiakiVideoDecoder *decoder = user;
|
|
||||||
chiaki_mutex_lock(&decoder->codec_mutex);
|
|
||||||
|
|
||||||
if(!decoder->codec)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "Received video data, but decoder is not initialized!");
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
while(buf_size > 0)
|
|
||||||
{
|
|
||||||
ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, INPUT_BUFFER_TIMEOUT_MS * 1000);
|
|
||||||
if(codec_buf_index < 0)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "Failed to get input buffer");
|
|
||||||
r = false;
|
|
||||||
goto beach;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t codec_buf_size;
|
|
||||||
uint8_t *codec_buf = AMediaCodec_getInputBuffer(decoder->codec, (size_t)codec_buf_index, &codec_buf_size);
|
|
||||||
size_t codec_sample_size = buf_size;
|
|
||||||
if(codec_sample_size > codec_buf_size)
|
|
||||||
{
|
|
||||||
//CHIAKI_LOGD(decoder->log, "Sample is bigger than buffer, splitting");
|
|
||||||
codec_sample_size = codec_buf_size;
|
|
||||||
}
|
|
||||||
memcpy(codec_buf, buf, codec_sample_size);
|
|
||||||
media_status_t r = AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, codec_sample_size, decoder->timestamp_cur++, 0); // timestamp just raised by 1 for maximum realtime
|
|
||||||
if(r != AMEDIA_OK)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGE(decoder->log, "AMediaCodec_queueInputBuffer() failed: %d", (int)r);
|
|
||||||
}
|
|
||||||
buf += codec_sample_size;
|
|
||||||
buf_size -= codec_sample_size;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
beach:
|
|
||||||
chiaki_mutex_unlock(&decoder->codec_mutex);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void *android_chiaki_video_decoder_output_thread_func(void *user)
|
|
||||||
{
|
|
||||||
AndroidChiakiVideoDecoder *decoder = user;
|
|
||||||
|
|
||||||
while(1)
|
|
||||||
{
|
|
||||||
AMediaCodecBufferInfo info;
|
|
||||||
ssize_t status = AMediaCodec_dequeueOutputBuffer(decoder->codec, &info, -1);
|
|
||||||
if(status >= 0)
|
|
||||||
{
|
|
||||||
AMediaCodec_releaseOutputBuffer(decoder->codec, (size_t)status, info.size != 0);
|
|
||||||
if(info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGI(decoder->log, "AMediaCodec reported EOS");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
chiaki_mutex_lock(&decoder->codec_mutex);
|
|
||||||
bool shutdown = decoder->shutdown_output;
|
|
||||||
chiaki_mutex_unlock(&decoder->codec_mutex);
|
|
||||||
if(shutdown)
|
|
||||||
{
|
|
||||||
CHIAKI_LOGI(decoder->log, "Video Decoder Output Thread detected shutdown after reported error");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CHIAKI_LOGI(decoder->log, "Video Decoder Output Thread exiting");
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
#ifndef CHIAKI_JNI_VIDEO_DECODER_H
|
|
||||||
#define CHIAKI_JNI_VIDEO_DECODER_H
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
|
|
||||||
#include <chiaki/thread.h>
|
|
||||||
#include <chiaki/log.h>
|
|
||||||
|
|
||||||
typedef struct AMediaCodec AMediaCodec;
|
|
||||||
typedef struct ANativeWindow ANativeWindow;
|
|
||||||
|
|
||||||
typedef struct android_chiaki_video_decoder_t
|
|
||||||
{
|
|
||||||
ChiakiLog *log;
|
|
||||||
ChiakiMutex codec_mutex;
|
|
||||||
AMediaCodec *codec;
|
|
||||||
ANativeWindow *window;
|
|
||||||
uint64_t timestamp_cur;
|
|
||||||
ChiakiThread output_thread;
|
|
||||||
bool shutdown_output;
|
|
||||||
int32_t target_width;
|
|
||||||
int32_t target_height;
|
|
||||||
ChiakiCodec target_codec;
|
|
||||||
} AndroidChiakiVideoDecoder;
|
|
||||||
|
|
||||||
ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *decoder, ChiakiLog *log, int32_t target_width, int32_t target_height, ChiakiCodec codec);
|
|
||||||
void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder);
|
|
||||||
void android_chiaki_video_decoder_set_surface(AndroidChiakiVideoDecoder *decoder, JNIEnv *env, jobject surface);
|
|
||||||
bool android_chiaki_video_decoder_video_sample(uint8_t *buf, size_t buf_size, void *user);
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1,63 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.room.*
|
|
||||||
import androidx.room.migration.Migration
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import com.metallic.chiaki.lib.Target
|
|
||||||
|
|
||||||
@Database(
|
|
||||||
version = 2,
|
|
||||||
entities = [RegisteredHost::class, ManualHost::class])
|
|
||||||
@TypeConverters(Converters::class)
|
|
||||||
abstract class AppDatabase: RoomDatabase()
|
|
||||||
{
|
|
||||||
abstract fun registeredHostDao(): RegisteredHostDao
|
|
||||||
abstract fun manualHostDao(): ManualHostDao
|
|
||||||
abstract fun importDao(): ImportDao
|
|
||||||
}
|
|
||||||
|
|
||||||
val MIGRATION_1_2 = object : Migration(1, 2)
|
|
||||||
{
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase)
|
|
||||||
{
|
|
||||||
database.execSQL("ALTER TABLE registered_host ADD target INTEGER NOT NULL DEFAULT 1000")
|
|
||||||
database.execSQL("CREATE TABLE `new_registered_host` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `target` INTEGER NOT NULL, `ap_ssid` TEXT, `ap_bssid` TEXT, `ap_key` TEXT, `ap_name` TEXT, `server_mac` INTEGER NOT NULL, `server_nickname` TEXT, `rp_regist_key` BLOB NOT NULL, `rp_key_type` INTEGER NOT NULL, `rp_key` BLOB NOT NULL)");
|
|
||||||
database.execSQL("INSERT INTO `new_registered_host` SELECT `id`, `target`, `ap_ssid`, `ap_bssid`, `ap_key`, `ap_name`, `ps4_mac`, `ps4_nickname`, `rp_regist_key`, `rp_key_type`, `rp_key` FROM `registered_host`")
|
|
||||||
database.execSQL("DROP TABLE registered_host")
|
|
||||||
database.execSQL("ALTER TABLE new_registered_host RENAME TO registered_host")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var database: AppDatabase? = null
|
|
||||||
fun getDatabase(context: Context): AppDatabase
|
|
||||||
{
|
|
||||||
val currentDb = database
|
|
||||||
if(currentDb != null)
|
|
||||||
return currentDb
|
|
||||||
val db = Room.databaseBuilder(
|
|
||||||
context.applicationContext,
|
|
||||||
AppDatabase::class.java,
|
|
||||||
"chiaki")
|
|
||||||
.addMigrations(MIGRATION_1_2)
|
|
||||||
.build()
|
|
||||||
database = db
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Converters
|
|
||||||
{
|
|
||||||
@TypeConverter
|
|
||||||
fun macFromValue(v: Long) = MacAddress(v)
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
fun macToValue(addr: MacAddress) = addr.value
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
fun targetFromValue(v: Int) = Target.fromValue(v)
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
fun targetToValue(target: Target) = target.value
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common
|
|
||||||
|
|
||||||
import com.metallic.chiaki.lib.DiscoveryHost
|
|
||||||
|
|
||||||
sealed class DisplayHost
|
|
||||||
{
|
|
||||||
abstract val registeredHost: RegisteredHost?
|
|
||||||
abstract val host: String
|
|
||||||
abstract val name: String?
|
|
||||||
abstract val id: String?
|
|
||||||
abstract val isPS5: Boolean
|
|
||||||
|
|
||||||
val isRegistered get() = registeredHost != null
|
|
||||||
}
|
|
||||||
|
|
||||||
class DiscoveredDisplayHost(
|
|
||||||
override val registeredHost: RegisteredHost?,
|
|
||||||
val discoveredHost: DiscoveryHost
|
|
||||||
): DisplayHost()
|
|
||||||
{
|
|
||||||
override val host get() = discoveredHost.hostAddr ?: ""
|
|
||||||
override val name get() = discoveredHost.hostName ?: registeredHost?.serverNickname
|
|
||||||
override val id get() = discoveredHost.hostId ?: registeredHost?.serverMac?.toString()
|
|
||||||
override val isPS5 get() = discoveredHost.isPS5
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean =
|
|
||||||
if(other !is DiscoveredDisplayHost)
|
|
||||||
false
|
|
||||||
else
|
|
||||||
other.discoveredHost == discoveredHost && other.registeredHost == registeredHost
|
|
||||||
|
|
||||||
override fun hashCode() = 31 * (registeredHost?.hashCode() ?: 0) + discoveredHost.hashCode()
|
|
||||||
|
|
||||||
override fun toString() = "DiscoveredDisplayHost{${registeredHost}, ${discoveredHost}}"
|
|
||||||
}
|
|
||||||
|
|
||||||
class ManualDisplayHost(
|
|
||||||
override val registeredHost: RegisteredHost?,
|
|
||||||
val manualHost: ManualHost
|
|
||||||
): DisplayHost()
|
|
||||||
{
|
|
||||||
override val host get() = manualHost.host
|
|
||||||
override val name get() = registeredHost?.serverNickname
|
|
||||||
override val id get() = registeredHost?.serverMac?.toString()
|
|
||||||
override val isPS5: Boolean get() = registeredHost?.target?.isPS5 ?: false
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean =
|
|
||||||
if(other !is ManualDisplayHost)
|
|
||||||
false
|
|
||||||
else
|
|
||||||
other.manualHost == manualHost && other.registeredHost == registeredHost
|
|
||||||
|
|
||||||
override fun hashCode() = 31 * (registeredHost?.hashCode() ?: 0) + manualHost.hashCode()
|
|
||||||
|
|
||||||
override fun toString() = "ManualDisplayHost{${registeredHost}, ${manualHost}}"
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FilenameFilter
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
val fileProviderAuthority = "com.metallic.chiaki.fileprovider"
|
|
||||||
private const val baseDirName = "session_logs" // must be in sync with filepaths.xml
|
|
||||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss-SSS", Locale.US)
|
|
||||||
private const val filePrefix = "chiaki_session_"
|
|
||||||
private const val filePostfix = ".log"
|
|
||||||
private val fileRegex = Regex("$filePrefix(.*)$filePostfix")
|
|
||||||
private const val keepLogFilesCount = 5
|
|
||||||
|
|
||||||
class LogFile private constructor(val logManager: LogManager, val filename: String)
|
|
||||||
{
|
|
||||||
val date = fileRegex.matchEntire(filename)?.groupValues?.get(1)?.let {
|
|
||||||
dateFormat.parse(it)
|
|
||||||
} ?: throw IllegalArgumentException()
|
|
||||||
|
|
||||||
val file get() = File(logManager.baseDir, filename)
|
|
||||||
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
fun fromFilename(logManager: LogManager, filename: String) = try { LogFile(logManager, filename) } catch(e: IllegalArgumentException) { null }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogManager(context: Context)
|
|
||||||
{
|
|
||||||
val baseDir = File(context.filesDir, baseDirName).also {
|
|
||||||
it.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
val files: List<LogFile> get() =
|
|
||||||
(baseDir.list { _, s -> s.matches(fileRegex) }?.toList() ?: listOf()).mapNotNull {
|
|
||||||
LogFile.fromFilename(this, it)
|
|
||||||
}.sortedByDescending {
|
|
||||||
it.date
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createNewFile(): LogFile
|
|
||||||
{
|
|
||||||
val currentFiles = files
|
|
||||||
if(currentFiles.size > keepLogFilesCount)
|
|
||||||
{
|
|
||||||
currentFiles.subList(keepLogFilesCount, currentFiles.size).forEach {
|
|
||||||
it.file.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val date = Date()
|
|
||||||
val filename = "$filePrefix${dateFormat.format(date)}$filePostfix"
|
|
||||||
return LogFile.fromFilename(this, filename)!!
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common
|
|
||||||
|
|
||||||
import com.squareup.moshi.FromJson
|
|
||||||
import com.squareup.moshi.JsonDataException
|
|
||||||
import com.squareup.moshi.ToJson
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
|
|
||||||
class MacAddress(v: Long)
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
val LENGTH = 6
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(data: ByteArray) : this(
|
|
||||||
if(data.size != LENGTH)
|
|
||||||
throw IllegalArgumentException("Data has invalid length for MAC")
|
|
||||||
else
|
|
||||||
data.let {
|
|
||||||
val buf = ByteBuffer.allocate(8)
|
|
||||||
buf.put(it, 0, LENGTH)
|
|
||||||
buf.order(ByteOrder.LITTLE_ENDIAN)
|
|
||||||
buf.getLong(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
constructor(string: String) : this(
|
|
||||||
(Regex("([0-9A-Fa-f]{2})[:-]([0-9A-Fa-f]{2})[:-]([0-9A-Fa-f]{2})[:-]([0-9A-Fa-f]{2})[:-]([0-9A-Fa-f]{2})[:-]([0-9A-Fa-f]{2})").matchEntire(string)
|
|
||||||
?: throw IllegalArgumentException("Invalid MAC Address String"))
|
|
||||||
.groupValues
|
|
||||||
.subList(1, 7)
|
|
||||||
.map { it.toUByte(16).toByte() }
|
|
||||||
.toByteArray())
|
|
||||||
|
|
||||||
val value: Long = v and 0xffffffffffff
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean =
|
|
||||||
if(other is MacAddress)
|
|
||||||
other.value == value
|
|
||||||
else
|
|
||||||
super.equals(other)
|
|
||||||
|
|
||||||
override fun hashCode() = value.hashCode()
|
|
||||||
|
|
||||||
override fun toString(): String = "%02x:%02x:%02x:%02x:%02x:%02x".format(
|
|
||||||
value and 0xff,
|
|
||||||
(value shr 0x8) and 0xff,
|
|
||||||
(value shr 0x10) and 0xff,
|
|
||||||
(value shr 0x18) and 0xff,
|
|
||||||
(value shr 0x20) and 0xff,
|
|
||||||
(value shr 0x28) and 0xff
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class MacAddressJsonAdapter
|
|
||||||
{
|
|
||||||
@ToJson fun toJson(macAddress: MacAddress) = macAddress.toString()
|
|
||||||
@FromJson fun fromJson(string: String) = try { MacAddress(string) } catch(e: IllegalArgumentException) { throw JsonDataException(e.message) }
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common
|
|
||||||
|
|
||||||
import androidx.room.*
|
|
||||||
import androidx.room.ForeignKey.Companion.SET_NULL
|
|
||||||
import io.reactivex.Completable
|
|
||||||
import io.reactivex.Flowable
|
|
||||||
import io.reactivex.Single
|
|
||||||
|
|
||||||
@Entity(tableName = "manual_host",
|
|
||||||
foreignKeys = [
|
|
||||||
ForeignKey(
|
|
||||||
entity = RegisteredHost::class,
|
|
||||||
parentColumns = ["id"],
|
|
||||||
childColumns = ["registered_host"],
|
|
||||||
onDelete = SET_NULL
|
|
||||||
)
|
|
||||||
])
|
|
||||||
data class ManualHost(
|
|
||||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
|
||||||
val host: String,
|
|
||||||
@ColumnInfo(name = "registered_host") val registeredHost: Long?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ManualHostAndRegisteredHost(
|
|
||||||
@Embedded(prefix = "manual_host_") val manualHost: ManualHost,
|
|
||||||
@Embedded val registeredHost: RegisteredHost?
|
|
||||||
)
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface ManualHostDao
|
|
||||||
{
|
|
||||||
@Query("SELECT * FROM manual_host WHERE id = :id")
|
|
||||||
fun getById(id: Long): Single<ManualHost>
|
|
||||||
|
|
||||||
@Query("""SELECT
|
|
||||||
manual_host.id as manual_host_id,
|
|
||||||
manual_host.host as manual_host_host,
|
|
||||||
manual_host.registered_host as manual_host_registered_host,
|
|
||||||
registered_host.*
|
|
||||||
FROM manual_host LEFT OUTER JOIN registered_host ON manual_host.registered_host = registered_host.id WHERE manual_host.id = :id""")
|
|
||||||
fun getByIdWithRegisteredHost(id: Long): Single<ManualHostAndRegisteredHost>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM manual_host")
|
|
||||||
fun getAll(): Flowable<List<ManualHost>>
|
|
||||||
|
|
||||||
@Query("UPDATE manual_host SET registered_host = :registeredHostId WHERE id = :manualHostId")
|
|
||||||
fun assignRegisteredHost(manualHostId: Long, registeredHostId: Long?): Completable
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
fun insert(host: ManualHost): Completable
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
fun delete(host: ManualHost): Completable
|
|
||||||
|
|
||||||
@Update
|
|
||||||
fun update(host: ManualHost): Completable
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.lib.Codec
|
|
||||||
import com.metallic.chiaki.lib.ConnectVideoProfile
|
|
||||||
import com.metallic.chiaki.lib.VideoFPSPreset
|
|
||||||
import com.metallic.chiaki.lib.VideoResolutionPreset
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.subjects.BehaviorSubject
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
class Preferences(context: Context)
|
|
||||||
{
|
|
||||||
enum class Resolution(val value: String, @StringRes val title: Int, val preset: VideoResolutionPreset)
|
|
||||||
{
|
|
||||||
RES_360P("360p", R.string.preferences_resolution_title_360p, VideoResolutionPreset.RES_360P),
|
|
||||||
RES_540P("540p", R.string.preferences_resolution_title_540p, VideoResolutionPreset.RES_540P),
|
|
||||||
RES_720P("720p", R.string.preferences_resolution_title_720p, VideoResolutionPreset.RES_720P),
|
|
||||||
RES_1080P("1080p", R.string.preferences_resolution_title_1080p, VideoResolutionPreset.RES_1080P),
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class FPS(val value: String, @StringRes val title: Int, val preset: VideoFPSPreset)
|
|
||||||
{
|
|
||||||
FPS_30("30", R.string.preferences_fps_title_30, VideoFPSPreset.FPS_30),
|
|
||||||
FPS_60("60", R.string.preferences_fps_title_60, VideoFPSPreset.FPS_60)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Codec(val value: String, @StringRes val title: Int, val codec: com.metallic.chiaki.lib.Codec)
|
|
||||||
{
|
|
||||||
CODEC_H264("h264", R.string.preferences_codec_title_h264, com.metallic.chiaki.lib.Codec.CODEC_H264),
|
|
||||||
CODEC_H265("h265", R.string.preferences_codec_title_h265, com.metallic.chiaki.lib.Codec.CODEC_H265)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
val resolutionDefault = Resolution.RES_720P
|
|
||||||
val resolutionAll = Resolution.values()
|
|
||||||
val fpsDefault = FPS.FPS_60
|
|
||||||
val fpsAll = FPS.values()
|
|
||||||
val codecDefault = Codec.CODEC_H265
|
|
||||||
val codecAll = Codec.values()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
|
||||||
when(key)
|
|
||||||
{
|
|
||||||
resolutionKey -> bitrateAutoSubject.onNext(bitrateAuto)
|
|
||||||
}
|
|
||||||
}.also { sharedPreferences.registerOnSharedPreferenceChangeListener(it) }
|
|
||||||
|
|
||||||
private val resources = context.resources
|
|
||||||
|
|
||||||
val discoveryEnabledKey get() = resources.getString(R.string.preferences_discovery_enabled_key)
|
|
||||||
var discoveryEnabled
|
|
||||||
get() = sharedPreferences.getBoolean(discoveryEnabledKey, true)
|
|
||||||
set(value) { sharedPreferences.edit().putBoolean(discoveryEnabledKey, value).apply() }
|
|
||||||
|
|
||||||
val onScreenControlsEnabledKey get() = resources.getString(R.string.preferences_on_screen_controls_enabled_key)
|
|
||||||
var onScreenControlsEnabled
|
|
||||||
get() = sharedPreferences.getBoolean(onScreenControlsEnabledKey, true)
|
|
||||||
set(value) { sharedPreferences.edit().putBoolean(onScreenControlsEnabledKey, value).apply() }
|
|
||||||
|
|
||||||
val touchpadOnlyEnabledKey get() = resources.getString(R.string.preferences_touchpad_only_enabled_key)
|
|
||||||
var touchpadOnlyEnabled
|
|
||||||
get() = sharedPreferences.getBoolean(touchpadOnlyEnabledKey, false)
|
|
||||||
set(value) { sharedPreferences.edit().putBoolean(touchpadOnlyEnabledKey, value).apply() }
|
|
||||||
|
|
||||||
val rumbleEnabledKey get() = resources.getString(R.string.preferences_rumble_enabled_key)
|
|
||||||
var rumbleEnabled
|
|
||||||
get() = sharedPreferences.getBoolean(rumbleEnabledKey, true)
|
|
||||||
set(value) { sharedPreferences.edit().putBoolean(rumbleEnabledKey, value).apply() }
|
|
||||||
|
|
||||||
val motionEnabledKey get() = resources.getString(R.string.preferences_motion_enabled_key)
|
|
||||||
var motionEnabled
|
|
||||||
get() = sharedPreferences.getBoolean(motionEnabledKey, true)
|
|
||||||
set(value) { sharedPreferences.edit().putBoolean(motionEnabledKey, value).apply() }
|
|
||||||
|
|
||||||
val buttonHapticEnabledKey get() = resources.getString(R.string.preferences_button_haptic_enabled_key)
|
|
||||||
var buttonHapticEnabled
|
|
||||||
get() = sharedPreferences.getBoolean(buttonHapticEnabledKey, true)
|
|
||||||
set(value) { sharedPreferences.edit().putBoolean(buttonHapticEnabledKey, value).apply() }
|
|
||||||
|
|
||||||
val logVerboseKey get() = resources.getString(R.string.preferences_log_verbose_key)
|
|
||||||
var logVerbose
|
|
||||||
get() = sharedPreferences.getBoolean(logVerboseKey, false)
|
|
||||||
set(value) { sharedPreferences.edit().putBoolean(logVerboseKey, value).apply() }
|
|
||||||
|
|
||||||
val swapCrossMoonKey get() = resources.getString(R.string.preferences_swap_cross_moon_key)
|
|
||||||
var swapCrossMoon
|
|
||||||
get() = sharedPreferences.getBoolean(swapCrossMoonKey, false)
|
|
||||||
set(value) { sharedPreferences.edit().putBoolean(swapCrossMoonKey, value).apply() }
|
|
||||||
|
|
||||||
val resolutionKey get() = resources.getString(R.string.preferences_resolution_key)
|
|
||||||
var resolution
|
|
||||||
get() = sharedPreferences.getString(resolutionKey, resolutionDefault.value)?.let { value ->
|
|
||||||
Resolution.values().firstOrNull { it.value == value }
|
|
||||||
} ?: resolutionDefault
|
|
||||||
set(value) { sharedPreferences.edit().putString(resolutionKey, value.value).apply() }
|
|
||||||
|
|
||||||
val fpsKey get() = resources.getString(R.string.preferences_fps_key)
|
|
||||||
var fps
|
|
||||||
get() = sharedPreferences.getString(fpsKey, fpsDefault.value)?.let { value ->
|
|
||||||
FPS.values().firstOrNull { it.value == value }
|
|
||||||
} ?: fpsDefault
|
|
||||||
set(value) { sharedPreferences.edit().putString(fpsKey, value.value).apply() }
|
|
||||||
|
|
||||||
fun validateBitrate(bitrate: Int) = max(2000, min(50000, bitrate))
|
|
||||||
val bitrateKey get() = resources.getString(R.string.preferences_bitrate_key)
|
|
||||||
var bitrate
|
|
||||||
get() = sharedPreferences.getInt(bitrateKey, 0).let { if(it == 0) null else validateBitrate(it) }
|
|
||||||
set(value) { sharedPreferences.edit().putInt(bitrateKey, if(value != null) validateBitrate(value) else 0).apply() }
|
|
||||||
val bitrateAuto get() = videoProfileDefaultBitrate.bitrate
|
|
||||||
private val bitrateAutoSubject by lazy { BehaviorSubject.createDefault(bitrateAuto) }
|
|
||||||
val bitrateAutoObservable: Observable<Int> get() = bitrateAutoSubject
|
|
||||||
|
|
||||||
val codecKey get() = resources.getString(R.string.preferences_codec_key)
|
|
||||||
var codec
|
|
||||||
get() = sharedPreferences.getString(codecKey, codecDefault.value)?.let { value ->
|
|
||||||
Codec.values().firstOrNull { it.value == value }
|
|
||||||
} ?: codecDefault
|
|
||||||
set(value) { sharedPreferences.edit().putString(codecKey, value.value).apply() }
|
|
||||||
|
|
||||||
private val videoProfileDefaultBitrate get() = ConnectVideoProfile.preset(resolution.preset, fps.preset, codec.codec)
|
|
||||||
val videoProfile get() = videoProfileDefaultBitrate.let {
|
|
||||||
val bitrate = bitrate
|
|
||||||
if(bitrate == null)
|
|
||||||
it
|
|
||||||
else
|
|
||||||
it.copy(bitrate = bitrate)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common
|
|
||||||
|
|
||||||
import androidx.room.*
|
|
||||||
import androidx.room.ColumnInfo.Companion.BLOB
|
|
||||||
import com.metallic.chiaki.lib.RegistHost
|
|
||||||
import com.metallic.chiaki.lib.Target
|
|
||||||
import io.reactivex.Completable
|
|
||||||
import io.reactivex.Flowable
|
|
||||||
import io.reactivex.Maybe
|
|
||||||
import io.reactivex.Single
|
|
||||||
|
|
||||||
@Entity(tableName = "registered_host")
|
|
||||||
data class RegisteredHost(
|
|
||||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
|
||||||
@ColumnInfo(name = "target") val target: Target,
|
|
||||||
@ColumnInfo(name = "ap_ssid") val apSsid: String?,
|
|
||||||
@ColumnInfo(name = "ap_bssid") val apBssid: String?,
|
|
||||||
@ColumnInfo(name = "ap_key") val apKey: String?,
|
|
||||||
@ColumnInfo(name = "ap_name") val apName: String?,
|
|
||||||
@ColumnInfo(name = "server_mac") val serverMac: MacAddress,
|
|
||||||
@ColumnInfo(name = "server_nickname") val serverNickname: String?,
|
|
||||||
@ColumnInfo(name = "rp_regist_key", typeAffinity = BLOB) val rpRegistKey: ByteArray, // CHIAKI_SESSION_AUTH_SIZE
|
|
||||||
@ColumnInfo(name = "rp_key_type") val rpKeyType: Int,
|
|
||||||
@ColumnInfo(name = "rp_key", typeAffinity = BLOB) val rpKey: ByteArray // 0x10
|
|
||||||
)
|
|
||||||
{
|
|
||||||
constructor(registHost: RegistHost) : this(
|
|
||||||
target = registHost.target,
|
|
||||||
apSsid = registHost.apSsid,
|
|
||||||
apBssid = registHost.apBssid,
|
|
||||||
apKey = registHost.apKey,
|
|
||||||
apName = registHost.apName,
|
|
||||||
serverMac = MacAddress(registHost.serverMac),
|
|
||||||
serverNickname = registHost.serverNickname,
|
|
||||||
rpRegistKey = registHost.rpRegistKey,
|
|
||||||
rpKeyType = registHost.rpKeyType.toInt(),
|
|
||||||
rpKey = registHost.rpKey
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean
|
|
||||||
{
|
|
||||||
if(this === other) return true
|
|
||||||
if(javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as RegisteredHost
|
|
||||||
|
|
||||||
if(id != other.id) return false
|
|
||||||
if(target != other.target) return false
|
|
||||||
if(apSsid != other.apSsid) return false
|
|
||||||
if(apBssid != other.apBssid) return false
|
|
||||||
if(apKey != other.apKey) return false
|
|
||||||
if(apName != other.apName) return false
|
|
||||||
if(serverMac != other.serverMac) return false
|
|
||||||
if(serverNickname != other.serverNickname) return false
|
|
||||||
if(!rpRegistKey.contentEquals(other.rpRegistKey)) return false
|
|
||||||
if(rpKeyType != other.rpKeyType) return false
|
|
||||||
if(!rpKey.contentEquals(other.rpKey)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int
|
|
||||||
{
|
|
||||||
var result = id.hashCode()
|
|
||||||
result = 31 * result + target.hashCode()
|
|
||||||
result = 31 * result + (apSsid?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (apBssid?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (apKey?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (apName?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + serverMac.hashCode()
|
|
||||||
result = 31 * result + (serverNickname?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + rpRegistKey.contentHashCode()
|
|
||||||
result = 31 * result + rpKeyType
|
|
||||||
result = 31 * result + rpKey.contentHashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface RegisteredHostDao
|
|
||||||
{
|
|
||||||
@Query("SELECT * FROM registered_host")
|
|
||||||
fun getAll(): Flowable<List<RegisteredHost>>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM registered_host WHERE server_mac == :mac LIMIT 1")
|
|
||||||
fun getByMac(mac: MacAddress): Maybe<RegisteredHost>
|
|
||||||
|
|
||||||
@Query("DELETE FROM registered_host WHERE server_mac == :mac")
|
|
||||||
fun deleteByMac(mac: MacAddress): Completable
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
fun delete(host: RegisteredHost): Completable
|
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM registered_host")
|
|
||||||
fun count(): Flowable<Int>
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
fun insert(host: RegisteredHost): Single<Long>
|
|
||||||
}
|
|
|
@ -1,289 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Base64
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.room.*
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.lib.Target
|
|
||||||
import com.squareup.moshi.*
|
|
||||||
import io.reactivex.Completable
|
|
||||||
import io.reactivex.Flowable
|
|
||||||
import io.reactivex.Single
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.disposables.Disposable
|
|
||||||
import io.reactivex.rxkotlin.Singles
|
|
||||||
import io.reactivex.rxkotlin.addTo
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import okio.Buffer
|
|
||||||
import okio.Okio
|
|
||||||
import okio.buffer
|
|
||||||
import okio.source
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
class SerializedRegisteredHost(
|
|
||||||
@Json(name = "target") val target: Target,
|
|
||||||
@Json(name = "ap_ssid") val apSsid: String?,
|
|
||||||
@Json(name = "ap_bssid") val apBssid: String?,
|
|
||||||
@Json(name = "ap_key") val apKey: String?,
|
|
||||||
@Json(name = "ap_name") val apName: String?,
|
|
||||||
@Json(name = "server_mac") val serverMac: MacAddress,
|
|
||||||
@Json(name = "server_nickname") val serverNickname: String?,
|
|
||||||
@Json(name = "rp_regist_key") val rpRegistKey: ByteArray,
|
|
||||||
@Json(name = "rp_key_type") val rpKeyType: Int,
|
|
||||||
@Json(name = "rp_key") val rpKey: ByteArray
|
|
||||||
){
|
|
||||||
constructor(registeredHost: RegisteredHost) : this(
|
|
||||||
registeredHost.target,
|
|
||||||
registeredHost.apSsid,
|
|
||||||
registeredHost.apBssid,
|
|
||||||
registeredHost.apKey,
|
|
||||||
registeredHost.apName,
|
|
||||||
registeredHost.serverMac,
|
|
||||||
registeredHost.serverNickname,
|
|
||||||
registeredHost.rpRegistKey,
|
|
||||||
registeredHost.rpKeyType,
|
|
||||||
registeredHost.rpKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
class SerializedManualHost(
|
|
||||||
@Json(name = "host") val host: String,
|
|
||||||
@Json(name = "server_mac") val serverMac: MacAddress?
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class SerializedSettings(
|
|
||||||
@Json(name = "registered_hosts") val registeredHosts: List<SerializedRegisteredHost>,
|
|
||||||
@Json(name = "manual_hosts") val manualHosts: List<SerializedManualHost>
|
|
||||||
)
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
fun fromDatabase(db: AppDatabase) = Singles.zip(
|
|
||||||
db.registeredHostDao().getAll().firstOrError(),
|
|
||||||
db.manualHostDao().getAll().firstOrError()
|
|
||||||
) { registeredHosts, manualHosts ->
|
|
||||||
SerializedSettings(
|
|
||||||
registeredHosts.map { SerializedRegisteredHost(it) },
|
|
||||||
manualHosts.map { manualHost ->
|
|
||||||
SerializedManualHost(
|
|
||||||
manualHost.host,
|
|
||||||
manualHost.registeredHost?.let { registeredHostId ->
|
|
||||||
registeredHosts.firstOrNull { it.id == registeredHostId }
|
|
||||||
}?.serverMac
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ByteArrayJsonAdapter
|
|
||||||
{
|
|
||||||
@ToJson fun toJson(byteArray: ByteArray) = Base64.encodeToString(byteArray, Base64.NO_WRAP)
|
|
||||||
@FromJson fun fromJson(string: String) = Base64.decode(string, Base64.DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun moshi() =
|
|
||||||
Moshi.Builder()
|
|
||||||
.add(MacAddressJsonAdapter())
|
|
||||||
.add(ByteArrayJsonAdapter())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private fun Moshi.serializedSettingsAdapter() =
|
|
||||||
adapter(SerializedSettings::class.java)
|
|
||||||
.serializeNulls()
|
|
||||||
|
|
||||||
private const val KEY_FORMAT = "format"
|
|
||||||
private const val FORMAT = "chiaki-settings"
|
|
||||||
private const val KEY_VERSION = "version"
|
|
||||||
private const val VERSION = 2
|
|
||||||
private const val KEY_SETTINGS = "settings"
|
|
||||||
|
|
||||||
fun exportAllSettings(db: AppDatabase) = SerializedSettings.fromDatabase(db)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.map {
|
|
||||||
val buffer = Buffer()
|
|
||||||
val writer = JsonWriter.of(buffer)
|
|
||||||
val adapter = moshi().serializedSettingsAdapter()
|
|
||||||
writer.indent = " "
|
|
||||||
writer.
|
|
||||||
beginObject()
|
|
||||||
.name(KEY_FORMAT).value(FORMAT)
|
|
||||||
.name(KEY_VERSION).value(VERSION)
|
|
||||||
writer.name(KEY_SETTINGS)
|
|
||||||
adapter.toJson(writer, it)
|
|
||||||
writer.endObject()
|
|
||||||
buffer.readUtf8()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exportAndShareAllSettings(activity: Activity): Disposable
|
|
||||||
{
|
|
||||||
val db = getDatabase(activity)
|
|
||||||
val dir = File(activity.cacheDir, "export_settings")
|
|
||||||
dir.mkdirs()
|
|
||||||
val file = File(dir, "chiaki-settings.json")
|
|
||||||
return exportAllSettings(db)
|
|
||||||
.map {
|
|
||||||
file.writeText(it, Charsets.UTF_8)
|
|
||||||
file
|
|
||||||
}
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { _ ->
|
|
||||||
val uri = FileProvider.getUriForFile(activity, fileProviderAuthority, file)
|
|
||||||
Intent(Intent.ACTION_SEND).also {
|
|
||||||
it.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
it.type = "application/json"
|
|
||||||
it.putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
it.clipData = ClipData.newRawUri("", uri)
|
|
||||||
activity.startActivity(Intent.createChooser(it, activity.getString(R.string.action_share_log)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importSettingsFromUri(activity: Activity, uri: Uri, disposable: CompositeDisposable)
|
|
||||||
{
|
|
||||||
fun loadFail(msg: String)
|
|
||||||
{
|
|
||||||
MaterialAlertDialogBuilder(activity)
|
|
||||||
.setMessage(activity.getString(R.string.alert_message_import_failed, msg))
|
|
||||||
.setPositiveButton(R.string.action_import_failed_ack) { _, _ -> }
|
|
||||||
.create()
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
val inputStream = activity.contentResolver.openInputStream(uri) ?: throw IOException()
|
|
||||||
val buffer = inputStream.source().buffer()
|
|
||||||
val reader = JsonReader.of(buffer)
|
|
||||||
val adapter = moshi().serializedSettingsAdapter()
|
|
||||||
|
|
||||||
var format: String? = null
|
|
||||||
var version: Int? = null
|
|
||||||
var settingsValue: Any? = null
|
|
||||||
|
|
||||||
reader.beginObject()
|
|
||||||
while(reader.hasNext())
|
|
||||||
{
|
|
||||||
when(reader.nextName())
|
|
||||||
{
|
|
||||||
KEY_FORMAT -> format = reader.nextString()
|
|
||||||
KEY_VERSION -> version = reader.nextInt()
|
|
||||||
KEY_SETTINGS -> settingsValue = reader.readJsonValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.endObject()
|
|
||||||
|
|
||||||
if(format == null || version == null || settingsValue == null)
|
|
||||||
throw IOException("Missing format, version or settings from JSON")
|
|
||||||
if(format != FORMAT)
|
|
||||||
throw IOException("Value of format is invalid")
|
|
||||||
if(version != VERSION) // Add migrations here when necessary
|
|
||||||
throw IOException("Value of version is invalid")
|
|
||||||
|
|
||||||
val settings = adapter.fromJsonValue(settingsValue) ?: throw JsonDataException("Failed to parse Settings JSON")
|
|
||||||
Log.i("SerializedSettings", "would import: $settings")
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(activity)
|
|
||||||
.setMessage(activity.getString(R.string.alert_message_import,
|
|
||||||
settings.registeredHosts.let {
|
|
||||||
if(it.isEmpty())
|
|
||||||
"-"
|
|
||||||
else
|
|
||||||
it.joinToString(separator = "") { host -> "\n - ${host.serverNickname ?: "?"} / ${host.serverMac}" }
|
|
||||||
},
|
|
||||||
settings.manualHosts.let {
|
|
||||||
if(it.isEmpty())
|
|
||||||
"-"
|
|
||||||
else
|
|
||||||
it.joinToString(separator = "") { host -> "\n - ${host.host} / ${host.serverMac ?: "unregistered"}" }
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.setTitle(R.string.alert_title_import)
|
|
||||||
.setNegativeButton(R.string.action_import_cancel) { _, _ -> }
|
|
||||||
.setPositiveButton(R.string.action_import_import) { _, _ ->
|
|
||||||
getDatabase(activity).importDao()
|
|
||||||
.importCompletable(settings)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe()
|
|
||||||
.addTo(disposable)
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
catch(e: IOException)
|
|
||||||
{
|
|
||||||
e.printStackTrace()
|
|
||||||
loadFail(e.message ?: "")
|
|
||||||
}
|
|
||||||
catch(e: JsonDataException)
|
|
||||||
{
|
|
||||||
e.printStackTrace()
|
|
||||||
loadFail(e.message ?: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
abstract class ImportDao
|
|
||||||
{
|
|
||||||
@Insert
|
|
||||||
abstract fun insertRegisteredHosts(hosts: List<RegisteredHost>)
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
abstract fun insertManualHosts(hosts: List<ManualHost>)
|
|
||||||
|
|
||||||
class IdWithMac(val id: Long, val mac: MacAddress)
|
|
||||||
|
|
||||||
@Query("SELECT id, server_mac AS mac FROM registered_host WHERE server_mac IN (:macs)")
|
|
||||||
abstract fun registeredHostsByMac(macs: List<MacAddress>): List<IdWithMac>
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
fun import(settings: SerializedSettings)
|
|
||||||
{
|
|
||||||
insertRegisteredHosts(
|
|
||||||
settings.registeredHosts.map {
|
|
||||||
RegisteredHost(
|
|
||||||
target = it.target,
|
|
||||||
apSsid = it.apSsid,
|
|
||||||
apBssid = it.apBssid,
|
|
||||||
apKey = it.apKey,
|
|
||||||
apName = it.apName,
|
|
||||||
serverMac = it.serverMac,
|
|
||||||
serverNickname = it.serverNickname,
|
|
||||||
rpRegistKey = it.rpRegistKey,
|
|
||||||
rpKeyType = it.rpKeyType,
|
|
||||||
rpKey = it.rpKey
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
val macs = settings.manualHosts.mapNotNull { it.serverMac }
|
|
||||||
val idMacs =
|
|
||||||
if(macs.isNotEmpty())
|
|
||||||
registeredHostsByMac(macs)
|
|
||||||
else
|
|
||||||
listOf()
|
|
||||||
|
|
||||||
insertManualHosts(
|
|
||||||
settings.manualHosts.map {
|
|
||||||
ManualHost(
|
|
||||||
host = it.host,
|
|
||||||
registeredHost = idMacs.firstOrNull { regHost -> regHost.mac == it.serverMac }?.id
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ImportDao.importCompletable(settings: SerializedSettings) = Completable.fromCallable { import(settings) }
|
|
|
@ -1,63 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common.ext
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.view.*
|
|
||||||
import android.view.animation.AccelerateInterpolator
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
interface RevealActivity
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
const val EXTRA_REVEAL_X = "reveal_x"
|
|
||||||
const val EXTRA_REVEAL_Y = "reveal_y"
|
|
||||||
}
|
|
||||||
|
|
||||||
val revealRootLayout: View
|
|
||||||
val revealIntent: Intent
|
|
||||||
val revealWindow: Window
|
|
||||||
|
|
||||||
private fun revealActivity(x: Int, y: Int)
|
|
||||||
{
|
|
||||||
val finalRadius = max(revealRootLayout.width, revealRootLayout.height).toFloat()
|
|
||||||
val reveal = ViewAnimationUtils.createCircularReveal(revealRootLayout, x, y, 0f, finalRadius)
|
|
||||||
reveal.interpolator = AccelerateInterpolator()
|
|
||||||
revealRootLayout.visibility = View.VISIBLE
|
|
||||||
reveal.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleReveal()
|
|
||||||
{
|
|
||||||
if(!revealIntent.hasExtra(EXTRA_REVEAL_X) || !revealIntent.hasExtra(EXTRA_REVEAL_Y))
|
|
||||||
return
|
|
||||||
revealWindow.setBackgroundDrawableResource(android.R.color.transparent)
|
|
||||||
val revealX = revealIntent.getIntExtra(EXTRA_REVEAL_X, 0)
|
|
||||||
val revealY = revealIntent.getIntExtra(EXTRA_REVEAL_Y, 0)
|
|
||||||
revealRootLayout.visibility = View.INVISIBLE
|
|
||||||
|
|
||||||
revealRootLayout.viewTreeObserver.also {
|
|
||||||
if(it.isAlive)
|
|
||||||
{
|
|
||||||
it.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
|
|
||||||
override fun onGlobalLayout()
|
|
||||||
{
|
|
||||||
revealActivity(revealX, revealY)
|
|
||||||
revealRootLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Intent.putRevealExtra(originView: View, rootLayout: ViewGroup)
|
|
||||||
{
|
|
||||||
val offsetRect = Rect()
|
|
||||||
originView.getDrawingRect(offsetRect)
|
|
||||||
rootLayout.offsetDescendantRectToMyCoords(originView, offsetRect)
|
|
||||||
putExtra(RevealActivity.EXTRA_REVEAL_X, offsetRect.left)
|
|
||||||
putExtra(RevealActivity.EXTRA_REVEAL_Y, offsetRect.top)
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common.ext
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveDataReactiveStreams
|
|
||||||
import io.reactivex.BackpressureStrategy
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.Single
|
|
||||||
import org.reactivestreams.Publisher
|
|
||||||
|
|
||||||
fun <T> Publisher<T>.toLiveData() = LiveDataReactiveStreams.fromPublisher(this)
|
|
||||||
fun <T> Observable<T>.toLiveData() = this.toFlowable(BackpressureStrategy.LATEST).toLiveData()
|
|
||||||
fun <T> Single<T>.toLiveData() = this.toFlowable().toLiveData()
|
|
|
@ -1,7 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common.ext
|
|
||||||
|
|
||||||
fun String.hexToByteArray(): ByteArray? = ByteArray(this.length / 2) {
|
|
||||||
this.substring(it * 2, it * 2 + 2).toIntOrNull(16)?.toByte() ?: return null
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.common.ext
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.annotation.LayoutRes
|
|
||||||
|
|
||||||
fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View
|
|
||||||
{
|
|
||||||
return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package com.metallic.chiaki.common.ext
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
|
|
||||||
inline fun <T: ViewModel> viewModelFactory(crossinline f: () -> T) =
|
|
||||||
object : ViewModelProvider.Factory
|
|
||||||
{
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.discovery
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.metallic.chiaki.common.MacAddress
|
|
||||||
import com.metallic.chiaki.common.ext.hexToByteArray
|
|
||||||
import com.metallic.chiaki.lib.CreateError
|
|
||||||
import com.metallic.chiaki.lib.DiscoveryHost
|
|
||||||
import com.metallic.chiaki.lib.DiscoveryService
|
|
||||||
import com.metallic.chiaki.lib.DiscoveryServiceOptions
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxkotlin.addTo
|
|
||||||
import io.reactivex.subjects.BehaviorSubject
|
|
||||||
import io.reactivex.subjects.Subject
|
|
||||||
import java.lang.NumberFormatException
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
val DiscoveryHost.serverMac get() = this.hostId?.hexToByteArray()?.let {
|
|
||||||
if(it.size == MacAddress.LENGTH)
|
|
||||||
MacAddress(it)
|
|
||||||
else
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
class DiscoveryManager
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
const val HOSTS_MAX: ULong = 16U
|
|
||||||
const val DROP_PINGS: ULong = 3U
|
|
||||||
const val PING_MS: ULong = 500U
|
|
||||||
const val PORT = 987
|
|
||||||
|
|
||||||
const val DEBOUNCE_EMPTY_MS = 1000L
|
|
||||||
}
|
|
||||||
|
|
||||||
private var discoveryService: DiscoveryService? = null
|
|
||||||
|
|
||||||
private val discoveryActiveSubject: Subject<Boolean> = BehaviorSubject.create<Boolean>().also { it.onNext(false) }
|
|
||||||
val discoveryActive: Observable<Boolean> get() = discoveryActiveSubject
|
|
||||||
var active = false
|
|
||||||
set(value)
|
|
||||||
{
|
|
||||||
field = value
|
|
||||||
discoveryActiveSubject.onNext(value)
|
|
||||||
updateService()
|
|
||||||
}
|
|
||||||
private var paused = false
|
|
||||||
|
|
||||||
private val disposable = CompositeDisposable()
|
|
||||||
|
|
||||||
private var discoveredHostsSubjectDebounced: Subject<List<DiscoveryHost>> = BehaviorSubject.create<List<DiscoveryHost>>().also {
|
|
||||||
it.onNext(listOf())
|
|
||||||
}.toSerialized()
|
|
||||||
|
|
||||||
private var discoveredHostsSubjectRaw: Subject<List<DiscoveryHost>> = BehaviorSubject.create<List<DiscoveryHost>>().also { subject ->
|
|
||||||
subject.debounce { hosts ->
|
|
||||||
if(hosts.isEmpty())
|
|
||||||
Observable.timer(DEBOUNCE_EMPTY_MS, TimeUnit.MILLISECONDS)
|
|
||||||
else
|
|
||||||
Observable.empty()
|
|
||||||
}
|
|
||||||
.subscribe { hosts ->
|
|
||||||
discoveredHostsSubjectDebounced.onNext(hosts)
|
|
||||||
}
|
|
||||||
.addTo(disposable)
|
|
||||||
}
|
|
||||||
|
|
||||||
val discoveredHosts: Observable<List<DiscoveryHost>> get() = discoveredHostsSubjectDebounced
|
|
||||||
|
|
||||||
fun resume()
|
|
||||||
{
|
|
||||||
paused = false
|
|
||||||
updateService()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pause()
|
|
||||||
{
|
|
||||||
paused = true
|
|
||||||
updateService()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispose()
|
|
||||||
{
|
|
||||||
active = false
|
|
||||||
disposable.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendWakeup(host: String, registKey: ByteArray, ps5: Boolean)
|
|
||||||
{
|
|
||||||
val registKeyString = registKey.indexOfFirst { it == 0.toByte() }.let { end -> registKey.copyOfRange(0, if(end >= 0) end else registKey.size) }.toString(StandardCharsets.UTF_8)
|
|
||||||
val credential = try { registKeyString.toULong(16) } catch(e: NumberFormatException) {
|
|
||||||
Log.e("DiscoveryManager", "Failed to convert registKey to int", e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DiscoveryService.wakeup(discoveryService, host, credential, ps5)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateService()
|
|
||||||
{
|
|
||||||
if(active && !paused && discoveryService == null)
|
|
||||||
{
|
|
||||||
discoveredHostsSubjectRaw.onNext(listOf())
|
|
||||||
try
|
|
||||||
{
|
|
||||||
discoveryService = DiscoveryService(DiscoveryServiceOptions(
|
|
||||||
HOSTS_MAX, DROP_PINGS, PING_MS, InetSocketAddress("255.255.255.255", PORT)
|
|
||||||
), discoveredHostsSubjectRaw::onNext)
|
|
||||||
}
|
|
||||||
catch(e: CreateError)
|
|
||||||
{
|
|
||||||
Log.e("DiscoveryManager", "Failed to start Discovery Service: $e")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if((!active || paused) && discoveryService != null)
|
|
||||||
{
|
|
||||||
val service = discoveryService ?: return
|
|
||||||
service.dispose()
|
|
||||||
discoveryService = null
|
|
||||||
if(!active)
|
|
||||||
discoveredHostsSubjectRaw.onNext(listOf())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,534 +0,0 @@
|
||||||
package com.metallic.chiaki.lib
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.Surface
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.lang.Exception
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
enum class Target(val value: Int)
|
|
||||||
{
|
|
||||||
PS4_UNKNOWN(0),
|
|
||||||
PS4_8(800),
|
|
||||||
PS4_9(900),
|
|
||||||
PS4_10(1000),
|
|
||||||
PS5_UNKNOWN(1000000),
|
|
||||||
PS5_1(1000100);
|
|
||||||
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
@JvmStatic
|
|
||||||
fun fromValue(value: Int) = values().firstOrNull { it.value == value } ?: PS4_10
|
|
||||||
}
|
|
||||||
|
|
||||||
val isPS5 get() = value >= PS5_UNKNOWN.value
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class VideoResolutionPreset(val value: Int)
|
|
||||||
{
|
|
||||||
RES_360P(1),
|
|
||||||
RES_540P(2),
|
|
||||||
RES_720P(3),
|
|
||||||
RES_1080P(4)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class VideoFPSPreset(val value: Int)
|
|
||||||
{
|
|
||||||
FPS_30(30),
|
|
||||||
FPS_60(60)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Codec(val value: Int)
|
|
||||||
{
|
|
||||||
CODEC_H264(0),
|
|
||||||
CODEC_H265(1),
|
|
||||||
CODEC_H265_HDR(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ConnectVideoProfile(
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
val maxFPS: Int,
|
|
||||||
val bitrate: Int,
|
|
||||||
val codec: Codec
|
|
||||||
): Parcelable
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
fun preset(resolutionPreset: VideoResolutionPreset, fpsPreset: VideoFPSPreset, codec: Codec)
|
|
||||||
= ChiakiNative.videoProfilePreset(resolutionPreset.value, fpsPreset.value, codec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ConnectInfo(
|
|
||||||
val ps5: Boolean,
|
|
||||||
val host: String,
|
|
||||||
val registKey: ByteArray,
|
|
||||||
val morning: ByteArray,
|
|
||||||
val videoProfile: ConnectVideoProfile
|
|
||||||
): Parcelable
|
|
||||||
|
|
||||||
private class ChiakiNative
|
|
||||||
{
|
|
||||||
data class CreateResult(var errorCode: Int, var ptr: Long)
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
init
|
|
||||||
{
|
|
||||||
System.loadLibrary("chiaki-jni")
|
|
||||||
}
|
|
||||||
@JvmStatic external fun errorCodeToString(value: Int): String
|
|
||||||
@JvmStatic external fun quitReasonToString(value: Int): String
|
|
||||||
@JvmStatic external fun quitReasonIsError(value: Int): Boolean
|
|
||||||
@JvmStatic external fun videoProfilePreset(resolutionPreset: Int, fpsPreset: Int, codec: Codec): ConnectVideoProfile
|
|
||||||
@JvmStatic external fun sessionCreate(result: CreateResult, connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean, javaSession: Session)
|
|
||||||
@JvmStatic external fun sessionFree(ptr: Long)
|
|
||||||
@JvmStatic external fun sessionStart(ptr: Long): Int
|
|
||||||
@JvmStatic external fun sessionStop(ptr: Long): Int
|
|
||||||
@JvmStatic external fun sessionJoin(ptr: Long): Int
|
|
||||||
@JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface?)
|
|
||||||
@JvmStatic external fun sessionSetControllerState(ptr: Long, controllerState: ControllerState)
|
|
||||||
@JvmStatic external fun sessionSetLoginPin(ptr: Long, pin: String)
|
|
||||||
@JvmStatic external fun discoveryServiceCreate(result: CreateResult, options: DiscoveryServiceOptions, javaService: DiscoveryService)
|
|
||||||
@JvmStatic external fun discoveryServiceFree(ptr: Long)
|
|
||||||
@JvmStatic external fun discoveryServiceWakeup(ptr: Long, host: String, userCredential: Long, ps5: Boolean)
|
|
||||||
@JvmStatic external fun registStart(result: CreateResult, registInfo: RegistInfo, javaLog: ChiakiLog, javaRegist: Regist)
|
|
||||||
@JvmStatic external fun registStop(ptr: Long)
|
|
||||||
@JvmStatic external fun registFree(ptr: Long)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ErrorCode(val value: Int)
|
|
||||||
{
|
|
||||||
override fun toString() = ChiakiNative.errorCodeToString(value)
|
|
||||||
var isSuccess = value == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChiakiLog(val levelMask: Int, val callback: (level: Int, text: String) -> Unit)
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
fun formatLog(level: Int, text: String) =
|
|
||||||
"[${when(level)
|
|
||||||
{
|
|
||||||
Level.DEBUG.value -> "D"
|
|
||||||
Level.VERBOSE.value -> "V"
|
|
||||||
Level.INFO.value -> "I"
|
|
||||||
Level.WARNING.value -> "W"
|
|
||||||
Level.ERROR.value -> "E"
|
|
||||||
else -> "?"
|
|
||||||
}
|
|
||||||
}] $text"
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Level(val value: Int)
|
|
||||||
{
|
|
||||||
DEBUG(1 shl 4),
|
|
||||||
VERBOSE(1 shl 3),
|
|
||||||
INFO(1 shl 2),
|
|
||||||
WARNING(1 shl 1),
|
|
||||||
ERROR(1 shl 0),
|
|
||||||
ALL(0.inv())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun log(level: Int, text: String)
|
|
||||||
{
|
|
||||||
callback(level, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun d(text: String) = log(Level.DEBUG.value, text)
|
|
||||||
fun v(text: String) = log(Level.VERBOSE.value, text)
|
|
||||||
fun i(text: String) = log(Level.INFO.value, text)
|
|
||||||
fun w(text: String) = log(Level.WARNING.value, text)
|
|
||||||
fun e(text: String) = log(Level.ERROR.value, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun maxAbs(a: Short, b: Short) = if(abs(a.toInt()) > abs(b.toInt())) a else b
|
|
||||||
|
|
||||||
private val CONTROLLER_TOUCHES_MAX = 2 // must be the same as CHIAKI_CONTROLLER_TOUCHES_MAX
|
|
||||||
|
|
||||||
data class ControllerTouch(
|
|
||||||
var x: UShort = 0U,
|
|
||||||
var y: UShort = 0U,
|
|
||||||
var id: Byte = -1 // -1 = up
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ControllerState constructor(
|
|
||||||
var buttons: UInt = 0U,
|
|
||||||
var l2State: UByte = 0U,
|
|
||||||
var r2State: UByte = 0U,
|
|
||||||
var leftX: Short = 0,
|
|
||||||
var leftY: Short = 0,
|
|
||||||
var rightX: Short = 0,
|
|
||||||
var rightY: Short = 0,
|
|
||||||
private var touchIdNext: UByte = 0U,
|
|
||||||
var touches: Array<ControllerTouch> = arrayOf(ControllerTouch(), ControllerTouch()),
|
|
||||||
var gyroX: Float = 0.0f,
|
|
||||||
var gyroY: Float = 0.0f,
|
|
||||||
var gyroZ: Float = 0.0f,
|
|
||||||
var accelX: Float = 0.0f,
|
|
||||||
var accelY: Float = 1.0f,
|
|
||||||
var accelZ: Float = 0.0f,
|
|
||||||
var orientX: Float = 0.0f,
|
|
||||||
var orientY: Float = 0.0f,
|
|
||||||
var orientZ: Float = 0.0f,
|
|
||||||
var orientW: Float = 1.0f
|
|
||||||
){
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
val BUTTON_CROSS = (1 shl 0).toUInt()
|
|
||||||
val BUTTON_MOON = (1 shl 1).toUInt()
|
|
||||||
val BUTTON_BOX = (1 shl 2).toUInt()
|
|
||||||
val BUTTON_PYRAMID = (1 shl 3).toUInt()
|
|
||||||
val BUTTON_DPAD_LEFT = (1 shl 4).toUInt()
|
|
||||||
val BUTTON_DPAD_RIGHT = (1 shl 5).toUInt()
|
|
||||||
val BUTTON_DPAD_UP = (1 shl 6).toUInt()
|
|
||||||
val BUTTON_DPAD_DOWN = (1 shl 7).toUInt()
|
|
||||||
val BUTTON_L1 = (1 shl 8).toUInt()
|
|
||||||
val BUTTON_R1 = (1 shl 9).toUInt()
|
|
||||||
val BUTTON_L3 = (1 shl 10).toUInt()
|
|
||||||
val BUTTON_R3 = (1 shl 11).toUInt()
|
|
||||||
val BUTTON_OPTIONS = (1 shl 12).toUInt()
|
|
||||||
val BUTTON_SHARE = (1 shl 13).toUInt()
|
|
||||||
val BUTTON_TOUCHPAD = (1 shl 14).toUInt()
|
|
||||||
val BUTTON_PS = (1 shl 15).toUInt()
|
|
||||||
val TOUCHPAD_WIDTH: UShort = 1920U
|
|
||||||
val TOUCHPAD_HEIGHT: UShort = 942U
|
|
||||||
}
|
|
||||||
|
|
||||||
infix fun or(o: ControllerState) = ControllerState(
|
|
||||||
buttons = buttons or o.buttons,
|
|
||||||
l2State = maxOf(l2State, o.l2State),
|
|
||||||
r2State = maxOf(r2State, o.r2State),
|
|
||||||
leftX = maxAbs(leftX, o.leftX),
|
|
||||||
leftY = maxAbs(leftY, o.leftY),
|
|
||||||
rightX = maxAbs(rightX, o.rightX),
|
|
||||||
rightY = maxAbs(rightY, o.rightY),
|
|
||||||
touches = touches.zip(o.touches) { a, b -> if(a.id >= 0) a else b }.toTypedArray(),
|
|
||||||
gyroX = gyroX,
|
|
||||||
gyroY = gyroY,
|
|
||||||
gyroZ = gyroZ,
|
|
||||||
accelX = accelX,
|
|
||||||
accelY = accelY,
|
|
||||||
accelZ = accelZ,
|
|
||||||
orientX = orientX,
|
|
||||||
orientY = orientY,
|
|
||||||
orientZ = orientZ,
|
|
||||||
orientW = orientW
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean
|
|
||||||
{
|
|
||||||
if(this === other) return true
|
|
||||||
if(javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ControllerState
|
|
||||||
|
|
||||||
if(buttons != other.buttons) return false
|
|
||||||
if(l2State != other.l2State) return false
|
|
||||||
if(r2State != other.r2State) return false
|
|
||||||
if(leftX != other.leftX) return false
|
|
||||||
if(leftY != other.leftY) return false
|
|
||||||
if(rightX != other.rightX) return false
|
|
||||||
if(rightY != other.rightY) return false
|
|
||||||
if(touchIdNext != other.touchIdNext) return false
|
|
||||||
if(!touches.contentEquals(other.touches)) return false
|
|
||||||
if(gyroX != other.gyroX) return false
|
|
||||||
if(gyroY != other.gyroY) return false
|
|
||||||
if(gyroZ != other.gyroZ) return false
|
|
||||||
if(accelX != other.accelX) return false
|
|
||||||
if(accelY != other.accelY) return false
|
|
||||||
if(accelZ != other.accelZ) return false
|
|
||||||
if(orientX != other.orientX) return false
|
|
||||||
if(orientY != other.orientY) return false
|
|
||||||
if(orientZ != other.orientZ) return false
|
|
||||||
if(orientW != other.orientW) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int
|
|
||||||
{
|
|
||||||
var result = buttons.hashCode()
|
|
||||||
result = 31 * result + l2State.hashCode()
|
|
||||||
result = 31 * result + r2State.hashCode()
|
|
||||||
result = 31 * result + leftX
|
|
||||||
result = 31 * result + leftY
|
|
||||||
result = 31 * result + rightX
|
|
||||||
result = 31 * result + rightY
|
|
||||||
result = 31 * result + touchIdNext.hashCode()
|
|
||||||
result = 31 * result + touches.contentHashCode()
|
|
||||||
result = 31 * result + gyroX.hashCode()
|
|
||||||
result = 31 * result + gyroY.hashCode()
|
|
||||||
result = 31 * result + gyroZ.hashCode()
|
|
||||||
result = 31 * result + accelX.hashCode()
|
|
||||||
result = 31 * result + accelY.hashCode()
|
|
||||||
result = 31 * result + accelZ.hashCode()
|
|
||||||
result = 31 * result + orientX.hashCode()
|
|
||||||
result = 31 * result + orientY.hashCode()
|
|
||||||
result = 31 * result + orientZ.hashCode()
|
|
||||||
result = 31 * result + orientW.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startTouch(x: UShort, y: UShort): UByte? =
|
|
||||||
touches
|
|
||||||
.find { it.id < 0 }
|
|
||||||
?.also {
|
|
||||||
it.id = touchIdNext.toByte()
|
|
||||||
it.x = x
|
|
||||||
it.y = y
|
|
||||||
touchIdNext = ((touchIdNext + 1U) and 0x7fU).toUByte()
|
|
||||||
}?.id?.toUByte()
|
|
||||||
|
|
||||||
fun stopTouch(id: UByte)
|
|
||||||
{
|
|
||||||
touches.find {
|
|
||||||
it.id >= 0 && it.id == id.toByte()
|
|
||||||
}?.let {
|
|
||||||
it.id = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTouchPos(id: UByte, x: UShort, y: UShort): Boolean
|
|
||||||
= touches.find {
|
|
||||||
it.id >= 0 && it.id == id.toByte()
|
|
||||||
}?.let {
|
|
||||||
val r = it.x != x || it.y != y
|
|
||||||
it.x = x
|
|
||||||
it.y = y
|
|
||||||
r
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
class QuitReason(val value: Int)
|
|
||||||
{
|
|
||||||
override fun toString() = ChiakiNative.quitReasonToString(value)
|
|
||||||
|
|
||||||
val isError = ChiakiNative.quitReasonIsError(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class Event
|
|
||||||
object ConnectedEvent: Event()
|
|
||||||
data class LoginPinRequestEvent(val pinIncorrect: Boolean): Event()
|
|
||||||
data class QuitEvent(val reason: QuitReason, val reasonString: String?): Event()
|
|
||||||
data class RumbleEvent(val left: UByte, val right: UByte): Event()
|
|
||||||
|
|
||||||
class CreateError(val errorCode: ErrorCode): Exception("Failed to create a native object: $errorCode")
|
|
||||||
|
|
||||||
class Session(connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean)
|
|
||||||
{
|
|
||||||
interface EventCallback
|
|
||||||
{
|
|
||||||
fun sessionEvent(event: Event)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var nativePtr: Long
|
|
||||||
var eventCallback: ((event: Event) -> Unit)? = null
|
|
||||||
|
|
||||||
init
|
|
||||||
{
|
|
||||||
val result = ChiakiNative.CreateResult(0, 0)
|
|
||||||
ChiakiNative.sessionCreate(result, connectInfo, logFile, logVerbose, this)
|
|
||||||
val errorCode = ErrorCode(result.errorCode)
|
|
||||||
if(!errorCode.isSuccess)
|
|
||||||
throw CreateError(errorCode)
|
|
||||||
nativePtr = result.ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start() = ErrorCode(ChiakiNative.sessionStart(nativePtr))
|
|
||||||
fun stop() = ErrorCode(ChiakiNative.sessionStop(nativePtr))
|
|
||||||
|
|
||||||
fun dispose()
|
|
||||||
{
|
|
||||||
if(nativePtr == 0L)
|
|
||||||
return
|
|
||||||
ChiakiNative.sessionJoin(nativePtr)
|
|
||||||
ChiakiNative.sessionFree(nativePtr)
|
|
||||||
nativePtr = 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun event(event: Event)
|
|
||||||
{
|
|
||||||
eventCallback?.let { it(event) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun eventConnected()
|
|
||||||
{
|
|
||||||
event(ConnectedEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun eventLoginPinRequest(pinIncorrect: Boolean)
|
|
||||||
{
|
|
||||||
event(LoginPinRequestEvent(pinIncorrect))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun eventQuit(reasonValue: Int, reasonString: String?)
|
|
||||||
{
|
|
||||||
event(QuitEvent(QuitReason(reasonValue), reasonString))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun eventRumble(left: Int, right: Int)
|
|
||||||
{
|
|
||||||
event(RumbleEvent(left.toUByte(), right.toUByte()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSurface(surface: Surface?)
|
|
||||||
{
|
|
||||||
ChiakiNative.sessionSetSurface(nativePtr, surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setControllerState(controllerState: ControllerState)
|
|
||||||
{
|
|
||||||
ChiakiNative.sessionSetControllerState(nativePtr, controllerState)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLoginPin(pin: String)
|
|
||||||
{
|
|
||||||
ChiakiNative.sessionSetLoginPin(nativePtr, pin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DiscoveryHost(
|
|
||||||
val state: State,
|
|
||||||
val hostRequestPort: UShort,
|
|
||||||
val hostAddr: String?,
|
|
||||||
val systemVersion: String?,
|
|
||||||
val deviceDiscoveryProtocolVersion: String?,
|
|
||||||
val hostName: String?,
|
|
||||||
val hostType: String?,
|
|
||||||
val hostId: String?,
|
|
||||||
val runningAppTitleid: String?,
|
|
||||||
val runningAppName: String?)
|
|
||||||
{
|
|
||||||
enum class State
|
|
||||||
{
|
|
||||||
UNKNOWN,
|
|
||||||
READY,
|
|
||||||
STANDBY
|
|
||||||
}
|
|
||||||
|
|
||||||
val isPS5 get() = deviceDiscoveryProtocolVersion == "00030010"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
data class DiscoveryServiceOptions(
|
|
||||||
val hostsMax: ULong,
|
|
||||||
val hostDropPings: ULong,
|
|
||||||
val pingMs: ULong,
|
|
||||||
val sendAddr: InetSocketAddress
|
|
||||||
)
|
|
||||||
|
|
||||||
class DiscoveryService(
|
|
||||||
options: DiscoveryServiceOptions,
|
|
||||||
val callback: ((hosts: List<DiscoveryHost>) -> Unit)?)
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
fun wakeup(service: DiscoveryService?, host: String, userCredential: ULong, ps5: Boolean) =
|
|
||||||
ChiakiNative.discoveryServiceWakeup(service?.nativePtr ?: 0, host, userCredential.toLong(), ps5)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var nativePtr: Long
|
|
||||||
|
|
||||||
init
|
|
||||||
{
|
|
||||||
val result = ChiakiNative.CreateResult(0, 0)
|
|
||||||
ChiakiNative.discoveryServiceCreate(result, options, this)
|
|
||||||
val errorCode = ErrorCode(result.errorCode)
|
|
||||||
if(!errorCode.isSuccess)
|
|
||||||
throw CreateError(errorCode)
|
|
||||||
nativePtr = result.ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispose()
|
|
||||||
{
|
|
||||||
if(nativePtr == 0L)
|
|
||||||
return
|
|
||||||
ChiakiNative.discoveryServiceFree(nativePtr)
|
|
||||||
nativePtr = 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hostsUpdated(hosts: Array<DiscoveryHost>)
|
|
||||||
{
|
|
||||||
val hostsList = hosts.toList()
|
|
||||||
Log.i("Chiaki", "got hosts from native: $hostsList")
|
|
||||||
callback?.let { it(hostsList) }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class RegistInfo(
|
|
||||||
val target: Target,
|
|
||||||
val host: String,
|
|
||||||
val broadcast: Boolean,
|
|
||||||
val psnOnlineId: String?,
|
|
||||||
val psnAccountId: ByteArray?,
|
|
||||||
val pin: Int
|
|
||||||
): Parcelable
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
const val ACCOUNT_ID_SIZE = 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class RegistHost(
|
|
||||||
val target: Target,
|
|
||||||
val apSsid: String,
|
|
||||||
val apBssid: String,
|
|
||||||
val apKey: String,
|
|
||||||
val apName: String,
|
|
||||||
val serverMac: ByteArray,
|
|
||||||
val serverNickname: String,
|
|
||||||
val rpRegistKey: ByteArray,
|
|
||||||
val rpKeyType: UInt,
|
|
||||||
val rpKey: ByteArray
|
|
||||||
)
|
|
||||||
|
|
||||||
sealed class RegistEvent
|
|
||||||
object RegistEventCanceled: RegistEvent()
|
|
||||||
object RegistEventFailed: RegistEvent()
|
|
||||||
class RegistEventSuccess(val host: RegistHost): RegistEvent()
|
|
||||||
|
|
||||||
class Regist(
|
|
||||||
info: RegistInfo,
|
|
||||||
log: ChiakiLog,
|
|
||||||
val callback: (RegistEvent) -> Unit
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private var nativePtr: Long
|
|
||||||
|
|
||||||
init
|
|
||||||
{
|
|
||||||
val result = ChiakiNative.CreateResult(0, 0)
|
|
||||||
ChiakiNative.registStart(result, info, log, this)
|
|
||||||
val errorCode = ErrorCode(result.errorCode)
|
|
||||||
if(!errorCode.isSuccess)
|
|
||||||
throw CreateError(errorCode)
|
|
||||||
nativePtr = result.ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop()
|
|
||||||
{
|
|
||||||
ChiakiNative.registStop(nativePtr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispose()
|
|
||||||
{
|
|
||||||
if(nativePtr == 0L)
|
|
||||||
return
|
|
||||||
ChiakiNative.registFree(nativePtr)
|
|
||||||
nativePtr = 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun event(event: RegistEvent)
|
|
||||||
{
|
|
||||||
callback(event)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,124 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.main
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.AnimationUtils
|
|
||||||
import android.widget.PopupMenu
|
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.common.DiscoveredDisplayHost
|
|
||||||
import com.metallic.chiaki.common.DisplayHost
|
|
||||||
import com.metallic.chiaki.common.ManualDisplayHost
|
|
||||||
import com.metallic.chiaki.common.ext.inflate
|
|
||||||
import com.metallic.chiaki.databinding.ItemDisplayHostBinding
|
|
||||||
import com.metallic.chiaki.lib.DiscoveryHost
|
|
||||||
|
|
||||||
class DisplayHostDiffCallback(val old: List<DisplayHost>, val new: List<DisplayHost>): DiffUtil.Callback()
|
|
||||||
{
|
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = (old[oldItemPosition] == new[newItemPosition])
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = (old[oldItemPosition] == new[newItemPosition])
|
|
||||||
override fun getOldListSize() = old.size
|
|
||||||
override fun getNewListSize() = new.size
|
|
||||||
}
|
|
||||||
|
|
||||||
class DisplayHostRecyclerViewAdapter(
|
|
||||||
val clickCallback: (DisplayHost) -> Unit,
|
|
||||||
val wakeupCallback: (DisplayHost) -> Unit,
|
|
||||||
val editCallback: (DisplayHost) -> Unit,
|
|
||||||
val deleteCallback: (DisplayHost) -> Unit
|
|
||||||
): RecyclerView.Adapter<DisplayHostRecyclerViewAdapter.ViewHolder>()
|
|
||||||
{
|
|
||||||
var hosts: List<DisplayHost> = listOf()
|
|
||||||
set(value)
|
|
||||||
{
|
|
||||||
val diff = DiffUtil.calculateDiff(DisplayHostDiffCallback(field, value))
|
|
||||||
field = value
|
|
||||||
diff.dispatchUpdatesTo(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewHolder(val binding: ItemDisplayHostBinding): RecyclerView.ViewHolder(binding.root)
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
|
|
||||||
= ViewHolder(ItemDisplayHostBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
|
|
||||||
override fun getItemCount() = hosts.count()
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int)
|
|
||||||
{
|
|
||||||
val context = holder.itemView.context
|
|
||||||
val host = hosts[position]
|
|
||||||
holder.binding.also {
|
|
||||||
it.nameTextView.text = host.name
|
|
||||||
it.hostTextView.text = context.getString(R.string.display_host_host, host.host)
|
|
||||||
val id = host.id
|
|
||||||
it.idTextView.text =
|
|
||||||
if(id != null)
|
|
||||||
context.getString(
|
|
||||||
if(host.isRegistered)
|
|
||||||
R.string.display_host_id_registered
|
|
||||||
else
|
|
||||||
R.string.display_host_id_unregistered,
|
|
||||||
id)
|
|
||||||
else
|
|
||||||
""
|
|
||||||
it.bottomInfoTextView.text = (host as? DiscoveredDisplayHost)?.discoveredHost?.let { discoveredHost ->
|
|
||||||
if(discoveredHost.runningAppName != null || discoveredHost.runningAppTitleid != null)
|
|
||||||
context.getString(R.string.display_host_app_title_id, discoveredHost.runningAppName ?: "", discoveredHost.runningAppTitleid ?: "")
|
|
||||||
else
|
|
||||||
""
|
|
||||||
} ?: ""
|
|
||||||
it.discoveredIndicatorLayout.visibility = if(host is DiscoveredDisplayHost) View.VISIBLE else View.GONE
|
|
||||||
it.stateIndicatorImageView.setImageResource(
|
|
||||||
when
|
|
||||||
{
|
|
||||||
host is DiscoveredDisplayHost -> when(host.discoveredHost.state)
|
|
||||||
{
|
|
||||||
DiscoveryHost.State.STANDBY -> if(host.isPS5) R.drawable.ic_console_ps5_standby else R.drawable.ic_console_standby
|
|
||||||
DiscoveryHost.State.READY -> if(host.isPS5) R.drawable.ic_console_ps5_ready else R.drawable.ic_console_ready
|
|
||||||
else -> if(host.isPS5) R.drawable.ic_console_ps5 else R.drawable.ic_console
|
|
||||||
}
|
|
||||||
host.isPS5 -> R.drawable.ic_console_ps5
|
|
||||||
else -> R.drawable.ic_console
|
|
||||||
}
|
|
||||||
)
|
|
||||||
it.root.setOnClickListener { clickCallback(host) }
|
|
||||||
|
|
||||||
val canWakeup = host.registeredHost != null
|
|
||||||
val canEditDelete = host is ManualDisplayHost
|
|
||||||
if(canWakeup || canEditDelete)
|
|
||||||
{
|
|
||||||
it.menuButton.isVisible = true
|
|
||||||
it.menuButton.setOnClickListener { _ ->
|
|
||||||
val menu = PopupMenu(context, it.menuButton)
|
|
||||||
menu.menuInflater.inflate(R.menu.display_host, menu.menu)
|
|
||||||
menu.menu.findItem(R.id.action_wakeup).isVisible = canWakeup
|
|
||||||
menu.menu.findItem(R.id.action_edit).isVisible = canEditDelete
|
|
||||||
menu.menu.findItem(R.id.action_delete).isVisible = canEditDelete
|
|
||||||
menu.setOnMenuItemClickListener { menuItem ->
|
|
||||||
when(menuItem.itemId)
|
|
||||||
{
|
|
||||||
R.id.action_wakeup -> wakeupCallback(host)
|
|
||||||
R.id.action_edit -> editCallback(host)
|
|
||||||
R.id.action_delete -> deleteCallback(host)
|
|
||||||
else -> return@setOnMenuItemClickListener false
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
menu.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
it.menuButton.isGone = true
|
|
||||||
it.menuButton.setOnClickListener(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.main
|
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorSet
|
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.animation.PropertyValuesHolder
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.AccelerateInterpolator
|
|
||||||
import android.view.animation.DecelerateInterpolator
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
import androidx.core.animation.addListener
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import com.google.android.material.transformation.ExpandableTransformationBehavior
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
|
|
||||||
class FloatingActionButtonBackgroundBehavior @JvmOverloads constructor(context: Context? = null, attrs: AttributeSet? = null) : ExpandableTransformationBehavior(context, attrs)
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
private const val DURATION = 150L
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View)
|
|
||||||
= dependency is FloatingActionButton
|
|
||||||
|
|
||||||
override fun onCreateExpandedStateChangeAnimation(dependency: View, child: View, expanded: Boolean, isAnimating: Boolean): AnimatorSet
|
|
||||||
= AnimatorSet().also {
|
|
||||||
it.playTogether(listOf(
|
|
||||||
if(expanded)
|
|
||||||
createExpandAnimation(child, isAnimating)
|
|
||||||
else
|
|
||||||
createCollapseAnimation(child)
|
|
||||||
|
|
||||||
))
|
|
||||||
it.addListener(
|
|
||||||
onStart = {
|
|
||||||
if(expanded)
|
|
||||||
child.isVisible = true
|
|
||||||
},
|
|
||||||
onEnd = {
|
|
||||||
if(!expanded)
|
|
||||||
child.isGone = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createExpandAnimation(child: View, currentlyAnimating: Boolean): Animator
|
|
||||||
{
|
|
||||||
if(!currentlyAnimating)
|
|
||||||
child.alpha = 0f
|
|
||||||
|
|
||||||
val animator = ObjectAnimator.ofPropertyValuesHolder(
|
|
||||||
child,
|
|
||||||
PropertyValuesHolder.ofFloat(View.ALPHA, 1f)
|
|
||||||
).apply {
|
|
||||||
duration = DURATION
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimatorSet().apply {
|
|
||||||
playTogether(listOf(animator))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createCollapseAnimation(child: View): Animator
|
|
||||||
{
|
|
||||||
val animator = ObjectAnimator.ofPropertyValuesHolder(
|
|
||||||
child,
|
|
||||||
PropertyValuesHolder.ofFloat(View.ALPHA, 0f)
|
|
||||||
).apply {
|
|
||||||
duration = DURATION
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimatorSet().apply {
|
|
||||||
playTogether(listOf(animator))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.main
|
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorSet
|
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.animation.PropertyValuesHolder
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.AccelerateInterpolator
|
|
||||||
import android.view.animation.DecelerateInterpolator
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
import androidx.core.animation.addListener
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import com.google.android.material.transformation.ExpandableTransformationBehavior
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
|
|
||||||
// see https://github.com/lcdsmao/ExpandableFABExample
|
|
||||||
|
|
||||||
class FloatingActionButtonSpeedDialBehavior @JvmOverloads constructor(context: Context? = null, attrs: AttributeSet? = null) : ExpandableTransformationBehavior(context, attrs)
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
private const val DELAY = 30L
|
|
||||||
private const val DURATION = 150L
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View)
|
|
||||||
= dependency is FloatingActionButton && child is ViewGroup
|
|
||||||
|
|
||||||
override fun onCreateExpandedStateChangeAnimation(dependency: View, child: View, expanded: Boolean, isAnimating: Boolean): AnimatorSet
|
|
||||||
= if(child !is ViewGroup)
|
|
||||||
AnimatorSet()
|
|
||||||
else
|
|
||||||
AnimatorSet().also {
|
|
||||||
it.playTogether(listOf(
|
|
||||||
if(expanded)
|
|
||||||
createExpandAnimation(child, isAnimating)
|
|
||||||
else
|
|
||||||
createCollapseAnimation(child)
|
|
||||||
|
|
||||||
))
|
|
||||||
it.addListener(
|
|
||||||
onStart = {
|
|
||||||
if(expanded)
|
|
||||||
child.isVisible = true
|
|
||||||
},
|
|
||||||
onEnd = {
|
|
||||||
if(!expanded)
|
|
||||||
child.isInvisible = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun offset(resources: Resources) = resources.getDimension(R.dimen.floating_action_button_speed_dial_anim_offset)
|
|
||||||
|
|
||||||
private fun createExpandAnimation(child: ViewGroup, currentlyAnimating: Boolean): Animator
|
|
||||||
{
|
|
||||||
if(!currentlyAnimating)
|
|
||||||
{
|
|
||||||
child.children.forEach {
|
|
||||||
it.alpha = 0f
|
|
||||||
it.translationY = this.offset(child.resources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val translationYHolder = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f)
|
|
||||||
val alphaHolder = PropertyValuesHolder.ofFloat(View.ALPHA, 1f)
|
|
||||||
|
|
||||||
val animators = child.children.mapIndexed { index, view ->
|
|
||||||
ObjectAnimator.ofPropertyValuesHolder(
|
|
||||||
view,
|
|
||||||
translationYHolder,
|
|
||||||
alphaHolder
|
|
||||||
).apply {
|
|
||||||
duration = DURATION
|
|
||||||
startDelay = (child.childCount - index - 1) * DELAY
|
|
||||||
interpolator = DecelerateInterpolator()
|
|
||||||
}
|
|
||||||
}.toList()
|
|
||||||
|
|
||||||
return AnimatorSet().apply {
|
|
||||||
playTogether(animators)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createCollapseAnimation(child: ViewGroup): Animator
|
|
||||||
{
|
|
||||||
val translationYHolder = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, this.offset(child.resources))
|
|
||||||
val alphaHolder = PropertyValuesHolder.ofFloat(View.ALPHA, 0f)
|
|
||||||
|
|
||||||
val animators = child.children.mapIndexed { index, view ->
|
|
||||||
ObjectAnimator.ofPropertyValuesHolder(
|
|
||||||
view,
|
|
||||||
translationYHolder,
|
|
||||||
alphaHolder
|
|
||||||
).apply {
|
|
||||||
duration = DURATION
|
|
||||||
startDelay = index * DELAY
|
|
||||||
interpolator = AccelerateInterpolator()
|
|
||||||
}
|
|
||||||
}.toList()
|
|
||||||
|
|
||||||
return AnimatorSet().apply {
|
|
||||||
playTogether(animators)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,240 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.main
|
|
||||||
|
|
||||||
import android.app.ActivityOptions
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.common.*
|
|
||||||
import com.metallic.chiaki.common.ext.putRevealExtra
|
|
||||||
import com.metallic.chiaki.common.ext.viewModelFactory
|
|
||||||
import com.metallic.chiaki.databinding.ActivityMainBinding
|
|
||||||
import com.metallic.chiaki.lib.ConnectInfo
|
|
||||||
import com.metallic.chiaki.lib.DiscoveryHost
|
|
||||||
import com.metallic.chiaki.manualconsole.EditManualConsoleActivity
|
|
||||||
import com.metallic.chiaki.regist.RegistActivity
|
|
||||||
import com.metallic.chiaki.settings.SettingsActivity
|
|
||||||
import com.metallic.chiaki.stream.StreamActivity
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity()
|
|
||||||
{
|
|
||||||
private lateinit var viewModel: MainViewModel
|
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
|
||||||
private var discoveryMenuItem: MenuItem? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?)
|
|
||||||
{
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
title = ""
|
|
||||||
setSupportActionBar(binding.toolbar)
|
|
||||||
|
|
||||||
binding.floatingActionButton.setOnClickListener {
|
|
||||||
expandFloatingActionButton(!binding.floatingActionButton.isExpanded)
|
|
||||||
}
|
|
||||||
binding.floatingActionButtonDialBackground.setOnClickListener {
|
|
||||||
expandFloatingActionButton(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.addManualButton.setOnClickListener { addManualConsole() }
|
|
||||||
binding.addManualLabelButton.setOnClickListener { addManualConsole() }
|
|
||||||
|
|
||||||
binding.registerButton.setOnClickListener { showRegistration() }
|
|
||||||
binding.registerLabelButton.setOnClickListener { showRegistration() }
|
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this, viewModelFactory { MainViewModel(getDatabase(this), Preferences(this)) })
|
|
||||||
.get(MainViewModel::class.java)
|
|
||||||
|
|
||||||
val recyclerViewAdapter = DisplayHostRecyclerViewAdapter(this::hostTriggered, this::wakeupHost, this::editHost, this::deleteHost)
|
|
||||||
binding.hostsRecyclerView.adapter = recyclerViewAdapter
|
|
||||||
binding.hostsRecyclerView.layoutManager = LinearLayoutManager(this)
|
|
||||||
viewModel.displayHosts.observe(this, Observer {
|
|
||||||
val top = binding.hostsRecyclerView.computeVerticalScrollOffset() == 0
|
|
||||||
recyclerViewAdapter.hosts = it
|
|
||||||
if(top)
|
|
||||||
binding.hostsRecyclerView.scrollToPosition(0)
|
|
||||||
updateEmptyInfo()
|
|
||||||
})
|
|
||||||
|
|
||||||
viewModel.discoveryActive.observe(this, Observer { active ->
|
|
||||||
discoveryMenuItem?.let { updateDiscoveryMenuItem(it, active) }
|
|
||||||
updateEmptyInfo()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateEmptyInfo()
|
|
||||||
{
|
|
||||||
if(viewModel.displayHosts.value?.isEmpty() ?: true)
|
|
||||||
{
|
|
||||||
binding.emptyInfoLayout.visibility = View.VISIBLE
|
|
||||||
val discoveryActive = viewModel.discoveryActive.value ?: false
|
|
||||||
binding.emptyInfoImageView.setImageResource(if(discoveryActive) R.drawable.ic_discover_on else R.drawable.ic_discover_off)
|
|
||||||
binding.emptyInfoTextView.setText(if(discoveryActive) R.string.display_hosts_empty_discovery_on_info else R.string.display_hosts_empty_discovery_off_info)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
binding.emptyInfoLayout.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun expandFloatingActionButton(expand: Boolean)
|
|
||||||
{
|
|
||||||
binding.floatingActionButton.isExpanded = expand
|
|
||||||
binding.floatingActionButton.isActivated = binding.floatingActionButton.isExpanded
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart()
|
|
||||||
{
|
|
||||||
super.onStart()
|
|
||||||
viewModel.discoveryManager.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop()
|
|
||||||
{
|
|
||||||
super.onStop()
|
|
||||||
viewModel.discoveryManager.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed()
|
|
||||||
{
|
|
||||||
if(binding.floatingActionButton.isExpanded)
|
|
||||||
{
|
|
||||||
expandFloatingActionButton(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean
|
|
||||||
{
|
|
||||||
menuInflater.inflate(R.menu.main, menu)
|
|
||||||
val discoveryItem = menu.findItem(R.id.action_discover)
|
|
||||||
discoveryMenuItem = discoveryItem
|
|
||||||
val discoveryActive = viewModel.discoveryActive.value ?: false
|
|
||||||
updateDiscoveryMenuItem(discoveryItem, discoveryActive)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateDiscoveryMenuItem(item: MenuItem, active: Boolean)
|
|
||||||
{
|
|
||||||
item.isChecked = active
|
|
||||||
item.setIcon(if(active) R.drawable.ic_discover_on else R.drawable.ic_discover_off)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when(item.itemId)
|
|
||||||
{
|
|
||||||
R.id.action_discover ->
|
|
||||||
{
|
|
||||||
viewModel.discoveryManager.active = !(viewModel.discoveryActive.value ?: false)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.action_settings ->
|
|
||||||
{
|
|
||||||
Intent(this, SettingsActivity::class.java).also {
|
|
||||||
startActivity(it)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addManualConsole()
|
|
||||||
{
|
|
||||||
Intent(this, EditManualConsoleActivity::class.java).also {
|
|
||||||
it.putRevealExtra(binding.addManualButton, binding.rootLayout)
|
|
||||||
startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showRegistration()
|
|
||||||
{
|
|
||||||
Intent(this, RegistActivity::class.java).also {
|
|
||||||
it.putRevealExtra(binding.registerButton, binding.rootLayout)
|
|
||||||
startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hostTriggered(host: DisplayHost)
|
|
||||||
{
|
|
||||||
val registeredHost = host.registeredHost
|
|
||||||
if(registeredHost != null)
|
|
||||||
{
|
|
||||||
fun connect() {
|
|
||||||
val connectInfo = ConnectInfo(host.isPS5, host.host, registeredHost.rpRegistKey, registeredHost.rpKey, Preferences(this).videoProfile)
|
|
||||||
Intent(this, StreamActivity::class.java).let {
|
|
||||||
it.putExtra(StreamActivity.EXTRA_CONNECT_INFO, connectInfo)
|
|
||||||
startActivity(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(host is DiscoveredDisplayHost && host.discoveredHost.state == DiscoveryHost.State.STANDBY)
|
|
||||||
{
|
|
||||||
MaterialAlertDialogBuilder(this)
|
|
||||||
.setMessage(R.string.alert_message_standby_wakeup)
|
|
||||||
.setPositiveButton(R.string.action_wakeup) { _, _ ->
|
|
||||||
wakeupHost(host)
|
|
||||||
}
|
|
||||||
.setNeutralButton(R.string.action_connect_immediately) { _, _ ->
|
|
||||||
connect()
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.action_connect_cancel_connect) { _, _ -> }
|
|
||||||
.create()
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
connect()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Intent(this, RegistActivity::class.java).let {
|
|
||||||
it.putExtra(RegistActivity.EXTRA_HOST, host.host)
|
|
||||||
it.putExtra(RegistActivity.EXTRA_BROADCAST, false)
|
|
||||||
if(host is ManualDisplayHost)
|
|
||||||
it.putExtra(RegistActivity.EXTRA_ASSIGN_MANUAL_HOST_ID, host.manualHost.id)
|
|
||||||
startActivity(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun wakeupHost(host: DisplayHost)
|
|
||||||
{
|
|
||||||
val registeredHost = host.registeredHost ?: return
|
|
||||||
viewModel.discoveryManager.sendWakeup(host.host, registeredHost.rpRegistKey, registeredHost.target.isPS5)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun editHost(host: DisplayHost)
|
|
||||||
{
|
|
||||||
if(host !is ManualDisplayHost)
|
|
||||||
return
|
|
||||||
Intent(this, EditManualConsoleActivity::class.java).also {
|
|
||||||
it.putExtra(EditManualConsoleActivity.EXTRA_MANUAL_HOST_ID, host.manualHost.id)
|
|
||||||
startActivity(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteHost(host: DisplayHost)
|
|
||||||
{
|
|
||||||
if(host !is ManualDisplayHost)
|
|
||||||
return
|
|
||||||
MaterialAlertDialogBuilder(this)
|
|
||||||
.setMessage(getString(R.string.alert_message_delete_manual_host, host.manualHost.host))
|
|
||||||
.setPositiveButton(R.string.action_delete) { _, _ ->
|
|
||||||
viewModel.deleteManualHost(host.manualHost)
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.action_keep) { _, _ -> }
|
|
||||||
.create()
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.main
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.metallic.chiaki.common.*
|
|
||||||
import com.metallic.chiaki.common.ext.toLiveData
|
|
||||||
import com.metallic.chiaki.discovery.DiscoveryManager
|
|
||||||
import com.metallic.chiaki.discovery.serverMac
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxkotlin.Observables
|
|
||||||
import io.reactivex.rxkotlin.addTo
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
|
|
||||||
class MainViewModel(val database: AppDatabase, val preferences: Preferences): ViewModel()
|
|
||||||
{
|
|
||||||
private val disposable = CompositeDisposable()
|
|
||||||
|
|
||||||
val discoveryManager = DiscoveryManager().also {
|
|
||||||
it.active = preferences.discoveryEnabled
|
|
||||||
it.discoveryActive
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { preferences.discoveryEnabled = it }
|
|
||||||
.addTo(disposable)
|
|
||||||
}
|
|
||||||
|
|
||||||
val displayHosts by lazy {
|
|
||||||
Observables.combineLatest(
|
|
||||||
database.manualHostDao().getAll().toObservable(),
|
|
||||||
database.registeredHostDao().getAll().toObservable(),
|
|
||||||
discoveryManager.discoveredHosts)
|
|
||||||
{ manualHosts, registeredHosts, discoveredHosts ->
|
|
||||||
val macRegisteredHosts = registeredHosts.associateBy { it.serverMac }
|
|
||||||
val idRegisteredHosts = registeredHosts.associateBy { it.id }
|
|
||||||
discoveredHosts.map {
|
|
||||||
DiscoveredDisplayHost(it.serverMac?.let { mac -> macRegisteredHosts[mac] }, it)
|
|
||||||
} +
|
|
||||||
manualHosts.map {
|
|
||||||
ManualDisplayHost(it.registeredHost?.let { id -> idRegisteredHosts[id] }, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toLiveData()
|
|
||||||
}
|
|
||||||
|
|
||||||
val discoveryActive by lazy {
|
|
||||||
discoveryManager.discoveryActive.toLiveData()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteManualHost(manualHost: ManualHost)
|
|
||||||
{
|
|
||||||
database.manualHostDao()
|
|
||||||
.delete(manualHost)
|
|
||||||
.onErrorComplete()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe()
|
|
||||||
.addTo(disposable)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared()
|
|
||||||
{
|
|
||||||
super.onCleared()
|
|
||||||
disposable.dispose()
|
|
||||||
discoveryManager.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.manualconsole
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.Window
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.common.RegisteredHost
|
|
||||||
import com.metallic.chiaki.common.ext.RevealActivity
|
|
||||||
import com.metallic.chiaki.common.ext.viewModelFactory
|
|
||||||
import com.metallic.chiaki.common.getDatabase
|
|
||||||
import com.metallic.chiaki.databinding.ActivityEditManualBinding
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxkotlin.addTo
|
|
||||||
|
|
||||||
class EditManualConsoleActivity: AppCompatActivity(), RevealActivity
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
const val EXTRA_MANUAL_HOST_ID = "manual_host_id"
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var viewModel: EditManualConsoleViewModel
|
|
||||||
private lateinit var binding: ActivityEditManualBinding
|
|
||||||
|
|
||||||
override val revealIntent: Intent get() = intent
|
|
||||||
override val revealRootLayout: View get() = binding.rootLayout
|
|
||||||
override val revealWindow: Window get() = window
|
|
||||||
|
|
||||||
private val disposable = CompositeDisposable()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?)
|
|
||||||
{
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ActivityEditManualBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
handleReveal()
|
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this, viewModelFactory {
|
|
||||||
EditManualConsoleViewModel(getDatabase(this),
|
|
||||||
if(intent.hasExtra(EXTRA_MANUAL_HOST_ID))
|
|
||||||
intent.getLongExtra(EXTRA_MANUAL_HOST_ID, 0)
|
|
||||||
else
|
|
||||||
null)
|
|
||||||
})
|
|
||||||
.get(EditManualConsoleViewModel::class.java)
|
|
||||||
|
|
||||||
viewModel.existingHost?.observe(this, Observer {
|
|
||||||
binding.hostEditText.setText(it.host)
|
|
||||||
})
|
|
||||||
|
|
||||||
viewModel.selectedRegisteredHost.observe(this, Observer {
|
|
||||||
binding.registeredHostTextView.setText(titleForRegisteredHost(it))
|
|
||||||
})
|
|
||||||
|
|
||||||
viewModel.registeredHosts.observe(this, Observer { hosts ->
|
|
||||||
binding.registeredHostTextView.setAdapter(ArrayAdapter<String>(this, R.layout.dropdown_menu_popup_item,
|
|
||||||
hosts.map { titleForRegisteredHost(it) }))
|
|
||||||
binding.registeredHostTextView.onItemClickListener = object: AdapterView.OnItemClickListener {
|
|
||||||
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long)
|
|
||||||
{
|
|
||||||
if(position >= hosts.size)
|
|
||||||
return
|
|
||||||
val host = hosts[position]
|
|
||||||
viewModel.selectedRegisteredHost.value = host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.saveButton.setOnClickListener { saveHost() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun titleForRegisteredHost(registeredHost: RegisteredHost?) =
|
|
||||||
if(registeredHost == null)
|
|
||||||
getString(R.string.add_manual_regist_on_connect)
|
|
||||||
else
|
|
||||||
"${registeredHost.serverNickname ?: ""} (${registeredHost.serverMac})"
|
|
||||||
|
|
||||||
private fun saveHost()
|
|
||||||
{
|
|
||||||
val host = binding.hostEditText.text.toString().trim()
|
|
||||||
if(host.isEmpty())
|
|
||||||
{
|
|
||||||
binding.hostEditText.error = getString(R.string.entered_host_invalid)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.saveButton.isEnabled = false
|
|
||||||
viewModel.saveHost(host)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
.addTo(disposable)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.manualconsole
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.metallic.chiaki.common.AppDatabase
|
|
||||||
import com.metallic.chiaki.common.ManualHost
|
|
||||||
import com.metallic.chiaki.common.RegisteredHost
|
|
||||||
import com.metallic.chiaki.common.ext.toLiveData
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
|
|
||||||
class EditManualConsoleViewModel(val database: AppDatabase, manualHostId: Long?): ViewModel()
|
|
||||||
{
|
|
||||||
val registeredHosts by lazy {
|
|
||||||
database.registeredHostDao().getAll().observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnNext { hosts ->
|
|
||||||
val selectedHost = selectedRegisteredHost.value
|
|
||||||
if(selectedHost != null)
|
|
||||||
selectedRegisteredHost.value = hosts.firstOrNull { it.id == selectedHost.id }
|
|
||||||
}
|
|
||||||
.map { listOf(null) + it }
|
|
||||||
.toLiveData()
|
|
||||||
}
|
|
||||||
|
|
||||||
val existingHost: LiveData<ManualHost>? =
|
|
||||||
if(manualHostId != null)
|
|
||||||
database.manualHostDao()
|
|
||||||
.getByIdWithRegisteredHost(manualHostId)
|
|
||||||
.toFlowable()
|
|
||||||
.doOnError {
|
|
||||||
Log.e("EditManualConsole", "Failed to fetch existing manual host", it)
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnNext { hosts ->
|
|
||||||
selectedRegisteredHost.value = hosts.registeredHost
|
|
||||||
}
|
|
||||||
.map { hosts -> hosts.manualHost }
|
|
||||||
.toLiveData()
|
|
||||||
else
|
|
||||||
null
|
|
||||||
|
|
||||||
var selectedRegisteredHost = MutableLiveData<RegisteredHost?>(null)
|
|
||||||
|
|
||||||
fun saveHost(host: String) =
|
|
||||||
database.manualHostDao()
|
|
||||||
.let {
|
|
||||||
val registeredHost = selectedRegisteredHost.value?.id
|
|
||||||
val existingHost = existingHost?.value
|
|
||||||
if(existingHost != null)
|
|
||||||
it.update(ManualHost(id = existingHost.id, host = host, registeredHost = registeredHost))
|
|
||||||
else
|
|
||||||
it.insert(ManualHost(host = host, registeredHost = registeredHost))
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.regist
|
|
||||||
|
|
||||||
import com.metallic.chiaki.lib.ChiakiLog
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.subjects.BehaviorSubject
|
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
import kotlin.concurrent.withLock
|
|
||||||
|
|
||||||
class ChiakiRxLog(levelMask: Int)
|
|
||||||
{
|
|
||||||
private val accSubject: BehaviorSubject<String> = BehaviorSubject.create<String>().also {
|
|
||||||
it.onNext("")
|
|
||||||
}
|
|
||||||
private val accMutex = ReentrantLock()
|
|
||||||
val logText: Observable<String> get() = accSubject
|
|
||||||
|
|
||||||
val log = ChiakiLog(levelMask, callback = { level, text ->
|
|
||||||
accMutex.withLock {
|
|
||||||
val cur = accSubject.value ?: ""
|
|
||||||
accSubject.onNext(cur + (if(cur.isEmpty()) "" else "\n") + ChiakiLog.formatLog(level, text))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,148 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.regist
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Base64
|
|
||||||
import android.view.View
|
|
||||||
import android.view.Window
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.common.ext.RevealActivity
|
|
||||||
import com.metallic.chiaki.databinding.ActivityRegistBinding
|
|
||||||
import com.metallic.chiaki.lib.RegistInfo
|
|
||||||
import com.metallic.chiaki.lib.Target
|
|
||||||
import java.lang.IllegalArgumentException
|
|
||||||
|
|
||||||
class RegistActivity: AppCompatActivity(), RevealActivity
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
const val EXTRA_HOST = "regist_host"
|
|
||||||
const val EXTRA_BROADCAST = "regist_broadcast"
|
|
||||||
const val EXTRA_ASSIGN_MANUAL_HOST_ID = "assign_manual_host_id"
|
|
||||||
|
|
||||||
private const val PIN_LENGTH = 8
|
|
||||||
|
|
||||||
private const val REQUEST_REGIST = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var viewModel: RegistViewModel
|
|
||||||
private lateinit var binding: ActivityRegistBinding
|
|
||||||
|
|
||||||
override val revealWindow: Window get() = window
|
|
||||||
override val revealIntent: Intent get() = intent
|
|
||||||
override val revealRootLayout: View get() = binding.rootLayout
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?)
|
|
||||||
{
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ActivityRegistBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
handleReveal()
|
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this).get(RegistViewModel::class.java)
|
|
||||||
|
|
||||||
binding.hostEditText.setText(intent.getStringExtra(EXTRA_HOST) ?: "255.255.255.255")
|
|
||||||
binding.broadcastCheckBox.isChecked = intent.getBooleanExtra(EXTRA_BROADCAST, true)
|
|
||||||
|
|
||||||
binding.registButton.setOnClickListener { doRegist() }
|
|
||||||
|
|
||||||
binding.ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5) {
|
|
||||||
RegistViewModel.ConsoleVersion.PS5 -> R.id.ps5RadioButton
|
|
||||||
RegistViewModel.ConsoleVersion.PS4_GE_8 -> R.id.ps4VersionGE8RadioButton
|
|
||||||
RegistViewModel.ConsoleVersion.PS4_GE_7 -> R.id.ps4VersionGE7RadioButton
|
|
||||||
RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.id.ps4VersionLT7RadioButton
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.ps4VersionRadioGroup.setOnCheckedChangeListener { _, checkedId ->
|
|
||||||
viewModel.ps4Version.value = when(checkedId)
|
|
||||||
{
|
|
||||||
R.id.ps5RadioButton -> RegistViewModel.ConsoleVersion.PS5
|
|
||||||
R.id.ps4VersionGE8RadioButton -> RegistViewModel.ConsoleVersion.PS4_GE_8
|
|
||||||
R.id.ps4VersionGE7RadioButton -> RegistViewModel.ConsoleVersion.PS4_GE_7
|
|
||||||
R.id.ps4VersionLT7RadioButton -> RegistViewModel.ConsoleVersion.PS4_LT_7
|
|
||||||
else -> RegistViewModel.ConsoleVersion.PS5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.ps4Version.observe(this, Observer {
|
|
||||||
binding.psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.ConsoleVersion.PS4_LT_7) View.GONE else View.VISIBLE
|
|
||||||
binding.psnIdTextInputLayout.hint = getString(when(it!!)
|
|
||||||
{
|
|
||||||
RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.string.hint_regist_psn_online_id
|
|
||||||
else -> R.string.hint_regist_psn_account_id
|
|
||||||
})
|
|
||||||
binding.pinHelpBeforeTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_before else R.string.regist_pin_instructions_ps4_before)
|
|
||||||
binding.pinHelpNavigationTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_navigation else R.string.regist_pin_instructions_ps4_navigation)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doRegist()
|
|
||||||
{
|
|
||||||
val ps4Version = viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5
|
|
||||||
|
|
||||||
val host = binding.hostEditText.text.toString().trim()
|
|
||||||
val hostValid = host.isNotEmpty()
|
|
||||||
val broadcast = binding.broadcastCheckBox.isChecked
|
|
||||||
|
|
||||||
val psnId = binding.psnIdEditText.text.toString().trim()
|
|
||||||
val psnOnlineId: String? = if(ps4Version == RegistViewModel.ConsoleVersion.PS4_LT_7) psnId else null
|
|
||||||
val psnAccountId: ByteArray? =
|
|
||||||
if(ps4Version != RegistViewModel.ConsoleVersion.PS4_LT_7)
|
|
||||||
try { Base64.decode(psnId, Base64.DEFAULT) } catch(e: IllegalArgumentException) { null }
|
|
||||||
else
|
|
||||||
null
|
|
||||||
val psnIdValid = when(ps4Version)
|
|
||||||
{
|
|
||||||
RegistViewModel.ConsoleVersion.PS4_LT_7 -> psnOnlineId?.isNotEmpty() ?: false
|
|
||||||
else -> psnAccountId != null && psnAccountId.size == RegistInfo.ACCOUNT_ID_SIZE
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val pin = binding.pinEditText.text.toString()
|
|
||||||
val pinValid = pin.length == PIN_LENGTH
|
|
||||||
|
|
||||||
binding.hostEditText.error = if(!hostValid) getString(R.string.entered_host_invalid) else null
|
|
||||||
binding.psnIdEditText.error =
|
|
||||||
if(!psnIdValid)
|
|
||||||
getString(when(ps4Version)
|
|
||||||
{
|
|
||||||
RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.string.regist_psn_online_id_invalid
|
|
||||||
else -> R.string.regist_psn_account_id_invalid
|
|
||||||
})
|
|
||||||
else
|
|
||||||
null
|
|
||||||
binding.pinEditText.error = if(!pinValid) getString(R.string.regist_pin_invalid, PIN_LENGTH) else null
|
|
||||||
|
|
||||||
if(!hostValid || !psnIdValid || !pinValid)
|
|
||||||
return
|
|
||||||
|
|
||||||
val target = when(ps4Version)
|
|
||||||
{
|
|
||||||
RegistViewModel.ConsoleVersion.PS5 -> Target.PS5_1
|
|
||||||
RegistViewModel.ConsoleVersion.PS4_GE_8 -> Target.PS4_10
|
|
||||||
RegistViewModel.ConsoleVersion.PS4_GE_7 -> Target.PS4_9
|
|
||||||
RegistViewModel.ConsoleVersion.PS4_LT_7 -> Target.PS4_8
|
|
||||||
}
|
|
||||||
|
|
||||||
val registInfo = RegistInfo(target, host, broadcast, psnOnlineId, psnAccountId, pin.toInt())
|
|
||||||
|
|
||||||
Intent(this, RegistExecuteActivity::class.java).also {
|
|
||||||
it.putExtra(RegistExecuteActivity.EXTRA_REGIST_INFO, registInfo)
|
|
||||||
if(intent.hasExtra(EXTRA_ASSIGN_MANUAL_HOST_ID))
|
|
||||||
it.putExtra(RegistExecuteActivity.EXTRA_ASSIGN_MANUAL_HOST_ID, intent.getLongExtra(EXTRA_ASSIGN_MANUAL_HOST_ID, 0L))
|
|
||||||
startActivityForResult(it, REQUEST_REGIST)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
|
|
||||||
{
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
if(requestCode == REQUEST_REGIST && resultCode == RESULT_OK)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.regist
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.method.ScrollingMovementMethod
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.common.MacAddress
|
|
||||||
import com.metallic.chiaki.common.ext.viewModelFactory
|
|
||||||
import com.metallic.chiaki.common.getDatabase
|
|
||||||
import com.metallic.chiaki.databinding.ActivityRegistExecuteBinding
|
|
||||||
import com.metallic.chiaki.lib.RegistInfo
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class RegistExecuteActivity: AppCompatActivity()
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
const val EXTRA_REGIST_INFO = "regist_info"
|
|
||||||
const val EXTRA_ASSIGN_MANUAL_HOST_ID = "assign_manual_host_id"
|
|
||||||
|
|
||||||
const val RESULT_FAILED = Activity.RESULT_FIRST_USER
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var viewModel: RegistExecuteViewModel
|
|
||||||
private lateinit var binding: ActivityRegistExecuteBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?)
|
|
||||||
{
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ActivityRegistExecuteBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this, viewModelFactory { RegistExecuteViewModel(getDatabase(this)) })
|
|
||||||
.get(RegistExecuteViewModel::class.java)
|
|
||||||
|
|
||||||
binding.logTextView.setHorizontallyScrolling(true)
|
|
||||||
binding.logTextView.movementMethod = ScrollingMovementMethod()
|
|
||||||
viewModel.logText.observe(this, Observer {
|
|
||||||
val textLayout = binding.logTextView.layout ?: return@Observer
|
|
||||||
val lineCount = textLayout.lineCount
|
|
||||||
if(lineCount < 1)
|
|
||||||
return@Observer
|
|
||||||
binding.logTextView.text = it
|
|
||||||
val scrollY = textLayout.getLineBottom(lineCount - 1) - binding.logTextView.height + binding.logTextView.paddingTop + binding.logTextView.paddingBottom
|
|
||||||
binding.logTextView.scrollTo(0, max(scrollY, 0))
|
|
||||||
})
|
|
||||||
|
|
||||||
viewModel.state.observe(this, Observer {
|
|
||||||
binding.progressBar.visibility = if(it == RegistExecuteViewModel.State.RUNNING) View.VISIBLE else View.GONE
|
|
||||||
when(it)
|
|
||||||
{
|
|
||||||
RegistExecuteViewModel.State.FAILED ->
|
|
||||||
{
|
|
||||||
binding.infoTextView.visibility = View.VISIBLE
|
|
||||||
binding.infoTextView.setText(R.string.regist_info_failed)
|
|
||||||
setResult(RESULT_FAILED)
|
|
||||||
}
|
|
||||||
RegistExecuteViewModel.State.SUCCESSFUL, RegistExecuteViewModel.State.SUCCESSFUL_DUPLICATE ->
|
|
||||||
{
|
|
||||||
binding.infoTextView.visibility = View.VISIBLE
|
|
||||||
binding.infoTextView.setText(R.string.regist_info_success)
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
if(it == RegistExecuteViewModel.State.SUCCESSFUL_DUPLICATE)
|
|
||||||
showDuplicateDialog()
|
|
||||||
}
|
|
||||||
RegistExecuteViewModel.State.STOPPED ->
|
|
||||||
{
|
|
||||||
binding.infoTextView.visibility = View.GONE
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
else -> binding.infoTextView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.shareLogButton.setOnClickListener {
|
|
||||||
val log = viewModel.logText.value ?: ""
|
|
||||||
Intent(Intent.ACTION_SEND).also {
|
|
||||||
it.type = "text/plain"
|
|
||||||
it.putExtra(Intent.EXTRA_TEXT, log)
|
|
||||||
startActivity(Intent.createChooser(it, resources.getString(R.string.action_share_log)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val registInfo = intent.getParcelableExtra<RegistInfo>(EXTRA_REGIST_INFO)
|
|
||||||
if(registInfo == null)
|
|
||||||
{
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModel.start(registInfo,
|
|
||||||
if(intent.hasExtra(EXTRA_ASSIGN_MANUAL_HOST_ID))
|
|
||||||
intent.getLongExtra(EXTRA_ASSIGN_MANUAL_HOST_ID, 0)
|
|
||||||
else
|
|
||||||
null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop()
|
|
||||||
{
|
|
||||||
super.onStop()
|
|
||||||
viewModel.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var dialog: AlertDialog? = null
|
|
||||||
|
|
||||||
private fun showDuplicateDialog()
|
|
||||||
{
|
|
||||||
if(dialog != null)
|
|
||||||
return
|
|
||||||
|
|
||||||
val macStr = viewModel.host?.serverMac?.let { MacAddress(it).toString() } ?: ""
|
|
||||||
|
|
||||||
dialog = MaterialAlertDialogBuilder(this)
|
|
||||||
.setMessage(getString(R.string.alert_regist_duplicate, macStr))
|
|
||||||
.setNegativeButton(R.string.action_regist_discard) { _, _ -> }
|
|
||||||
.setPositiveButton(R.string.action_regist_overwrite) { _, _ ->
|
|
||||||
viewModel.saveHost()
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
.also { it.show() }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.regist
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.metallic.chiaki.common.AppDatabase
|
|
||||||
import com.metallic.chiaki.common.MacAddress
|
|
||||||
import com.metallic.chiaki.common.RegisteredHost
|
|
||||||
import com.metallic.chiaki.common.ext.toLiveData
|
|
||||||
import com.metallic.chiaki.lib.*
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxkotlin.addTo
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
|
|
||||||
class RegistExecuteViewModel(val database: AppDatabase): ViewModel()
|
|
||||||
{
|
|
||||||
enum class State
|
|
||||||
{
|
|
||||||
IDLE,
|
|
||||||
RUNNING,
|
|
||||||
STOPPED,
|
|
||||||
FAILED,
|
|
||||||
SUCCESSFUL,
|
|
||||||
SUCCESSFUL_DUPLICATE,
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _state = MutableLiveData<State>(State.IDLE)
|
|
||||||
val state: LiveData<State> get() = _state
|
|
||||||
|
|
||||||
private val log = ChiakiRxLog(ChiakiLog.Level.ALL.value/* and ChiakiLog.Level.VERBOSE.value.inv()*/)
|
|
||||||
private var regist: Regist? = null
|
|
||||||
|
|
||||||
val logText: LiveData<String> = log.logText.toLiveData()
|
|
||||||
|
|
||||||
private val disposable = CompositeDisposable()
|
|
||||||
|
|
||||||
var host: RegistHost? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var assignManualHostId: Long? = null
|
|
||||||
|
|
||||||
fun start(info: RegistInfo, assignManualHostId: Long?)
|
|
||||||
{
|
|
||||||
if(regist != null)
|
|
||||||
return
|
|
||||||
try
|
|
||||||
{
|
|
||||||
regist = Regist(info, log.log, this::registEvent)
|
|
||||||
this.assignManualHostId = assignManualHostId
|
|
||||||
_state.value = State.RUNNING
|
|
||||||
}
|
|
||||||
catch(error: CreateError)
|
|
||||||
{
|
|
||||||
log.log.e("Failed to create Regist: ${error.errorCode}")
|
|
||||||
_state.value = State.FAILED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop()
|
|
||||||
{
|
|
||||||
regist?.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun registEvent(event: RegistEvent)
|
|
||||||
{
|
|
||||||
when(event)
|
|
||||||
{
|
|
||||||
is RegistEventCanceled -> _state.postValue(State.STOPPED)
|
|
||||||
is RegistEventFailed -> _state.postValue(State.FAILED)
|
|
||||||
is RegistEventSuccess -> registSuccess(event.host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun registSuccess(host: RegistHost)
|
|
||||||
{
|
|
||||||
this.host = host
|
|
||||||
database.registeredHostDao().getByMac(MacAddress(host.serverMac))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnSuccess {
|
|
||||||
_state.value = State.SUCCESSFUL_DUPLICATE
|
|
||||||
}
|
|
||||||
.doOnComplete {
|
|
||||||
saveHost()
|
|
||||||
}
|
|
||||||
.subscribe()
|
|
||||||
.addTo(disposable)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveHost()
|
|
||||||
{
|
|
||||||
val host = host ?: return
|
|
||||||
val assignManualHostId = assignManualHostId
|
|
||||||
val dao = database.registeredHostDao()
|
|
||||||
val manualHostDao = database.manualHostDao()
|
|
||||||
val registeredHost = RegisteredHost(host)
|
|
||||||
dao.deleteByMac(registeredHost.serverMac)
|
|
||||||
.andThen(dao.insert(registeredHost))
|
|
||||||
.let {
|
|
||||||
if(assignManualHostId != null)
|
|
||||||
it.flatMapCompletable { registeredHostId ->
|
|
||||||
manualHostDao.assignRegisteredHost(assignManualHostId, registeredHostId)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
it.ignoreElement()
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
Log.i("RegistExecute", "Registered Host saved in db")
|
|
||||||
_state.value = State.SUCCESSFUL
|
|
||||||
}
|
|
||||||
.addTo(disposable)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared()
|
|
||||||
{
|
|
||||||
super.onCleared()
|
|
||||||
regist?.dispose()
|
|
||||||
disposable.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.regist
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
|
|
||||||
class RegistViewModel: ViewModel()
|
|
||||||
{
|
|
||||||
enum class ConsoleVersion {
|
|
||||||
PS5,
|
|
||||||
PS4_GE_8,
|
|
||||||
PS4_GE_7,
|
|
||||||
PS4_LT_7;
|
|
||||||
|
|
||||||
val isPS5 get() = this == PS5
|
|
||||||
}
|
|
||||||
|
|
||||||
val ps4Version = MutableLiveData<ConsoleVersion>(ConsoleVersion.PS5)
|
|
||||||
}
|
|
|
@ -1,206 +0,0 @@
|
||||||
package com.metallic.chiaki.session
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.hardware.*
|
|
||||||
import android.view.*
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import com.metallic.chiaki.common.Preferences
|
|
||||||
import com.metallic.chiaki.lib.ControllerState
|
|
||||||
|
|
||||||
class StreamInput(val context: Context, val preferences: Preferences)
|
|
||||||
{
|
|
||||||
var controllerStateChangedCallback: ((ControllerState) -> Unit)? = null
|
|
||||||
|
|
||||||
val controllerState: ControllerState get()
|
|
||||||
{
|
|
||||||
val controllerState = sensorControllerState or keyControllerState or motionControllerState
|
|
||||||
|
|
||||||
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
when(windowManager.defaultDisplay.rotation)
|
|
||||||
{
|
|
||||||
Surface.ROTATION_90 -> {
|
|
||||||
controllerState.accelX *= -1.0f
|
|
||||||
controllerState.accelZ *= -1.0f
|
|
||||||
controllerState.gyroX *= -1.0f
|
|
||||||
controllerState.gyroZ *= -1.0f
|
|
||||||
controllerState.orientX *= -1.0f
|
|
||||||
controllerState.orientZ *= -1.0f
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prioritize motion controller's l2 and r2 over key
|
|
||||||
// (some controllers send only key, others both but key earlier than full press)
|
|
||||||
if(motionControllerState.l2State > 0U)
|
|
||||||
controllerState.l2State = motionControllerState.l2State
|
|
||||||
if(motionControllerState.r2State > 0U)
|
|
||||||
controllerState.r2State = motionControllerState.r2State
|
|
||||||
|
|
||||||
return controllerState or touchControllerState
|
|
||||||
}
|
|
||||||
|
|
||||||
private val sensorControllerState = ControllerState() // from Motion Sensors
|
|
||||||
private val keyControllerState = ControllerState() // from KeyEvents
|
|
||||||
private val motionControllerState = ControllerState() // from MotionEvents
|
|
||||||
var touchControllerState = ControllerState()
|
|
||||||
set(value)
|
|
||||||
{
|
|
||||||
field = value
|
|
||||||
controllerStateUpdated()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val swapCrossMoon = preferences.swapCrossMoon
|
|
||||||
|
|
||||||
private val sensorEventListener = object: SensorEventListener {
|
|
||||||
override fun onSensorChanged(event: SensorEvent)
|
|
||||||
{
|
|
||||||
when(event.sensor.type)
|
|
||||||
{
|
|
||||||
Sensor.TYPE_ACCELEROMETER -> {
|
|
||||||
sensorControllerState.accelX = event.values[1] / SensorManager.GRAVITY_EARTH
|
|
||||||
sensorControllerState.accelY = event.values[2] / SensorManager.GRAVITY_EARTH
|
|
||||||
sensorControllerState.accelZ = event.values[0] / SensorManager.GRAVITY_EARTH
|
|
||||||
}
|
|
||||||
Sensor.TYPE_GYROSCOPE -> {
|
|
||||||
sensorControllerState.gyroX = event.values[1]
|
|
||||||
sensorControllerState.gyroY = event.values[2]
|
|
||||||
sensorControllerState.gyroZ = event.values[0]
|
|
||||||
}
|
|
||||||
Sensor.TYPE_ROTATION_VECTOR -> {
|
|
||||||
val q = floatArrayOf(0f, 0f, 0f, 0f)
|
|
||||||
SensorManager.getQuaternionFromVector(q, event.values)
|
|
||||||
sensorControllerState.orientX = q[2]
|
|
||||||
sensorControllerState.orientY = q[3]
|
|
||||||
sensorControllerState.orientZ = q[1]
|
|
||||||
sensorControllerState.orientW = q[0]
|
|
||||||
}
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
controllerStateUpdated()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val motionLifecycleObserver = object: LifecycleObserver {
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
|
||||||
fun onResume()
|
|
||||||
{
|
|
||||||
val samplingPeriodUs = 4000
|
|
||||||
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
|
||||||
listOfNotNull(
|
|
||||||
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
|
|
||||||
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
|
|
||||||
sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
|
|
||||||
).forEach {
|
|
||||||
sensorManager.registerListener(sensorEventListener, it, samplingPeriodUs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
|
||||||
fun onPause()
|
|
||||||
{
|
|
||||||
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
|
||||||
sensorManager.unregisterListener(sensorEventListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun observe(lifecycleOwner: LifecycleOwner)
|
|
||||||
{
|
|
||||||
if(preferences.motionEnabled)
|
|
||||||
lifecycleOwner.lifecycle.addObserver(motionLifecycleObserver)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun controllerStateUpdated()
|
|
||||||
{
|
|
||||||
controllerStateChangedCallback?.let { it(controllerState) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispatchKeyEvent(event: KeyEvent): Boolean
|
|
||||||
{
|
|
||||||
//Log.i("StreamSession", "key event $event")
|
|
||||||
if(event.action != KeyEvent.ACTION_DOWN && event.action != KeyEvent.ACTION_UP)
|
|
||||||
return false
|
|
||||||
|
|
||||||
when(event.keyCode)
|
|
||||||
{
|
|
||||||
KeyEvent.KEYCODE_BUTTON_L2 -> {
|
|
||||||
keyControllerState.l2State = if(event.action == KeyEvent.ACTION_DOWN) UByte.MAX_VALUE else 0U
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
KeyEvent.KEYCODE_BUTTON_R2 -> {
|
|
||||||
keyControllerState.r2State = if(event.action == KeyEvent.ACTION_DOWN) UByte.MAX_VALUE else 0U
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val buttonMask: UInt = when(event.keyCode)
|
|
||||||
{
|
|
||||||
// dpad handled by MotionEvents
|
|
||||||
//KeyEvent.KEYCODE_DPAD_LEFT -> ControllerState.BUTTON_DPAD_LEFT
|
|
||||||
//KeyEvent.KEYCODE_DPAD_RIGHT -> ControllerState.BUTTON_DPAD_RIGHT
|
|
||||||
//KeyEvent.KEYCODE_DPAD_UP -> ControllerState.BUTTON_DPAD_UP
|
|
||||||
//KeyEvent.KEYCODE_DPAD_DOWN -> ControllerState.BUTTON_DPAD_DOWN
|
|
||||||
KeyEvent.KEYCODE_BUTTON_A -> if(swapCrossMoon) ControllerState.BUTTON_MOON else ControllerState.BUTTON_CROSS
|
|
||||||
KeyEvent.KEYCODE_BUTTON_B -> if(swapCrossMoon) ControllerState.BUTTON_CROSS else ControllerState.BUTTON_MOON
|
|
||||||
KeyEvent.KEYCODE_BUTTON_X -> if(swapCrossMoon) ControllerState.BUTTON_PYRAMID else ControllerState.BUTTON_BOX
|
|
||||||
KeyEvent.KEYCODE_BUTTON_Y -> if(swapCrossMoon) ControllerState.BUTTON_BOX else ControllerState.BUTTON_PYRAMID
|
|
||||||
KeyEvent.KEYCODE_BUTTON_L1 -> ControllerState.BUTTON_L1
|
|
||||||
KeyEvent.KEYCODE_BUTTON_R1 -> ControllerState.BUTTON_R1
|
|
||||||
KeyEvent.KEYCODE_BUTTON_THUMBL -> ControllerState.BUTTON_L3
|
|
||||||
KeyEvent.KEYCODE_BUTTON_THUMBR -> ControllerState.BUTTON_R3
|
|
||||||
KeyEvent.KEYCODE_BUTTON_SELECT -> ControllerState.BUTTON_SHARE
|
|
||||||
KeyEvent.KEYCODE_BUTTON_START -> ControllerState.BUTTON_OPTIONS
|
|
||||||
KeyEvent.KEYCODE_BUTTON_C -> ControllerState.BUTTON_PS
|
|
||||||
KeyEvent.KEYCODE_BUTTON_MODE -> ControllerState.BUTTON_PS
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
|
|
||||||
keyControllerState.buttons = keyControllerState.buttons.run {
|
|
||||||
when(event.action)
|
|
||||||
{
|
|
||||||
KeyEvent.ACTION_DOWN -> this or buttonMask
|
|
||||||
KeyEvent.ACTION_UP -> this and buttonMask.inv()
|
|
||||||
else -> this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
controllerStateUpdated()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onGenericMotionEvent(event: MotionEvent): Boolean
|
|
||||||
{
|
|
||||||
if(event.source and InputDevice.SOURCE_CLASS_JOYSTICK != InputDevice.SOURCE_CLASS_JOYSTICK)
|
|
||||||
return false
|
|
||||||
fun Float.signedAxis() = (this * Short.MAX_VALUE).toInt().toShort()
|
|
||||||
fun Float.unsignedAxis() = (this * UByte.MAX_VALUE.toFloat()).toUInt().toUByte()
|
|
||||||
motionControllerState.leftX = event.getAxisValue(MotionEvent.AXIS_X).signedAxis()
|
|
||||||
motionControllerState.leftY = event.getAxisValue(MotionEvent.AXIS_Y).signedAxis()
|
|
||||||
motionControllerState.rightX = event.getAxisValue(MotionEvent.AXIS_Z).signedAxis()
|
|
||||||
motionControllerState.rightY = event.getAxisValue(MotionEvent.AXIS_RZ).signedAxis()
|
|
||||||
motionControllerState.l2State = event.getAxisValue(MotionEvent.AXIS_LTRIGGER).unsignedAxis()
|
|
||||||
motionControllerState.r2State = event.getAxisValue(MotionEvent.AXIS_RTRIGGER).unsignedAxis()
|
|
||||||
motionControllerState.buttons = motionControllerState.buttons.let {
|
|
||||||
val dpadX = event.getAxisValue(MotionEvent.AXIS_HAT_X)
|
|
||||||
val dpadY = event.getAxisValue(MotionEvent.AXIS_HAT_Y)
|
|
||||||
val dpadButtons =
|
|
||||||
(if(dpadX > 0.5f) ControllerState.BUTTON_DPAD_RIGHT else 0U) or
|
|
||||||
(if(dpadX < -0.5f) ControllerState.BUTTON_DPAD_LEFT else 0U) or
|
|
||||||
(if(dpadY > 0.5f) ControllerState.BUTTON_DPAD_DOWN else 0U) or
|
|
||||||
(if(dpadY < -0.5f) ControllerState.BUTTON_DPAD_UP else 0U)
|
|
||||||
it and (ControllerState.BUTTON_DPAD_RIGHT or
|
|
||||||
ControllerState.BUTTON_DPAD_LEFT or
|
|
||||||
ControllerState.BUTTON_DPAD_DOWN or
|
|
||||||
ControllerState.BUTTON_DPAD_UP).inv() or
|
|
||||||
dpadButtons
|
|
||||||
}
|
|
||||||
//Log.i("StreamSession", "motionEvent => $motionControllerState")
|
|
||||||
controllerStateUpdated()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,147 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.session
|
|
||||||
|
|
||||||
import android.graphics.SurfaceTexture
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.*
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.metallic.chiaki.common.LogManager
|
|
||||||
import com.metallic.chiaki.lib.*
|
|
||||||
|
|
||||||
sealed class StreamState
|
|
||||||
object StreamStateIdle: StreamState()
|
|
||||||
object StreamStateConnecting: StreamState()
|
|
||||||
object StreamStateConnected: StreamState()
|
|
||||||
data class StreamStateCreateError(val error: CreateError): StreamState()
|
|
||||||
data class StreamStateQuit(val reason: QuitReason, val reasonString: String?): StreamState()
|
|
||||||
data class StreamStateLoginPinRequest(val pinIncorrect: Boolean): StreamState()
|
|
||||||
|
|
||||||
class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, val logVerbose: Boolean, val input: StreamInput)
|
|
||||||
{
|
|
||||||
var session: Session? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val _state = MutableLiveData<StreamState>(StreamStateIdle)
|
|
||||||
val state: LiveData<StreamState> get() = _state
|
|
||||||
private val _rumbleState = MutableLiveData<RumbleEvent>(RumbleEvent(0U, 0U))
|
|
||||||
val rumbleState: LiveData<RumbleEvent> get() = _rumbleState
|
|
||||||
|
|
||||||
private var surfaceTexture: SurfaceTexture? = null
|
|
||||||
private var surface: Surface? = null
|
|
||||||
|
|
||||||
init
|
|
||||||
{
|
|
||||||
input.controllerStateChangedCallback = {
|
|
||||||
session?.setControllerState(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun shutdown()
|
|
||||||
{
|
|
||||||
session?.stop()
|
|
||||||
session?.dispose()
|
|
||||||
session = null
|
|
||||||
_state.value = StreamStateIdle
|
|
||||||
//surfaceTexture?.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pause()
|
|
||||||
{
|
|
||||||
shutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resume()
|
|
||||||
{
|
|
||||||
if(session != null)
|
|
||||||
return
|
|
||||||
try
|
|
||||||
{
|
|
||||||
val session = Session(connectInfo, logManager.createNewFile().file.absolutePath, logVerbose)
|
|
||||||
_state.value = StreamStateConnecting
|
|
||||||
session.eventCallback = this::eventCallback
|
|
||||||
session.start()
|
|
||||||
val surface = surface
|
|
||||||
if(surface != null)
|
|
||||||
session.setSurface(surface)
|
|
||||||
this.session = session
|
|
||||||
}
|
|
||||||
catch(e: CreateError)
|
|
||||||
{
|
|
||||||
_state.value = StreamStateCreateError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun eventCallback(event: Event)
|
|
||||||
{
|
|
||||||
when(event)
|
|
||||||
{
|
|
||||||
is ConnectedEvent -> _state.postValue(StreamStateConnected)
|
|
||||||
is QuitEvent -> _state.postValue(
|
|
||||||
StreamStateQuit(
|
|
||||||
event.reason,
|
|
||||||
event.reasonString
|
|
||||||
)
|
|
||||||
)
|
|
||||||
is LoginPinRequestEvent -> _state.postValue(
|
|
||||||
StreamStateLoginPinRequest(
|
|
||||||
event.pinIncorrect
|
|
||||||
)
|
|
||||||
)
|
|
||||||
is RumbleEvent -> _rumbleState.postValue(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun attachToSurfaceView(surfaceView: SurfaceView)
|
|
||||||
{
|
|
||||||
surfaceView.holder.addCallback(object: SurfaceHolder.Callback {
|
|
||||||
override fun surfaceCreated(holder: SurfaceHolder) { }
|
|
||||||
|
|
||||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int)
|
|
||||||
{
|
|
||||||
val surface = holder.surface
|
|
||||||
this@StreamSession.surface = surface
|
|
||||||
session?.setSurface(surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun surfaceDestroyed(holder: SurfaceHolder)
|
|
||||||
{
|
|
||||||
this@StreamSession.surface = null
|
|
||||||
session?.setSurface(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun attachToTextureView(textureView: TextureView)
|
|
||||||
{
|
|
||||||
textureView.surfaceTextureListener = object: TextureView.SurfaceTextureListener {
|
|
||||||
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int)
|
|
||||||
{
|
|
||||||
if(surfaceTexture != null)
|
|
||||||
return
|
|
||||||
surfaceTexture = surface
|
|
||||||
this@StreamSession.surface = Surface(surfaceTexture)
|
|
||||||
session?.setSurface(Surface(surface))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean
|
|
||||||
{
|
|
||||||
// return false if we want to keep the surface texture
|
|
||||||
return surfaceTexture == null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { }
|
|
||||||
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
val surfaceTexture = surfaceTexture
|
|
||||||
if(surfaceTexture != null)
|
|
||||||
textureView.setSurfaceTexture(surfaceTexture)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLoginPin(pin: String)
|
|
||||||
{
|
|
||||||
session?.setLoginPin(pin)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.settings
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Rect
|
|
||||||
import androidx.core.graphics.withClip
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
|
|
||||||
abstract class ItemTouchSwipeCallback(context: Context): ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT)
|
|
||||||
{
|
|
||||||
private val backgroundDrawable = context.getDrawable(R.color.item_delete_background)
|
|
||||||
private val icon = context.getDrawable(R.drawable.ic_delete_row)
|
|
||||||
|
|
||||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false
|
|
||||||
|
|
||||||
override fun onChildDraw(canvas: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean)
|
|
||||||
{
|
|
||||||
val itemView = viewHolder.itemView
|
|
||||||
val itemHeight = itemView.bottom - itemView.top
|
|
||||||
|
|
||||||
val bounds = Rect(
|
|
||||||
itemView.right + dX.toInt(),
|
|
||||||
itemView.top,
|
|
||||||
itemView.right,
|
|
||||||
itemView.bottom
|
|
||||||
)
|
|
||||||
|
|
||||||
backgroundDrawable?.bounds = bounds
|
|
||||||
backgroundDrawable?.draw(canvas)
|
|
||||||
|
|
||||||
val icon = icon
|
|
||||||
if(icon != null)
|
|
||||||
{
|
|
||||||
val iconMargin = (itemHeight - icon.intrinsicHeight) / 2
|
|
||||||
val iconTop = itemView.top + iconMargin
|
|
||||||
val iconLeft = itemView.right - iconMargin - icon.intrinsicWidth
|
|
||||||
val iconRight = itemView.right - iconMargin
|
|
||||||
val iconBottom = iconTop + icon.intrinsicHeight
|
|
||||||
canvas.withClip(bounds) {
|
|
||||||
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
|
|
||||||
icon.draw(canvas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.settings
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.databinding.ActivitySettingsBinding
|
|
||||||
|
|
||||||
interface TitleFragment
|
|
||||||
{
|
|
||||||
fun getTitle(resources: Resources): String
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsActivity: AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
|
|
||||||
{
|
|
||||||
private lateinit var binding: ActivitySettingsBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?)
|
|
||||||
{
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
title = ""
|
|
||||||
setSupportActionBar(binding.toolbar)
|
|
||||||
|
|
||||||
val rootFragment = SettingsFragment()
|
|
||||||
replaceFragment(rootFragment, false)
|
|
||||||
supportFragmentManager.addOnBackStackChangedListener {
|
|
||||||
val titleFragment = supportFragmentManager.findFragmentById(R.id.settingsFragment) as? TitleFragment ?: return@addOnBackStackChangedListener
|
|
||||||
binding.titleTextView.text = titleFragment.getTitle(resources)
|
|
||||||
}
|
|
||||||
binding.titleTextView.text = rootFragment.getTitle(resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference) = when(pref.fragment)
|
|
||||||
{
|
|
||||||
SettingsRegisteredHostsFragment::class.java.canonicalName -> {
|
|
||||||
replaceFragment(SettingsRegisteredHostsFragment(), true)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun replaceFragment(fragment: Fragment, addToBackStack: Boolean)
|
|
||||||
{
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
|
|
||||||
.replace(R.id.settingsFragment, fragment)
|
|
||||||
.also {
|
|
||||||
if(addToBackStack)
|
|
||||||
it.addToBackStack(null)
|
|
||||||
}
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,174 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.settings
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.InputType
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.preference.*
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.common.Preferences
|
|
||||||
import com.metallic.chiaki.common.exportAndShareAllSettings
|
|
||||||
import com.metallic.chiaki.common.ext.viewModelFactory
|
|
||||||
import com.metallic.chiaki.common.getDatabase
|
|
||||||
import com.metallic.chiaki.common.importSettingsFromUri
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxkotlin.addTo
|
|
||||||
|
|
||||||
class DataStore(val preferences: Preferences): PreferenceDataStore()
|
|
||||||
{
|
|
||||||
override fun getBoolean(key: String?, defValue: Boolean) = when(key)
|
|
||||||
{
|
|
||||||
preferences.logVerboseKey -> preferences.logVerbose
|
|
||||||
preferences.swapCrossMoonKey -> preferences.swapCrossMoon
|
|
||||||
preferences.rumbleEnabledKey -> preferences.rumbleEnabled
|
|
||||||
preferences.motionEnabledKey -> preferences.motionEnabled
|
|
||||||
preferences.buttonHapticEnabledKey -> preferences.buttonHapticEnabled
|
|
||||||
else -> defValue
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putBoolean(key: String?, value: Boolean)
|
|
||||||
{
|
|
||||||
when(key)
|
|
||||||
{
|
|
||||||
preferences.logVerboseKey -> preferences.logVerbose = value
|
|
||||||
preferences.swapCrossMoonKey -> preferences.swapCrossMoon = value
|
|
||||||
preferences.rumbleEnabledKey -> preferences.rumbleEnabled = value
|
|
||||||
preferences.motionEnabledKey -> preferences.motionEnabled = value
|
|
||||||
preferences.buttonHapticEnabledKey -> preferences.buttonHapticEnabled = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getString(key: String, defValue: String?) = when(key)
|
|
||||||
{
|
|
||||||
preferences.resolutionKey -> preferences.resolution.value
|
|
||||||
preferences.fpsKey -> preferences.fps.value
|
|
||||||
preferences.bitrateKey -> preferences.bitrate?.toString() ?: ""
|
|
||||||
preferences.codecKey -> preferences.codec.value
|
|
||||||
else -> defValue
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putString(key: String, value: String?)
|
|
||||||
{
|
|
||||||
when(key)
|
|
||||||
{
|
|
||||||
preferences.resolutionKey ->
|
|
||||||
{
|
|
||||||
val resolution = Preferences.Resolution.values().firstOrNull { it.value == value } ?: return
|
|
||||||
preferences.resolution = resolution
|
|
||||||
}
|
|
||||||
preferences.fpsKey ->
|
|
||||||
{
|
|
||||||
val fps = Preferences.FPS.values().firstOrNull { it.value == value } ?: return
|
|
||||||
preferences.fps = fps
|
|
||||||
}
|
|
||||||
preferences.bitrateKey -> preferences.bitrate = value?.toIntOrNull()
|
|
||||||
preferences.codecKey ->
|
|
||||||
{
|
|
||||||
val codec = Preferences.Codec.values().firstOrNull { it.value == value } ?: return
|
|
||||||
preferences.codec = codec
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsFragment: PreferenceFragmentCompat(), TitleFragment
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
private const val PICK_SETTINGS_JSON_REQUEST = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
private var disposable = CompositeDisposable()
|
|
||||||
private var exportDisposable = CompositeDisposable().also { it.addTo(disposable) }
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?)
|
|
||||||
{
|
|
||||||
val context = context ?: return
|
|
||||||
|
|
||||||
val viewModel = ViewModelProvider(this, viewModelFactory { SettingsViewModel(getDatabase(context), Preferences(context)) })
|
|
||||||
.get(SettingsViewModel::class.java)
|
|
||||||
|
|
||||||
val preferences = viewModel.preferences
|
|
||||||
preferenceManager.preferenceDataStore = DataStore(preferences)
|
|
||||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
|
||||||
|
|
||||||
preferenceScreen.findPreference<ListPreference>(getString(R.string.preferences_resolution_key))?.let {
|
|
||||||
it.entryValues = Preferences.resolutionAll.map { res -> res.value }.toTypedArray()
|
|
||||||
it.entries = Preferences.resolutionAll.map { res -> getString(res.title) }.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
preferenceScreen.findPreference<ListPreference>(getString(R.string.preferences_fps_key))?.let {
|
|
||||||
it.entryValues = Preferences.fpsAll.map { fps -> fps.value }.toTypedArray()
|
|
||||||
it.entries = Preferences.fpsAll.map { fps -> getString(fps.title) }.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
val bitratePreference = preferenceScreen.findPreference<EditTextPreference>(getString(R.string.preferences_bitrate_key))
|
|
||||||
val bitrateSummaryProvider = Preference.SummaryProvider<EditTextPreference> {
|
|
||||||
preferences.bitrate?.toString() ?: getString(R.string.preferences_bitrate_auto, preferences.bitrateAuto)
|
|
||||||
}
|
|
||||||
bitratePreference?.let {
|
|
||||||
it.summaryProvider = bitrateSummaryProvider
|
|
||||||
it.setOnBindEditTextListener { editText ->
|
|
||||||
editText.hint = getString(R.string.preferences_bitrate_auto, preferences.bitrateAuto)
|
|
||||||
editText.inputType = InputType.TYPE_CLASS_NUMBER
|
|
||||||
editText.setText(preferences.bitrate?.toString() ?: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.bitrateAuto.observe(this, Observer {
|
|
||||||
bitratePreference?.summaryProvider = bitrateSummaryProvider
|
|
||||||
})
|
|
||||||
|
|
||||||
preferenceScreen.findPreference<ListPreference>(getString(R.string.preferences_codec_key))?.let {
|
|
||||||
it.entryValues = Preferences.codecAll.map { codec -> codec.value }.toTypedArray()
|
|
||||||
it.entries = Preferences.codecAll.map { codec -> getString(codec.title) }.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
val registeredHostsPreference = preferenceScreen.findPreference<Preference>("registered_hosts")
|
|
||||||
viewModel.registeredHostsCount.observe(this, Observer {
|
|
||||||
registeredHostsPreference?.summary = getString(R.string.preferences_registered_hosts_summary, it)
|
|
||||||
})
|
|
||||||
|
|
||||||
preferenceScreen.findPreference<Preference>(getString(R.string.preferences_export_settings_key))?.setOnPreferenceClickListener { exportSettings(); true }
|
|
||||||
preferenceScreen.findPreference<Preference>(getString(R.string.preferences_import_settings_key))?.setOnPreferenceClickListener { importSettings(); true }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy()
|
|
||||||
{
|
|
||||||
super.onDestroy()
|
|
||||||
disposable.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTitle(resources: Resources): String = resources.getString(R.string.title_settings)
|
|
||||||
|
|
||||||
private fun exportSettings()
|
|
||||||
{
|
|
||||||
val activity = activity ?: return
|
|
||||||
exportDisposable.clear()
|
|
||||||
exportAndShareAllSettings(activity).addTo(exportDisposable)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importSettings()
|
|
||||||
{
|
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "application/json"
|
|
||||||
}
|
|
||||||
startActivityForResult(intent, PICK_SETTINGS_JSON_REQUEST)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
|
|
||||||
{
|
|
||||||
if(requestCode == PICK_SETTINGS_JSON_REQUEST && resultCode == Activity.RESULT_OK)
|
|
||||||
{
|
|
||||||
val activity = activity ?: return
|
|
||||||
data?.data?.also {
|
|
||||||
importSettingsFromUri(activity, it, disposable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.settings
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.common.LogFile
|
|
||||||
import com.metallic.chiaki.common.ext.inflate
|
|
||||||
import com.metallic.chiaki.databinding.ItemLogFileBinding
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class SettingsLogsAdapter: RecyclerView.Adapter<SettingsLogsAdapter.ViewHolder>()
|
|
||||||
{
|
|
||||||
var shareCallback: ((LogFile) -> Unit)? = null
|
|
||||||
|
|
||||||
private val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
|
|
||||||
private val timeFormat = SimpleDateFormat("HH:mm:ss:SSS", Locale.getDefault())
|
|
||||||
|
|
||||||
class ViewHolder(val binding: ItemLogFileBinding): RecyclerView.ViewHolder(binding.root)
|
|
||||||
|
|
||||||
var logFiles: List<LogFile> = listOf()
|
|
||||||
set(value)
|
|
||||||
{
|
|
||||||
field = value
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
|
||||||
ViewHolder(ItemLogFileBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
|
|
||||||
override fun getItemCount() = logFiles.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int)
|
|
||||||
{
|
|
||||||
val logFile = logFiles[position]
|
|
||||||
holder.binding.nameTextView.text = "${dateFormat.format(logFile.date)} ${timeFormat.format(logFile.date)}"
|
|
||||||
holder.binding.summaryTextView.text = logFile.filename
|
|
||||||
holder.binding.shareButton.setOnClickListener { shareCallback?.let { it(logFile) } }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.settings
|
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.common.LogFile
|
|
||||||
import com.metallic.chiaki.common.LogManager
|
|
||||||
import com.metallic.chiaki.common.ext.viewModelFactory
|
|
||||||
import com.metallic.chiaki.common.fileProviderAuthority
|
|
||||||
import com.metallic.chiaki.databinding.FragmentSettingsLogsBinding
|
|
||||||
|
|
||||||
class SettingsLogsFragment: AppCompatDialogFragment(), TitleFragment
|
|
||||||
{
|
|
||||||
private lateinit var viewModel: SettingsLogsViewModel
|
|
||||||
|
|
||||||
private var _binding: FragmentSettingsLogsBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
|
||||||
FragmentSettingsLogsBinding.inflate(inflater, container, false).let {
|
|
||||||
_binding = it
|
|
||||||
it.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
|
|
||||||
{
|
|
||||||
val context = requireContext()
|
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this, viewModelFactory { SettingsLogsViewModel(LogManager(context)) })
|
|
||||||
.get(SettingsLogsViewModel::class.java)
|
|
||||||
|
|
||||||
val adapter = SettingsLogsAdapter()
|
|
||||||
binding.logsRecyclerView.layoutManager = LinearLayoutManager(context)
|
|
||||||
binding.logsRecyclerView.adapter = adapter
|
|
||||||
adapter.shareCallback = this::shareLogFile
|
|
||||||
viewModel.sessionLogs.observe(viewLifecycleOwner, Observer {
|
|
||||||
adapter.logFiles = it
|
|
||||||
binding.emptyInfoGroup.visibility = if(it.isEmpty()) View.VISIBLE else View.GONE
|
|
||||||
})
|
|
||||||
|
|
||||||
val itemTouchSwipeCallback = object : ItemTouchSwipeCallback(context)
|
|
||||||
{
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int)
|
|
||||||
{
|
|
||||||
val pos = viewHolder.adapterPosition
|
|
||||||
val file = viewModel.sessionLogs.value?.getOrNull(pos) ?: return
|
|
||||||
viewModel.deleteLog(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ItemTouchHelper(itemTouchSwipeCallback).attachToRecyclerView(binding.logsRecyclerView)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTitle(resources: Resources): String = resources.getString(R.string.preferences_logs_title)
|
|
||||||
|
|
||||||
private fun shareLogFile(file: LogFile)
|
|
||||||
{
|
|
||||||
val activity = activity ?: return
|
|
||||||
val uri = FileProvider.getUriForFile(activity, fileProviderAuthority, file.file)
|
|
||||||
Intent(Intent.ACTION_SEND).also {
|
|
||||||
it.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
it.type = "text/plain"
|
|
||||||
it.putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
it.clipData = ClipData.newRawUri("", uri)
|
|
||||||
startActivity(Intent.createChooser(it, resources.getString(R.string.action_share_log)))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.settings
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.metallic.chiaki.common.AppDatabase
|
|
||||||
import com.metallic.chiaki.common.LogFile
|
|
||||||
import com.metallic.chiaki.common.LogManager
|
|
||||||
import com.metallic.chiaki.common.RegisteredHost
|
|
||||||
import com.metallic.chiaki.common.ext.toLiveData
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxkotlin.addTo
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
|
|
||||||
class SettingsLogsViewModel(val logManager: LogManager): ViewModel()
|
|
||||||
{
|
|
||||||
private val _sessionLogs = MutableLiveData<List<LogFile>>(logManager.files)
|
|
||||||
val sessionLogs: LiveData<List<LogFile>> get() = _sessionLogs
|
|
||||||
|
|
||||||
private fun updateLogs()
|
|
||||||
{
|
|
||||||
_sessionLogs.value = logManager.files
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteLog(file: LogFile)
|
|
||||||
{
|
|
||||||
file.file.delete()
|
|
||||||
updateLogs()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.settings
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.metallic.chiaki.common.RegisteredHost
|
|
||||||
import com.metallic.chiaki.databinding.ItemRegisteredHostBinding
|
|
||||||
|
|
||||||
class SettingsRegisteredHostsAdapter: RecyclerView.Adapter<SettingsRegisteredHostsAdapter.ViewHolder>()
|
|
||||||
{
|
|
||||||
class ViewHolder(val binding: ItemRegisteredHostBinding): RecyclerView.ViewHolder(binding.root)
|
|
||||||
|
|
||||||
var hosts: List<RegisteredHost> = listOf()
|
|
||||||
set(value)
|
|
||||||
{
|
|
||||||
field = value
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
|
|
||||||
= ViewHolder(ItemRegisteredHostBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
|
|
||||||
override fun getItemCount() = hosts.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int)
|
|
||||||
{
|
|
||||||
val host = hosts[position]
|
|
||||||
holder.binding.nameTextView.text = "${host.serverNickname} (${if(host.target.isPS5) "PS5" else "PS4"})"
|
|
||||||
holder.binding.summaryTextView.text = host.serverMac.toString()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.settings
|
|
||||||
|
|
||||||
import android.app.ActivityOptions
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.common.ext.putRevealExtra
|
|
||||||
import com.metallic.chiaki.common.ext.viewModelFactory
|
|
||||||
import com.metallic.chiaki.common.getDatabase
|
|
||||||
import com.metallic.chiaki.databinding.FragmentSettingsRegisteredHostsBinding
|
|
||||||
import com.metallic.chiaki.regist.RegistActivity
|
|
||||||
|
|
||||||
class SettingsRegisteredHostsFragment: AppCompatDialogFragment(), TitleFragment
|
|
||||||
{
|
|
||||||
private lateinit var viewModel: SettingsRegisteredHostsViewModel
|
|
||||||
|
|
||||||
private var _binding: FragmentSettingsRegisteredHostsBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
|
||||||
FragmentSettingsRegisteredHostsBinding.inflate(inflater, container, false).let {
|
|
||||||
_binding = it
|
|
||||||
it.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
|
|
||||||
{
|
|
||||||
val context = requireContext()
|
|
||||||
viewModel = ViewModelProvider(this, viewModelFactory { SettingsRegisteredHostsViewModel(getDatabase(context)) })
|
|
||||||
.get(SettingsRegisteredHostsViewModel::class.java)
|
|
||||||
|
|
||||||
val adapter = SettingsRegisteredHostsAdapter()
|
|
||||||
binding.hostsRecyclerView.layoutManager = LinearLayoutManager(context)
|
|
||||||
binding.hostsRecyclerView.adapter = adapter
|
|
||||||
val itemTouchSwipeCallback = object : ItemTouchSwipeCallback(context)
|
|
||||||
{
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int)
|
|
||||||
{
|
|
||||||
val pos = viewHolder.adapterPosition
|
|
||||||
val host = viewModel.registeredHosts.value?.getOrNull(pos) ?: return
|
|
||||||
MaterialAlertDialogBuilder(viewHolder.itemView.context)
|
|
||||||
.setMessage(getString(R.string.alert_message_delete_registered_host, host.serverNickname, host.serverMac.toString()))
|
|
||||||
.setPositiveButton(R.string.action_delete) { _, _ ->
|
|
||||||
viewModel.deleteHost(host)
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.action_keep) { _, _ ->
|
|
||||||
adapter.notifyItemChanged(pos) // to reset the swipe
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ItemTouchHelper(itemTouchSwipeCallback).attachToRecyclerView(binding.hostsRecyclerView)
|
|
||||||
viewModel.registeredHosts.observe(this, Observer {
|
|
||||||
adapter.hosts = it
|
|
||||||
binding.emptyInfoGroup.visibility = if(it.isEmpty()) View.VISIBLE else View.GONE
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.floatingActionButton.setOnClickListener {
|
|
||||||
Intent(context, RegistActivity::class.java).also {
|
|
||||||
it.putRevealExtra(binding.floatingActionButton, binding.rootLayout)
|
|
||||||
startActivity(it, ActivityOptions.makeSceneTransitionAnimation(activity).toBundle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTitle(resources: Resources): String = resources.getString(R.string.preferences_registered_hosts_title)
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.settings
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.metallic.chiaki.common.AppDatabase
|
|
||||||
import com.metallic.chiaki.common.RegisteredHost
|
|
||||||
import com.metallic.chiaki.common.ext.toLiveData
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxkotlin.addTo
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
|
|
||||||
class SettingsRegisteredHostsViewModel(val database: AppDatabase): ViewModel()
|
|
||||||
{
|
|
||||||
private val disposable = CompositeDisposable()
|
|
||||||
|
|
||||||
val registeredHosts by lazy {
|
|
||||||
database.registeredHostDao().getAll().toLiveData()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteHost(host: RegisteredHost)
|
|
||||||
{
|
|
||||||
database.registeredHostDao()
|
|
||||||
.delete(host)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe()
|
|
||||||
.addTo(disposable)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared()
|
|
||||||
{
|
|
||||||
super.onCleared()
|
|
||||||
disposable.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.settings
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.metallic.chiaki.common.AppDatabase
|
|
||||||
import com.metallic.chiaki.common.Preferences
|
|
||||||
import com.metallic.chiaki.common.ext.toLiveData
|
|
||||||
|
|
||||||
class SettingsViewModel(val database: AppDatabase, val preferences: Preferences): ViewModel()
|
|
||||||
{
|
|
||||||
val registeredHostsCount by lazy {
|
|
||||||
database.registeredHostDao().count().toLiveData()
|
|
||||||
}
|
|
||||||
|
|
||||||
val bitrateAuto by lazy {
|
|
||||||
preferences.bitrateAutoObservable.toLiveData()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
package com.metallic.chiaki.stream
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
|
|
||||||
// see ExoPlayer's AspectRatioFrameLayout
|
|
||||||
class AspectRatioFrameLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null): FrameLayout(context, attrs)
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
private const val MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f
|
|
||||||
}
|
|
||||||
|
|
||||||
var aspectRatio = 0f
|
|
||||||
set(value)
|
|
||||||
{
|
|
||||||
if(field != value)
|
|
||||||
{
|
|
||||||
field = value
|
|
||||||
requestLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mode: TransformMode = TransformMode.FIT
|
|
||||||
set(value)
|
|
||||||
{
|
|
||||||
if(field != value)
|
|
||||||
{
|
|
||||||
field = value
|
|
||||||
requestLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
|
|
||||||
{
|
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
||||||
if(aspectRatio <= 0)
|
|
||||||
{
|
|
||||||
// Aspect ratio not set.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var width = measuredWidth
|
|
||||||
var height = measuredHeight
|
|
||||||
val viewAspectRatio = width.toFloat() / height
|
|
||||||
val aspectDeformation = aspectRatio / viewAspectRatio - 1
|
|
||||||
if(Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION)
|
|
||||||
return
|
|
||||||
when(mode)
|
|
||||||
{
|
|
||||||
TransformMode.ZOOM ->
|
|
||||||
if(aspectDeformation > 0)
|
|
||||||
width = (height * aspectRatio).toInt()
|
|
||||||
else
|
|
||||||
height = (width / aspectRatio).toInt()
|
|
||||||
TransformMode.FIT ->
|
|
||||||
if(aspectDeformation > 0)
|
|
||||||
height = (width / aspectRatio).toInt()
|
|
||||||
else
|
|
||||||
width = (height * aspectRatio).toInt()
|
|
||||||
TransformMode.STRETCH -> {}
|
|
||||||
}
|
|
||||||
super.onMeasure(
|
|
||||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
||||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,400 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.stream
|
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorListenerAdapter
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.graphics.Matrix
|
|
||||||
import android.os.*
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.EditText
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.*
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.common.Preferences
|
|
||||||
import com.metallic.chiaki.common.ext.viewModelFactory
|
|
||||||
import com.metallic.chiaki.databinding.ActivityStreamBinding
|
|
||||||
import com.metallic.chiaki.lib.ConnectInfo
|
|
||||||
import com.metallic.chiaki.lib.ConnectVideoProfile
|
|
||||||
import com.metallic.chiaki.session.*
|
|
||||||
import com.metallic.chiaki.touchcontrols.DefaultTouchControlsFragment
|
|
||||||
import com.metallic.chiaki.touchcontrols.TouchControlsFragment
|
|
||||||
import com.metallic.chiaki.touchcontrols.TouchpadOnlyFragment
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxkotlin.addTo
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
private sealed class DialogContents
|
|
||||||
private object StreamQuitDialog: DialogContents()
|
|
||||||
private object CreateErrorDialog: DialogContents()
|
|
||||||
private object PinRequestDialog: DialogContents()
|
|
||||||
|
|
||||||
class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListener
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
const val EXTRA_CONNECT_INFO = "connect_info"
|
|
||||||
private const val HIDE_UI_TIMEOUT_MS = 2000L
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var viewModel: StreamViewModel
|
|
||||||
private lateinit var binding: ActivityStreamBinding
|
|
||||||
|
|
||||||
private val uiVisibilityHandler = Handler()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?)
|
|
||||||
{
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
val connectInfo = intent.getParcelableExtra<ConnectInfo>(EXTRA_CONNECT_INFO)
|
|
||||||
if(connectInfo == null)
|
|
||||||
{
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this, viewModelFactory {
|
|
||||||
StreamViewModel(application, connectInfo)
|
|
||||||
})[StreamViewModel::class.java]
|
|
||||||
|
|
||||||
viewModel.input.observe(this)
|
|
||||||
|
|
||||||
binding = ActivityStreamBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
window.decorView.setOnSystemUiVisibilityChangeListener(this)
|
|
||||||
|
|
||||||
viewModel.onScreenControlsEnabled.observe(this, Observer {
|
|
||||||
if(binding.onScreenControlsSwitch.isChecked != it)
|
|
||||||
binding.onScreenControlsSwitch.isChecked = it
|
|
||||||
if(binding.onScreenControlsSwitch.isChecked)
|
|
||||||
binding.touchpadOnlySwitch.isChecked = false
|
|
||||||
})
|
|
||||||
binding.onScreenControlsSwitch.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
viewModel.setOnScreenControlsEnabled(isChecked)
|
|
||||||
showOverlay()
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.touchpadOnlyEnabled.observe(this, Observer {
|
|
||||||
if(binding.touchpadOnlySwitch.isChecked != it)
|
|
||||||
binding.touchpadOnlySwitch.isChecked = it
|
|
||||||
if(binding.touchpadOnlySwitch.isChecked)
|
|
||||||
binding.onScreenControlsSwitch.isChecked = false
|
|
||||||
})
|
|
||||||
binding.touchpadOnlySwitch.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
viewModel.setTouchpadOnlyEnabled(isChecked)
|
|
||||||
showOverlay()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.displayModeToggle.addOnButtonCheckedListener { _, _, _ ->
|
|
||||||
adjustStreamViewAspect()
|
|
||||||
showOverlay()
|
|
||||||
}
|
|
||||||
|
|
||||||
//viewModel.session.attachToTextureView(textureView)
|
|
||||||
viewModel.session.attachToSurfaceView(binding.surfaceView)
|
|
||||||
viewModel.session.state.observe(this, Observer { this.stateChanged(it) })
|
|
||||||
adjustStreamViewAspect()
|
|
||||||
|
|
||||||
if(Preferences(this).rumbleEnabled)
|
|
||||||
{
|
|
||||||
val vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator
|
|
||||||
viewModel.session.rumbleState.observe(this, Observer {
|
|
||||||
val amplitude = min(255, (it.left.toInt() + it.right.toInt()) / 2)
|
|
||||||
vibrator.cancel()
|
|
||||||
if(amplitude == 0)
|
|
||||||
return@Observer
|
|
||||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
|
||||||
vibrator.vibrate(VibrationEffect.createOneShot(1000, amplitude))
|
|
||||||
else
|
|
||||||
vibrator.vibrate(1000)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val controlsDisposable = CompositeDisposable()
|
|
||||||
|
|
||||||
override fun onAttachFragment(fragment: Fragment)
|
|
||||||
{
|
|
||||||
super.onAttachFragment(fragment)
|
|
||||||
if(fragment is TouchControlsFragment)
|
|
||||||
{
|
|
||||||
fragment.controllerState
|
|
||||||
.subscribe { viewModel.input.touchControllerState = it }
|
|
||||||
.addTo(controlsDisposable)
|
|
||||||
fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled
|
|
||||||
if(fragment is TouchpadOnlyFragment)
|
|
||||||
fragment.touchpadOnlyEnabled = viewModel.touchpadOnlyEnabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume()
|
|
||||||
{
|
|
||||||
super.onResume()
|
|
||||||
hideSystemUI()
|
|
||||||
viewModel.session.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause()
|
|
||||||
{
|
|
||||||
super.onPause()
|
|
||||||
viewModel.session.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy()
|
|
||||||
{
|
|
||||||
super.onDestroy()
|
|
||||||
controlsDisposable.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reconnect()
|
|
||||||
{
|
|
||||||
viewModel.session.shutdown()
|
|
||||||
viewModel.session.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val hideSystemUIRunnable = Runnable { hideSystemUI() }
|
|
||||||
|
|
||||||
override fun onSystemUiVisibilityChange(visibility: Int)
|
|
||||||
{
|
|
||||||
if(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
|
|
||||||
showOverlay()
|
|
||||||
else
|
|
||||||
hideOverlay()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showOverlay()
|
|
||||||
{
|
|
||||||
binding.overlay.isVisible = true
|
|
||||||
binding.overlay.animate()
|
|
||||||
.alpha(1.0f)
|
|
||||||
.setListener(object: AnimatorListenerAdapter()
|
|
||||||
{
|
|
||||||
override fun onAnimationEnd(animation: Animator)
|
|
||||||
{
|
|
||||||
binding.overlay.alpha = 1.0f
|
|
||||||
}
|
|
||||||
})
|
|
||||||
uiVisibilityHandler.removeCallbacks(hideSystemUIRunnable)
|
|
||||||
uiVisibilityHandler.postDelayed(hideSystemUIRunnable, HIDE_UI_TIMEOUT_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideOverlay()
|
|
||||||
{
|
|
||||||
binding.overlay.animate()
|
|
||||||
.alpha(0.0f)
|
|
||||||
.setListener(object: AnimatorListenerAdapter()
|
|
||||||
{
|
|
||||||
override fun onAnimationEnd(animation: Animator)
|
|
||||||
{
|
|
||||||
binding.overlay.isGone = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowFocusChanged(hasFocus: Boolean)
|
|
||||||
{
|
|
||||||
super.onWindowFocusChanged(hasFocus)
|
|
||||||
if(hasFocus)
|
|
||||||
hideSystemUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideSystemUI()
|
|
||||||
{
|
|
||||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var dialogContents: DialogContents? = null
|
|
||||||
private var dialog: AlertDialog? = null
|
|
||||||
set(value)
|
|
||||||
{
|
|
||||||
field = value
|
|
||||||
if(value == null)
|
|
||||||
dialogContents = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stateChanged(state: StreamState)
|
|
||||||
{
|
|
||||||
binding.progressBar.visibility = if(state == StreamStateConnecting) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
when(state)
|
|
||||||
{
|
|
||||||
is StreamStateQuit ->
|
|
||||||
{
|
|
||||||
if(dialogContents != StreamQuitDialog)
|
|
||||||
{
|
|
||||||
if(state.reason.isError)
|
|
||||||
{
|
|
||||||
dialog?.dismiss()
|
|
||||||
val reasonStr = state.reasonString
|
|
||||||
val dialog = MaterialAlertDialogBuilder(this)
|
|
||||||
.setMessage(getString(R.string.alert_message_session_quit, state.reason.toString())
|
|
||||||
+ (if(reasonStr != null) "\n$reasonStr" else ""))
|
|
||||||
.setPositiveButton(R.string.action_reconnect) { _, _ ->
|
|
||||||
dialog = null
|
|
||||||
reconnect()
|
|
||||||
}
|
|
||||||
.setOnCancelListener {
|
|
||||||
dialog = null
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.action_quit_session) { _, _ ->
|
|
||||||
dialog = null
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
dialogContents = StreamQuitDialog
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is StreamStateCreateError ->
|
|
||||||
{
|
|
||||||
if(dialogContents != CreateErrorDialog)
|
|
||||||
{
|
|
||||||
dialog?.dismiss()
|
|
||||||
val dialog = MaterialAlertDialogBuilder(this)
|
|
||||||
.setMessage(getString(R.string.alert_message_session_create_error, state.error.errorCode.toString()))
|
|
||||||
.setOnDismissListener {
|
|
||||||
dialog = null
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.action_quit_session) { _, _ -> }
|
|
||||||
.create()
|
|
||||||
dialogContents = CreateErrorDialog
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is StreamStateLoginPinRequest ->
|
|
||||||
{
|
|
||||||
if(dialogContents != PinRequestDialog)
|
|
||||||
{
|
|
||||||
dialog?.dismiss()
|
|
||||||
|
|
||||||
val view = layoutInflater.inflate(R.layout.dialog_login_pin, null)
|
|
||||||
val pinEditText = view.findViewById<EditText>(R.id.pinEditText)
|
|
||||||
|
|
||||||
val dialog = MaterialAlertDialogBuilder(this)
|
|
||||||
.setMessage(
|
|
||||||
if(state.pinIncorrect)
|
|
||||||
R.string.alert_message_login_pin_request_incorrect
|
|
||||||
else
|
|
||||||
R.string.alert_message_login_pin_request)
|
|
||||||
.setView(view)
|
|
||||||
.setPositiveButton(R.string.action_login_pin_connect) { _, _ ->
|
|
||||||
dialog = null
|
|
||||||
viewModel.session.setLoginPin(pinEditText.text.toString())
|
|
||||||
}
|
|
||||||
.setOnCancelListener {
|
|
||||||
dialog = null
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.action_quit_session) { _, _ ->
|
|
||||||
dialog = null
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
dialogContents = PinRequestDialog
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustTextureViewAspect(textureView: TextureView)
|
|
||||||
{
|
|
||||||
val trans = TextureViewTransform(viewModel.session.connectInfo.videoProfile, textureView)
|
|
||||||
val resolution = trans.resolutionFor(TransformMode.fromButton(binding.displayModeToggle.checkedButtonId))
|
|
||||||
Matrix().also {
|
|
||||||
textureView.getTransform(it)
|
|
||||||
it.setScale(resolution.width / trans.viewWidth, resolution.height / trans.viewHeight)
|
|
||||||
it.postTranslate((trans.viewWidth - resolution.width) * 0.5f, (trans.viewHeight - resolution.height) * 0.5f)
|
|
||||||
textureView.setTransform(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustSurfaceViewAspect()
|
|
||||||
{
|
|
||||||
val videoProfile = viewModel.session.connectInfo.videoProfile
|
|
||||||
binding.aspectRatioLayout.aspectRatio = videoProfile.width.toFloat() / videoProfile.height.toFloat()
|
|
||||||
binding.aspectRatioLayout.mode = TransformMode.fromButton(binding.displayModeToggle.checkedButtonId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustStreamViewAspect() = adjustSurfaceViewAspect()
|
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
|
|
||||||
override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class TransformMode
|
|
||||||
{
|
|
||||||
FIT,
|
|
||||||
STRETCH,
|
|
||||||
ZOOM;
|
|
||||||
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
fun fromButton(displayModeButtonId: Int)
|
|
||||||
= when (displayModeButtonId)
|
|
||||||
{
|
|
||||||
R.id.display_mode_stretch_button -> STRETCH
|
|
||||||
R.id.display_mode_zoom_button -> ZOOM
|
|
||||||
else -> FIT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TextureViewTransform(private val videoProfile: ConnectVideoProfile, private val textureView: TextureView)
|
|
||||||
{
|
|
||||||
private val contentWidth : Float get() = videoProfile.width.toFloat()
|
|
||||||
private val contentHeight : Float get() = videoProfile.height.toFloat()
|
|
||||||
val viewWidth : Float get() = textureView.width.toFloat()
|
|
||||||
val viewHeight : Float get() = textureView.height.toFloat()
|
|
||||||
private val contentAspect : Float get() = contentHeight / contentWidth
|
|
||||||
|
|
||||||
fun resolutionFor(mode: TransformMode): Resolution
|
|
||||||
= when(mode)
|
|
||||||
{
|
|
||||||
TransformMode.STRETCH -> strechedResolution
|
|
||||||
TransformMode.ZOOM -> zoomedResolution
|
|
||||||
TransformMode.FIT -> normalResolution
|
|
||||||
}
|
|
||||||
|
|
||||||
private val strechedResolution get() = Resolution(viewWidth, viewHeight)
|
|
||||||
|
|
||||||
private val zoomedResolution get() =
|
|
||||||
if(viewHeight > viewWidth * contentAspect)
|
|
||||||
{
|
|
||||||
val zoomFactor = viewHeight / contentHeight
|
|
||||||
Resolution(contentWidth * zoomFactor, viewHeight)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
val zoomFactor = viewWidth / contentWidth
|
|
||||||
Resolution(viewWidth, contentHeight * zoomFactor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val normalResolution get() =
|
|
||||||
if(viewHeight > viewWidth * contentAspect)
|
|
||||||
Resolution(viewWidth, viewWidth * contentAspect)
|
|
||||||
else
|
|
||||||
Resolution(viewHeight / contentAspect, viewHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
data class Resolution(val width: Float, val height: Float)
|
|
|
@ -1,46 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.stream
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.*
|
|
||||||
import com.metallic.chiaki.common.LogManager
|
|
||||||
import com.metallic.chiaki.session.StreamSession
|
|
||||||
import com.metallic.chiaki.common.Preferences
|
|
||||||
import com.metallic.chiaki.lib.*
|
|
||||||
import com.metallic.chiaki.session.StreamInput
|
|
||||||
|
|
||||||
class StreamViewModel(val application: Application, val connectInfo: ConnectInfo): ViewModel()
|
|
||||||
{
|
|
||||||
val preferences = Preferences(application)
|
|
||||||
val logManager = LogManager(application)
|
|
||||||
|
|
||||||
private var _session: StreamSession? = null
|
|
||||||
val input = StreamInput(application, preferences)
|
|
||||||
val session = StreamSession(connectInfo, logManager, preferences.logVerbose, input)
|
|
||||||
|
|
||||||
private var _onScreenControlsEnabled = MutableLiveData<Boolean>(preferences.onScreenControlsEnabled)
|
|
||||||
val onScreenControlsEnabled: LiveData<Boolean> get() = _onScreenControlsEnabled
|
|
||||||
|
|
||||||
private var _touchpadOnlyEnabled = MutableLiveData<Boolean>(preferences.touchpadOnlyEnabled)
|
|
||||||
val touchpadOnlyEnabled: LiveData<Boolean> get() = _touchpadOnlyEnabled
|
|
||||||
|
|
||||||
override fun onCleared()
|
|
||||||
{
|
|
||||||
super.onCleared()
|
|
||||||
_session?.shutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setOnScreenControlsEnabled(enabled: Boolean)
|
|
||||||
{
|
|
||||||
preferences.onScreenControlsEnabled = enabled
|
|
||||||
_onScreenControlsEnabled.value = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTouchpadOnlyEnabled(enabled: Boolean)
|
|
||||||
{
|
|
||||||
preferences.touchpadOnlyEnabled = enabled
|
|
||||||
_touchpadOnlyEnabled.value = enabled
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.touchcontrols
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
class AnalogStickView @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
|
||||||
) : View(context, attrs, defStyleAttr)
|
|
||||||
{
|
|
||||||
val radius: Float
|
|
||||||
private val handleRadius: Float
|
|
||||||
private val drawableBase: Drawable?
|
|
||||||
private val drawableHandle: Drawable?
|
|
||||||
|
|
||||||
var state = Vector(0f, 0f)
|
|
||||||
private set(value)
|
|
||||||
{
|
|
||||||
field = value
|
|
||||||
stateChangedCallback?.let { it(field) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var stateChangedCallback: ((Vector) -> Unit)? = null
|
|
||||||
|
|
||||||
private val touchTracker = TouchTracker().also {
|
|
||||||
it.positionChangedCallback = this::updateState
|
|
||||||
}
|
|
||||||
|
|
||||||
private var center: Vector? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as state, but scaled to the circle
|
|
||||||
*/
|
|
||||||
private var handlePosition: Vector = Vector(0f, 0f)
|
|
||||||
|
|
||||||
private val clipBoundsTmp = Rect()
|
|
||||||
|
|
||||||
init
|
|
||||||
{
|
|
||||||
context.theme.obtainStyledAttributes(attrs, R.styleable.AnalogStickView, 0, 0).apply {
|
|
||||||
radius = getDimension(R.styleable.AnalogStickView_radius, 0f)
|
|
||||||
handleRadius = getDimension(R.styleable.AnalogStickView_handleRadius, 0f)
|
|
||||||
drawableBase = getDrawable(R.styleable.AnalogStickView_drawableBase)
|
|
||||||
drawableHandle = getDrawable(R.styleable.AnalogStickView_drawableHandle)
|
|
||||||
recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas)
|
|
||||||
{
|
|
||||||
super.onDraw(canvas)
|
|
||||||
|
|
||||||
val center = center
|
|
||||||
if(center != null)
|
|
||||||
{
|
|
||||||
val circleRadius = radius + handleRadius
|
|
||||||
drawableBase?.setBounds((center.x - circleRadius).toInt(), (center.y - circleRadius).toInt(), (center.x + circleRadius).toInt(), (center.y + circleRadius).toInt())
|
|
||||||
drawableBase?.draw(canvas)
|
|
||||||
|
|
||||||
val handleX = center.x + handlePosition.x * radius
|
|
||||||
val handleY = center.y + handlePosition.y * radius
|
|
||||||
drawableHandle?.setBounds((handleX - handleRadius).toInt(), (handleY - handleRadius).toInt(), (handleX + handleRadius).toInt(),(handleY + handleRadius).toInt())
|
|
||||||
drawableHandle?.draw(canvas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateState(position: Vector?)
|
|
||||||
{
|
|
||||||
if(radius <= 0f)
|
|
||||||
return
|
|
||||||
|
|
||||||
if(position == null)
|
|
||||||
{
|
|
||||||
center = null
|
|
||||||
state = Vector(0f, 0f)
|
|
||||||
handlePosition = Vector(0f, 0f)
|
|
||||||
invalidate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val center: Vector = this.center ?: position
|
|
||||||
this.center = center
|
|
||||||
|
|
||||||
val dir = position - center
|
|
||||||
val length = dir.length
|
|
||||||
if(length > 0)
|
|
||||||
{
|
|
||||||
val strength = if(length > radius) 1.0f else length / radius
|
|
||||||
val dirNormalized = dir / length
|
|
||||||
handlePosition = dirNormalized * strength
|
|
||||||
val dirBoxNormalized =
|
|
||||||
if(abs(dirNormalized.x) > abs(dirNormalized.y))
|
|
||||||
dirNormalized / abs(dirNormalized.x)
|
|
||||||
else
|
|
||||||
dirNormalized / abs(dirNormalized.y)
|
|
||||||
state = dirBoxNormalized * strength
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
handlePosition = Vector(0f, 0f)
|
|
||||||
state = Vector(0f, 0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean
|
|
||||||
{
|
|
||||||
touchTracker.touchEvent(event)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package com.metallic.chiaki.touchcontrols
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.VibrationEffect
|
|
||||||
import android.os.Vibrator
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.metallic.chiaki.common.Preferences
|
|
||||||
|
|
||||||
class ButtonHaptics(val context: Context)
|
|
||||||
{
|
|
||||||
private val enabled = Preferences(context).buttonHapticEnabled
|
|
||||||
|
|
||||||
fun trigger(harder: Boolean = false)
|
|
||||||
{
|
|
||||||
if(!enabled)
|
|
||||||
return
|
|
||||||
val vibrator = context.getSystemService(AppCompatActivity.VIBRATOR_SERVICE) as Vibrator
|
|
||||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
|
||||||
vibrator.vibrate(
|
|
||||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
|
||||||
VibrationEffect.createPredefined(if(harder) VibrationEffect.EFFECT_CLICK else VibrationEffect.EFFECT_TICK)
|
|
||||||
else
|
|
||||||
VibrationEffect.createOneShot(10, if(harder) 200 else 100)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
vibrator.vibrate(10)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.touchcontrols
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.children
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
|
|
||||||
class ButtonView @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
|
||||||
) : View(context, attrs, defStyleAttr)
|
|
||||||
{
|
|
||||||
private val haptics = ButtonHaptics(context)
|
|
||||||
|
|
||||||
var buttonPressed = false
|
|
||||||
private set(value)
|
|
||||||
{
|
|
||||||
val diff = field != value
|
|
||||||
field = value
|
|
||||||
if(diff)
|
|
||||||
{
|
|
||||||
if(value)
|
|
||||||
haptics.trigger()
|
|
||||||
invalidate()
|
|
||||||
buttonPressedCallback?.let { it(field) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var buttonPressedCallback: ((Boolean) -> Unit)? = null
|
|
||||||
|
|
||||||
private val drawableIdle: Drawable?
|
|
||||||
private val drawablePressed: Drawable?
|
|
||||||
|
|
||||||
init
|
|
||||||
{
|
|
||||||
context.theme.obtainStyledAttributes(attrs, R.styleable.ButtonView, 0, 0).apply {
|
|
||||||
drawableIdle = getDrawable(R.styleable.ButtonView_drawableIdle)
|
|
||||||
drawablePressed = getDrawable(R.styleable.ButtonView_drawablePressed)
|
|
||||||
recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
isClickable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas)
|
|
||||||
{
|
|
||||||
super.onDraw(canvas)
|
|
||||||
val drawable = if(buttonPressed) drawablePressed else drawableIdle
|
|
||||||
drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom)
|
|
||||||
drawable?.draw(canvas)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this button overlaps with others in the same layout,
|
|
||||||
* let the one whose center is closest to the touch handle it.
|
|
||||||
*/
|
|
||||||
private fun bestFittingTouchView(x: Float, y: Float): View
|
|
||||||
{
|
|
||||||
val loc = locationOnScreen + Vector(x, y)
|
|
||||||
return (parent as? ViewGroup)?.children?.filter {
|
|
||||||
it is ButtonView
|
|
||||||
}?.filter {
|
|
||||||
val pos = it.locationOnScreen
|
|
||||||
loc.x >= pos.x && loc.x < pos.x + it.width && loc.y >= pos.y && loc.y < pos.y + it.height
|
|
||||||
}?.sortedBy {
|
|
||||||
(loc - (it.locationOnScreen + Vector(it.width.toFloat(), it.height.toFloat()) * 0.5f)).lengthSq
|
|
||||||
}?.firstOrNull() ?: this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean
|
|
||||||
{
|
|
||||||
when(event.actionMasked)
|
|
||||||
{
|
|
||||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
|
|
||||||
if(bestFittingTouchView(event.getX(event.actionIndex), event.getY(event.actionIndex)) != this)
|
|
||||||
return false
|
|
||||||
buttonPressed = true
|
|
||||||
}
|
|
||||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
|
|
||||||
buttonPressed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.touchcontrols
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
|
|
||||||
class ControlsBackgroundView @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
|
||||||
) : View(context, attrs, defStyleAttr)
|
|
||||||
{
|
|
||||||
override fun onTouchEvent(event: MotionEvent) = true
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.touchcontrols
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import kotlin.math.PI
|
|
||||||
import kotlin.math.atan2
|
|
||||||
|
|
||||||
class DPadView @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
|
||||||
) : View(context, attrs, defStyleAttr)
|
|
||||||
{
|
|
||||||
private val haptics = ButtonHaptics(context)
|
|
||||||
|
|
||||||
enum class Direction {
|
|
||||||
LEFT,
|
|
||||||
RIGHT,
|
|
||||||
UP,
|
|
||||||
DOWN,
|
|
||||||
LEFT_UP,
|
|
||||||
RIGHT_UP,
|
|
||||||
LEFT_DOWN,
|
|
||||||
RIGHT_DOWN;
|
|
||||||
|
|
||||||
val isDiagonal get() = when(this) {
|
|
||||||
LEFT_UP, RIGHT_UP, LEFT_DOWN, RIGHT_DOWN -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var state: Direction? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
var stateChangeCallback: ((Direction?) -> Unit)? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Radius (as a fraction of the entire DPad Radius)
|
|
||||||
* to be used as a deadzone in the center on move events
|
|
||||||
*/
|
|
||||||
private val deadzoneRadius = 0.3f
|
|
||||||
|
|
||||||
private val dpadIdleDrawable = VectorDrawableCompat.create(resources, R.drawable.control_dpad_idle, null)
|
|
||||||
private val dpadLeftDrawable = VectorDrawableCompat.create(resources, R.drawable.control_dpad_left, null)
|
|
||||||
private val dpadLeftUpDrawable = VectorDrawableCompat.create(resources, R.drawable.control_dpad_left_up, null)
|
|
||||||
|
|
||||||
private val touchTracker = TouchTracker().also {
|
|
||||||
it.positionChangedCallback = this::updateState
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas)
|
|
||||||
{
|
|
||||||
super.onDraw(canvas)
|
|
||||||
|
|
||||||
val state = state
|
|
||||||
val drawable: VectorDrawableCompat?
|
|
||||||
if(state != null)
|
|
||||||
{
|
|
||||||
drawable = if(state.isDiagonal) dpadLeftUpDrawable else dpadLeftDrawable
|
|
||||||
when(state)
|
|
||||||
{
|
|
||||||
Direction.UP, Direction.RIGHT_UP -> canvas.rotate(90f, width.toFloat() * 0.5f, height.toFloat() * 0.5f)
|
|
||||||
Direction.DOWN, Direction.LEFT_DOWN -> canvas.rotate(90f*3f, width.toFloat() * 0.5f, height.toFloat() * 0.5f)
|
|
||||||
Direction.LEFT, Direction.LEFT_UP -> {}
|
|
||||||
Direction.RIGHT, Direction.RIGHT_DOWN -> canvas.rotate(90f*2f, width.toFloat() * 0.5f, height.toFloat() * 0.5f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
drawable = dpadIdleDrawable
|
|
||||||
|
|
||||||
drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom)
|
|
||||||
//drawable?.alpha = 127
|
|
||||||
drawable?.draw(canvas)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun directionForPosition(position: Vector): Direction
|
|
||||||
{
|
|
||||||
val dir = (position / Vector(width.toFloat(), height.toFloat()) - 0.5f) * 2.0f
|
|
||||||
val angleSection = PI.toFloat() * 2.0f / 8.0f
|
|
||||||
val angle = atan2(dir.x, dir.y) + PI + angleSection * 0.5f
|
|
||||||
return when
|
|
||||||
{
|
|
||||||
angle < 1.0f * angleSection -> Direction.UP
|
|
||||||
angle < 2.0f * angleSection -> Direction.LEFT_UP
|
|
||||||
angle < 3.0f * angleSection -> Direction.LEFT
|
|
||||||
angle < 4.0f * angleSection -> Direction.LEFT_DOWN
|
|
||||||
angle < 5.0f * angleSection -> Direction.DOWN
|
|
||||||
angle < 6.0f * angleSection -> Direction.RIGHT_DOWN
|
|
||||||
angle < 7.0f * angleSection -> Direction.RIGHT
|
|
||||||
angle < 8.0f * angleSection -> Direction.RIGHT_UP
|
|
||||||
else -> Direction.UP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateState(position: Vector?)
|
|
||||||
{
|
|
||||||
val newState =
|
|
||||||
if(position == null)
|
|
||||||
null
|
|
||||||
else
|
|
||||||
{
|
|
||||||
val xFrac = 2.0f * (position.x / width.toFloat() - 0.5f)
|
|
||||||
val yFrac = 2.0f * (position.y / height.toFloat() - 0.5f)
|
|
||||||
val radiusSq = xFrac * xFrac + yFrac * yFrac
|
|
||||||
if(radiusSq < deadzoneRadius * deadzoneRadius && state != null)
|
|
||||||
state
|
|
||||||
else
|
|
||||||
directionForPosition(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(state != newState)
|
|
||||||
{
|
|
||||||
if(newState != null)
|
|
||||||
haptics.trigger()
|
|
||||||
state = newState
|
|
||||||
invalidate()
|
|
||||||
stateChangeCallback?.let { it(state) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean
|
|
||||||
{
|
|
||||||
touchTracker.touchEvent(event)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.touchcontrols
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.metallic.chiaki.databinding.FragmentControlsBinding
|
|
||||||
import com.metallic.chiaki.lib.ControllerState
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.rxkotlin.Observables.combineLatest
|
|
||||||
import io.reactivex.subjects.BehaviorSubject
|
|
||||||
import io.reactivex.subjects.Subject
|
|
||||||
|
|
||||||
abstract class TouchControlsFragment : Fragment()
|
|
||||||
{
|
|
||||||
protected var ownControllerState = ControllerState()
|
|
||||||
set(value)
|
|
||||||
{
|
|
||||||
val diff = field != value
|
|
||||||
field = value
|
|
||||||
if(diff)
|
|
||||||
ownControllerStateSubject.onNext(ownControllerState)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected val ownControllerStateSubject: Subject<ControllerState>
|
|
||||||
= BehaviorSubject.create<ControllerState>().also { it.onNext(ownControllerState) }
|
|
||||||
|
|
||||||
// to delay attaching to the touchpadView until it's available
|
|
||||||
protected val controllerStateProxy: Subject<Observable<ControllerState>>
|
|
||||||
= BehaviorSubject.create<Observable<ControllerState>>().also { it.onNext(ownControllerStateSubject) }
|
|
||||||
val controllerState: Observable<ControllerState> get() =
|
|
||||||
controllerStateProxy.flatMap { it }
|
|
||||||
|
|
||||||
var onScreenControlsEnabled: LiveData<Boolean>? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
class DefaultTouchControlsFragment : TouchControlsFragment()
|
|
||||||
{
|
|
||||||
private var _binding: FragmentControlsBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
|
||||||
FragmentControlsBinding.inflate(inflater, container, false).let {
|
|
||||||
_binding = it
|
|
||||||
controllerStateProxy.onNext(
|
|
||||||
combineLatest(ownControllerStateSubject, binding.touchpadView.controllerState) { a, b -> a or b }
|
|
||||||
)
|
|
||||||
it.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
|
|
||||||
{
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
binding.dpadView.stateChangeCallback = this::dpadStateChanged
|
|
||||||
binding.crossButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_CROSS)
|
|
||||||
binding.moonButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_MOON)
|
|
||||||
binding.pyramidButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PYRAMID)
|
|
||||||
binding.boxButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_BOX)
|
|
||||||
binding.l1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L1)
|
|
||||||
binding.r1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R1)
|
|
||||||
binding.l3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L3)
|
|
||||||
binding.r3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R3)
|
|
||||||
binding.optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS)
|
|
||||||
binding.shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE)
|
|
||||||
binding.psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS)
|
|
||||||
|
|
||||||
binding.l2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { l2State = if(it) 255U else 0U } }
|
|
||||||
binding.r2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { r2State = if(it) 255U else 0U } }
|
|
||||||
|
|
||||||
val quantizeStick = { f: Float ->
|
|
||||||
(Short.MAX_VALUE * f).toInt().toShort()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.leftAnalogStickView.stateChangedCallback = { ownControllerState = ownControllerState.copy().apply {
|
|
||||||
leftX = quantizeStick(it.x)
|
|
||||||
leftY = quantizeStick(it.y)
|
|
||||||
}}
|
|
||||||
|
|
||||||
binding.rightAnalogStickView.stateChangedCallback = { ownControllerState = ownControllerState.copy().apply {
|
|
||||||
rightX = quantizeStick(it.x)
|
|
||||||
rightY = quantizeStick(it.y)
|
|
||||||
}}
|
|
||||||
|
|
||||||
onScreenControlsEnabled?.observe(viewLifecycleOwner, Observer {
|
|
||||||
view.visibility = if(it) View.VISIBLE else View.GONE
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dpadStateChanged(direction: DPadView.Direction?)
|
|
||||||
{
|
|
||||||
ownControllerState = ownControllerState.copy().apply {
|
|
||||||
buttons = ((buttons
|
|
||||||
and ControllerState.BUTTON_DPAD_LEFT.inv()
|
|
||||||
and ControllerState.BUTTON_DPAD_RIGHT.inv()
|
|
||||||
and ControllerState.BUTTON_DPAD_UP.inv()
|
|
||||||
and ControllerState.BUTTON_DPAD_DOWN.inv())
|
|
||||||
or when(direction)
|
|
||||||
{
|
|
||||||
DPadView.Direction.UP -> ControllerState.BUTTON_DPAD_UP
|
|
||||||
DPadView.Direction.DOWN -> ControllerState.BUTTON_DPAD_DOWN
|
|
||||||
DPadView.Direction.LEFT -> ControllerState.BUTTON_DPAD_LEFT
|
|
||||||
DPadView.Direction.RIGHT -> ControllerState.BUTTON_DPAD_RIGHT
|
|
||||||
DPadView.Direction.LEFT_UP -> ControllerState.BUTTON_DPAD_LEFT or ControllerState.BUTTON_DPAD_UP
|
|
||||||
DPadView.Direction.LEFT_DOWN -> ControllerState.BUTTON_DPAD_LEFT or ControllerState.BUTTON_DPAD_DOWN
|
|
||||||
DPadView.Direction.RIGHT_UP -> ControllerState.BUTTON_DPAD_RIGHT or ControllerState.BUTTON_DPAD_UP
|
|
||||||
DPadView.Direction.RIGHT_DOWN -> ControllerState.BUTTON_DPAD_RIGHT or ControllerState.BUTTON_DPAD_DOWN
|
|
||||||
null -> 0U
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buttonStateChanged(buttonMask: UInt) = { pressed: Boolean ->
|
|
||||||
ownControllerState = ownControllerState.copy().apply {
|
|
||||||
buttons =
|
|
||||||
if(pressed)
|
|
||||||
buttons or buttonMask
|
|
||||||
else
|
|
||||||
buttons and buttonMask.inv()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.touchcontrols
|
|
||||||
|
|
||||||
import android.view.MotionEvent
|
|
||||||
|
|
||||||
class TouchTracker
|
|
||||||
{
|
|
||||||
var currentPosition: Vector? = null
|
|
||||||
private set(value)
|
|
||||||
{
|
|
||||||
field = value
|
|
||||||
positionChangedCallback?.let { it(field) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var positionChangedCallback: ((Vector?) -> Unit)? = null
|
|
||||||
|
|
||||||
private var pointerId: Int? = null
|
|
||||||
|
|
||||||
fun touchEvent(event: MotionEvent)
|
|
||||||
{
|
|
||||||
when(event.actionMasked)
|
|
||||||
{
|
|
||||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN ->
|
|
||||||
{
|
|
||||||
if(pointerId == null)
|
|
||||||
{
|
|
||||||
pointerId = event.getPointerId(event.actionIndex)
|
|
||||||
currentPosition = Vector(event.getX(event.actionIndex), event.getY(event.actionIndex))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP ->
|
|
||||||
{
|
|
||||||
if(event.getPointerId(event.actionIndex) == pointerId)
|
|
||||||
{
|
|
||||||
pointerId = null
|
|
||||||
currentPosition = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_MOVE ->
|
|
||||||
{
|
|
||||||
val pointerId = pointerId
|
|
||||||
if(pointerId != null)
|
|
||||||
{
|
|
||||||
val pointerIndex = event.findPointerIndex(pointerId)
|
|
||||||
if(pointerIndex >= 0)
|
|
||||||
currentPosition = Vector(event.getX(pointerIndex), event.getY(pointerIndex))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.touchcontrols
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.metallic.chiaki.databinding.FragmentTouchpadOnlyBinding
|
|
||||||
import io.reactivex.rxkotlin.Observables.combineLatest
|
|
||||||
|
|
||||||
class TouchpadOnlyFragment : TouchControlsFragment()
|
|
||||||
{
|
|
||||||
var touchpadOnlyEnabled: LiveData<Boolean>? = null
|
|
||||||
|
|
||||||
private var _binding: FragmentTouchpadOnlyBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
|
||||||
FragmentTouchpadOnlyBinding.inflate(inflater, container, false).let {
|
|
||||||
_binding = it
|
|
||||||
controllerStateProxy.onNext(
|
|
||||||
combineLatest(ownControllerStateSubject, binding.touchpadView.controllerState) { a, b -> a or b }
|
|
||||||
)
|
|
||||||
it.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
|
|
||||||
{
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
touchpadOnlyEnabled?.observe(viewLifecycleOwner, Observer {
|
|
||||||
view.visibility = if(it) View.VISIBLE else View.GONE
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buttonStateChanged(buttonMask: UInt) = { pressed: Boolean ->
|
|
||||||
ownControllerState = ownControllerState.copy().apply {
|
|
||||||
buttons =
|
|
||||||
if(pressed)
|
|
||||||
buttons or buttonMask
|
|
||||||
else
|
|
||||||
buttons and buttonMask.inv()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,168 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.touchcontrols
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import com.metallic.chiaki.R
|
|
||||||
import com.metallic.chiaki.lib.ControllerState
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.subjects.BehaviorSubject
|
|
||||||
import io.reactivex.subjects.Subject
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class TouchpadView @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
|
||||||
) : View(context, attrs, defStyleAttr)
|
|
||||||
{
|
|
||||||
companion object
|
|
||||||
{
|
|
||||||
private const val BUTTON_PRESS_MAX_MOVE_DIST_DP = 32.0f
|
|
||||||
private const val SHORT_BUTTON_PRESS_DURATION_MS = 200L
|
|
||||||
private const val BUTTON_HOLD_DELAY_MS = 500L
|
|
||||||
}
|
|
||||||
|
|
||||||
private val haptics = ButtonHaptics(context)
|
|
||||||
|
|
||||||
private val drawableIdle: Drawable?
|
|
||||||
private val drawablePressed: Drawable?
|
|
||||||
|
|
||||||
private val state: ControllerState = ControllerState()
|
|
||||||
|
|
||||||
inner class Touch(
|
|
||||||
val stateId: UByte,
|
|
||||||
private val startX: Float,
|
|
||||||
private val startY: Float)
|
|
||||||
{
|
|
||||||
var lifted = false // will be true but touch still in list when only relevant for short touch
|
|
||||||
private var maxDist: Float = 0.0f
|
|
||||||
val moveInsignificant: Boolean get() = maxDist < BUTTON_PRESS_MAX_MOVE_DIST_DP
|
|
||||||
|
|
||||||
fun onMove(x: Float, y: Float)
|
|
||||||
{
|
|
||||||
val d = (Vector(x, y) - Vector(startX, startY)).length / resources.displayMetrics.density
|
|
||||||
maxDist = max(d, maxDist)
|
|
||||||
}
|
|
||||||
|
|
||||||
val startButtonHoldRunnable = Runnable {
|
|
||||||
if(!moveInsignificant || buttonHeld)
|
|
||||||
return@Runnable
|
|
||||||
haptics.trigger(true)
|
|
||||||
state.buttons = state.buttons or ControllerState.BUTTON_TOUCHPAD
|
|
||||||
buttonHeld = true
|
|
||||||
triggerStateChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val pointerTouches = mutableMapOf<Int, Touch>()
|
|
||||||
|
|
||||||
private val stateSubject: Subject<ControllerState>
|
|
||||||
= BehaviorSubject.create<ControllerState>().also { it.onNext(state) }
|
|
||||||
val controllerState: Observable<ControllerState> get() = stateSubject
|
|
||||||
|
|
||||||
private var shortPressingTouches = listOf<Touch>()
|
|
||||||
private val shortButtonPressLiftRunnable = Runnable {
|
|
||||||
state.buttons = state.buttons and ControllerState.BUTTON_TOUCHPAD.inv()
|
|
||||||
shortPressingTouches.forEach {
|
|
||||||
state.stopTouch(it.stateId)
|
|
||||||
}
|
|
||||||
shortPressingTouches = listOf()
|
|
||||||
triggerStateChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var buttonHeld = false
|
|
||||||
|
|
||||||
init
|
|
||||||
{
|
|
||||||
context.theme.obtainStyledAttributes(attrs, R.styleable.TouchpadView, 0, 0).apply {
|
|
||||||
drawableIdle = getDrawable(R.styleable.TouchpadView_drawableIdle)
|
|
||||||
drawablePressed = getDrawable(R.styleable.TouchpadView_drawablePressed)
|
|
||||||
recycle()
|
|
||||||
}
|
|
||||||
isClickable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas)
|
|
||||||
{
|
|
||||||
super.onDraw(canvas)
|
|
||||||
if(pointerTouches.values.find { !it.lifted } == null)
|
|
||||||
return
|
|
||||||
val drawable = if(state.buttons and ControllerState.BUTTON_TOUCHPAD != 0U) drawablePressed else drawableIdle
|
|
||||||
drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom)
|
|
||||||
drawable?.draw(canvas)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun touchX(event: MotionEvent, index: Int): UShort =
|
|
||||||
maxOf(0U.toUShort(), minOf((ControllerState.TOUCHPAD_WIDTH - 1u).toUShort(),
|
|
||||||
(ControllerState.TOUCHPAD_WIDTH.toFloat() * event.getX(index) / width.toFloat()).toUInt().toUShort()))
|
|
||||||
|
|
||||||
private fun touchY(event: MotionEvent, index: Int): UShort =
|
|
||||||
maxOf(0U.toUShort(), minOf((ControllerState.TOUCHPAD_HEIGHT - 1u).toUShort(),
|
|
||||||
(ControllerState.TOUCHPAD_HEIGHT.toFloat() * event.getY(index) / height.toFloat()).toUInt().toUShort()))
|
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean
|
|
||||||
{
|
|
||||||
when(event.actionMasked)
|
|
||||||
{
|
|
||||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
|
|
||||||
state.startTouch(touchX(event, event.actionIndex), touchY(event, event.actionIndex))?.let {
|
|
||||||
haptics.trigger()
|
|
||||||
val touch = Touch(it, event.getX(event.actionIndex), event.getY(event.actionIndex))
|
|
||||||
pointerTouches[event.getPointerId(event.actionIndex)] = touch
|
|
||||||
if(!buttonHeld)
|
|
||||||
postDelayed(touch.startButtonHoldRunnable, BUTTON_HOLD_DELAY_MS)
|
|
||||||
triggerStateChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
|
|
||||||
pointerTouches.remove(event.getPointerId(event.actionIndex))?.let {
|
|
||||||
removeCallbacks(it.startButtonHoldRunnable)
|
|
||||||
when
|
|
||||||
{
|
|
||||||
buttonHeld ->
|
|
||||||
{
|
|
||||||
buttonHeld = false
|
|
||||||
state.buttons = state.buttons and ControllerState.BUTTON_TOUCHPAD.inv()
|
|
||||||
state.stopTouch(it.stateId)
|
|
||||||
}
|
|
||||||
it.moveInsignificant -> triggerShortButtonPress(it)
|
|
||||||
else -> state.stopTouch(it.stateId)
|
|
||||||
}
|
|
||||||
triggerStateChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
|
||||||
val changed = pointerTouches.entries.fold(false) { acc, it ->
|
|
||||||
val index = event.findPointerIndex(it.key)
|
|
||||||
if(index < 0)
|
|
||||||
acc
|
|
||||||
else
|
|
||||||
{
|
|
||||||
it.value.onMove(event.getX(event.actionIndex), event.getY(event.actionIndex))
|
|
||||||
acc || state.setTouchPos(it.value.stateId, touchX(event, index), touchY(event, index))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(changed)
|
|
||||||
triggerStateChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun triggerShortButtonPress(touch: Touch)
|
|
||||||
{
|
|
||||||
shortPressingTouches = shortPressingTouches + listOf(touch)
|
|
||||||
removeCallbacks(shortButtonPressLiftRunnable)
|
|
||||||
state.buttons = state.buttons or ControllerState.BUTTON_TOUCHPAD
|
|
||||||
postDelayed(shortButtonPressLiftRunnable, SHORT_BUTTON_PRESS_DURATION_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun triggerStateChanged()
|
|
||||||
{
|
|
||||||
invalidate()
|
|
||||||
stateSubject.onNext(state)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL
|
|
||||||
|
|
||||||
package com.metallic.chiaki.touchcontrols
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import kotlin.math.sqrt
|
|
||||||
|
|
||||||
data class Vector(val x: Float, val y: Float)
|
|
||||||
{
|
|
||||||
operator fun plus(o: Vector) = Vector(x + o.x, y + o.y)
|
|
||||||
operator fun minus(o: Vector) = Vector(x - o.x, y - o.y)
|
|
||||||
operator fun plus(s: Float) = Vector(x + s, y + s)
|
|
||||||
operator fun minus(s: Float) = Vector(x - s, y - s)
|
|
||||||
operator fun times(s: Float) = Vector(x * s, y * s)
|
|
||||||
operator fun div(s: Float) = this * (1f / s)
|
|
||||||
operator fun times(o: Vector) = Vector(x * o.x, y * o.y)
|
|
||||||
operator fun div(o: Vector) = this * Vector(1.0f / o.x, 1.0f / o.y)
|
|
||||||
|
|
||||||
val lengthSq get() = x*x + y*y
|
|
||||||
val length get() = sqrt(lengthSq)
|
|
||||||
val normalized get() = this / length
|
|
||||||
}
|
|
||||||
|
|
||||||
val View.locationOnScreen: Vector get() {
|
|
||||||
val v = intArrayOf(0, 0)
|
|
||||||
this.getLocationOnScreen(v)
|
|
||||||
return Vector(v[0].toFloat(), v[1].toFloat())
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:color="?attr/colorAccent" android:state_focused="true"/>
|
|
||||||
<item android:alpha="0.87" android:color="?attr/colorOnSurface" android:state_hovered="true"/>
|
|
||||||
<item android:alpha="0.12" android:color="?attr/colorOnSurface" android:state_enabled="false"/>
|
|
||||||
<item android:alpha="0.38" android:color="?attr/colorOnSurface"/>
|
|
||||||
</selector>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:color="?attr/colorAccent" android:state_checked="true"/>
|
|
||||||
<item android:color="@android:color/white"/>
|
|
||||||
</selector>
|
|
|
@ -1,53 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:drawable="@drawable/ic_add_close">
|
|
||||||
|
|
||||||
<target
|
|
||||||
android:name="add">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:duration="100"
|
|
||||||
android:propertyName="rotation"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="45"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
|
|
||||||
<target
|
|
||||||
android:name="add_path">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:duration="100"
|
|
||||||
android:propertyName="fillAlpha"
|
|
||||||
android:valueFrom="1"
|
|
||||||
android:valueTo="0"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
|
|
||||||
<target
|
|
||||||
android:name="close">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:startOffset="50"
|
|
||||||
android:duration="100"
|
|
||||||
android:propertyName="rotation"
|
|
||||||
android:valueFrom="-45"
|
|
||||||
android:valueTo="0"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
|
|
||||||
<target
|
|
||||||
android:name="close_path">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:startOffset="50"
|
|
||||||
android:duration="100"
|
|
||||||
android:propertyName="fillAlpha"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="1"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
|
|
||||||
</animated-vector>
|
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:drawable="@drawable/ic_add_close">
|
|
||||||
|
|
||||||
<target
|
|
||||||
android:name="close">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:duration="100"
|
|
||||||
android:propertyName="rotation"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="-45"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
|
|
||||||
<target
|
|
||||||
android:name="close_path">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:duration="100"
|
|
||||||
android:propertyName="fillAlpha"
|
|
||||||
android:valueFrom="1"
|
|
||||||
android:valueTo="0"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
|
|
||||||
<target
|
|
||||||
android:name="add">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:startOffset="50"
|
|
||||||
android:duration="100"
|
|
||||||
android:propertyName="rotation"
|
|
||||||
android:valueFrom="45"
|
|
||||||
android:valueTo="0"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
|
|
||||||
<target
|
|
||||||
android:name="add_path">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:startOffset="50"
|
|
||||||
android:duration="100"
|
|
||||||
android:propertyName="fillAlpha"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="1"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
</animated-vector>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="512dp"
|
|
||||||
android:height="512dp"
|
|
||||||
android:viewportWidth="135.46666"
|
|
||||||
android:viewportHeight="135.46667">
|
|
||||||
<path
|
|
||||||
android:pathData="M67.733,67.733m-67.733,0a67.733,67.733 0,1 1,135.467 0a67.733,67.733 0,1 1,-135.467 0"
|
|
||||||
android:strokeAlpha="1"
|
|
||||||
android:strokeLineJoin="miter"
|
|
||||||
android:strokeWidth="4.56389332"
|
|
||||||
android:fillColor="@color/control_primary"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:fillAlpha="1"
|
|
||||||
android:strokeLineCap="butt"/>
|
|
||||||
</vector>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="512dp"
|
|
||||||
android:height="512dp"
|
|
||||||
android:viewportWidth="135.46666"
|
|
||||||
android:viewportHeight="135.46667">
|
|
||||||
<path
|
|
||||||
android:pathData="M67.733,67.733m-67.733,0a67.733,67.733 0,1 1,135.467 0a67.733,67.733 0,1 1,-135.467 0"
|
|
||||||
android:strokeAlpha="1"
|
|
||||||
android:strokeLineJoin="miter"
|
|
||||||
android:strokeWidth="4.56389332"
|
|
||||||
android:fillColor="@color/control_pressed"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:fillAlpha="1"
|
|
||||||
android:strokeLineCap="butt"/>
|
|
||||||
</vector>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="512dp"
|
|
||||||
android:height="512dp"
|
|
||||||
android:viewportWidth="135.46666"
|
|
||||||
android:viewportHeight="135.46667">
|
|
||||||
<path
|
|
||||||
android:pathData="m67.733,0c-43.925,-1.668 -79.229,46.32 -64.708,87.75 11.396,42.428 67.652,62.049 102.953,35.886 37.18,-23.426 39.288,-82.963 3.859,-108.96 -11.851,-9.454 -26.946,-14.714 -42.104,-14.676zM25.401,25.401c28.222,0 56.444,0 84.666,0 0,28.222 0,56.444 0,84.666 -28.222,0 -56.444,0 -84.666,0 0,-28.222 0,-56.444 0,-84.666zM37.277,37.277c0,20.304 0,40.608 0,60.912 20.304,0 40.608,0 60.912,0 0,-20.304 0,-40.608 0,-60.912 -20.304,0 -40.608,0 -60.912,0z"
|
|
||||||
android:strokeAlpha="1"
|
|
||||||
android:strokeWidth="9.17431831"
|
|
||||||
android:fillColor="@color/control_primary"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:fillAlpha="1"/>
|
|
||||||
</vector>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="512dp"
|
|
||||||
android:height="512dp"
|
|
||||||
android:viewportWidth="135.46666"
|
|
||||||
android:viewportHeight="135.46667">
|
|
||||||
<path
|
|
||||||
android:pathData="m67.733,0c-43.925,-1.668 -79.229,46.32 -64.708,87.75 11.396,42.428 67.652,62.049 102.953,35.886 37.18,-23.426 39.288,-82.963 3.859,-108.96 -11.851,-9.454 -26.946,-14.714 -42.104,-14.676zM25.401,25.401c28.222,0 56.444,0 84.666,0 0,28.222 0,56.444 0,84.666 -28.222,0 -56.444,0 -84.666,0 0,-28.222 0,-56.444 0,-84.666zM37.277,37.277c0,20.304 0,40.608 0,60.912 20.304,0 40.608,0 60.912,0 0,-20.304 0,-40.608 0,-60.912 -20.304,0 -40.608,0 -60.912,0z"
|
|
||||||
android:strokeAlpha="1"
|
|
||||||
android:strokeWidth="9.17431831"
|
|
||||||
android:fillColor="@color/control_pressed"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:fillAlpha="1"/>
|
|
||||||
</vector>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="512dp"
|
|
||||||
android:height="512dp"
|
|
||||||
android:viewportWidth="135.46666"
|
|
||||||
android:viewportHeight="135.46667">
|
|
||||||
<path
|
|
||||||
android:pathData="M67.733,0A67.733,67.733 0,0 0,0 67.733,67.733 67.733,46.804 0,0 67.733,135.467 67.733,67.733 0,0 0,135.467 67.733,67.733 67.733,133.241 0,0 67.733,0ZM33.309,24.328 L67.733,58.753 102.158,24.328 111.138,33.309 76.714,67.733 111.138,102.158 102.158,111.138 67.733,76.714 33.309,111.138 24.328,102.158 58.753,67.733 24.328,33.309Z"
|
|
||||||
android:strokeAlpha="1"
|
|
||||||
android:strokeWidth="34.67459106"
|
|
||||||
android:fillColor="@color/control_primary"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:fillAlpha="1"/>
|
|
||||||
</vector>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="512dp"
|
|
||||||
android:height="512dp"
|
|
||||||
android:viewportWidth="135.46666"
|
|
||||||
android:viewportHeight="135.46667">
|
|
||||||
<path
|
|
||||||
android:pathData="M67.733,0A67.733,67.733 0,0 0,0 67.733,67.733 67.733,46.804 0,0 67.733,135.467 67.733,67.733 0,0 0,135.467 67.733,67.733 67.733,133.241 0,0 67.733,0ZM33.309,24.328 L67.733,58.753 102.158,24.328 111.138,33.309 76.714,67.733 111.138,102.158 102.158,111.138 67.733,76.714 33.309,111.138 24.328,102.158 58.753,67.733 24.328,33.309Z"
|
|
||||||
android:strokeAlpha="1"
|
|
||||||
android:strokeWidth="34.67459106"
|
|
||||||
android:fillColor="@color/control_pressed"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:fillAlpha="1"/>
|
|
||||||
</vector>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="512dp"
|
|
||||||
android:height="512dp"
|
|
||||||
android:viewportWidth="135.46666"
|
|
||||||
android:viewportHeight="135.46667">
|
|
||||||
<path
|
|
||||||
android:pathData="M67.733,0A67.733,67.733 0,0 0,0 67.733,67.733 67.733,46.804 0,0 67.733,135.467 67.733,67.733 0,0 0,135.467 67.733,67.733 67.733,133.241 0,0 67.733,0ZM67.733,19.592 L121.224,67.733L105.177,67.733L105.177,110.526L78.431,110.526L78.431,78.431L57.035,78.431L57.035,110.526L30.29,110.526L30.29,67.733L14.243,67.733Z"
|
|
||||||
android:fillColor="@color/control_primary"
|
|
||||||
android:fillAlpha="1"/>
|
|
||||||
</vector>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="512dp"
|
|
||||||
android:height="512dp"
|
|
||||||
android:viewportWidth="135.46666"
|
|
||||||
android:viewportHeight="135.46667">
|
|
||||||
<path
|
|
||||||
android:pathData="M67.733,0A67.733,67.733 0,0 0,0 67.733,67.733 67.733,46.804 0,0 67.733,135.467 67.733,67.733 0,0 0,135.467 67.733,67.733 67.733,133.241 0,0 67.733,0ZM67.733,19.592 L121.224,67.733L105.177,67.733L105.177,110.526L78.431,110.526L78.431,78.431L57.035,78.431L57.035,110.526L30.29,110.526L30.29,67.733L14.243,67.733Z"
|
|
||||||
android:fillColor="@color/control_pressed"
|
|
||||||
android:fillAlpha="1"/>
|
|
||||||
</vector>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="256dp"
|
|
||||||
android:height="256dp"
|
|
||||||
android:viewportWidth="67.73333"
|
|
||||||
android:viewportHeight="67.73334">
|
|
||||||
<path
|
|
||||||
android:pathData="M39.2255,0 L0,39.2255 20.3838,59.6093a27.7366,27.7366 88.4859,0 0,39.2255 0,27.7366 27.7366,88.4859 0,0 0,-39.2255zM25.4129,29.4287l2.7735,0l0,18.1643l9.9818,0l0,2.3342L25.4129,49.9272ZM46.3098,29.4287l2.7735,0l0,18.1643l4.5305,0l0,2.3342l-11.8076,0l0,-2.3342l4.531,0l0,-15.6383l-4.9289,0.9886l0,-2.5259z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="5.54109"
|
|
||||||
android:fillColor="@color/control_primary"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
</vector>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="256dp"
|
|
||||||
android:height="256dp"
|
|
||||||
android:viewportWidth="67.73333"
|
|
||||||
android:viewportHeight="67.73334">
|
|
||||||
<path
|
|
||||||
android:pathData="M39.2255,0 L0,39.2255 20.3838,59.6093a27.7366,27.7366 88.4859,0 0,39.2255 0,27.7366 27.7366,88.4859 0,0 0,-39.2255zM25.4129,29.4287l2.7735,0l0,18.1643l9.9818,0l0,2.3342L25.4129,49.9272ZM46.3098,29.4287l2.7735,0l0,18.1643l4.5305,0l0,2.3342l-11.8076,0l0,-2.3342l4.531,0l0,-15.6383l-4.9289,0.9886l0,-2.5259z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="5.54109"
|
|
||||||
android:fillColor="@color/control_pressed"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
</vector>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="256dp"
|
|
||||||
android:height="256dp"
|
|
||||||
android:viewportWidth="67.73333"
|
|
||||||
android:viewportHeight="67.73334">
|
|
||||||
<path
|
|
||||||
android:pathData="m27.6526,0a27.7366,27.7366 88.4859,0 0,-19.5285 8.1241,27.7366 27.7366,88.4859 0,0 0,39.2255L28.5078,67.7333 67.7333,28.5078 47.3496,8.1241A27.7366,27.7366 88.4859,0 0,27.6526 0ZM34.9017,15.1846c2.1235,0 3.8172,0.5309 5.0803,1.5927 1.2631,1.0618 1.8945,2.4804 1.8945,4.2561 0,0.8421 -0.1602,1.6432 -0.4806,2.403 -0.3112,0.7506 -0.8832,1.6383 -1.7162,2.6634 -0.2288,0.2654 -0.9563,1.034 -2.1828,2.3063 -1.2265,1.2631 -2.9565,3.0342 -5.1899,5.3134l9.6795,0l0,2.3342l-13.0157,0l0,-2.3342c1.0526,-1.0892 2.4851,-2.549 4.2974,-4.3796 1.8215,-1.8398 2.9655,-3.025 3.4323,-3.5559 0.8879,-0.9977 1.5058,-1.84 1.8536,-2.5265 0.357,-0.6956 0.5354,-1.3777 0.5354,-2.0459 0,-1.0892 -0.3846,-1.9769 -1.1534,-2.6634 -0.7597,-0.6865 -1.7526,-1.0299 -2.9791,-1.0299 -0.8695,-0 -1.7898,0.1511 -2.76,0.4532 -0.9611,0.3021 -1.9908,0.7598 -3.0892,1.373L29.1078,16.5437c1.1167,-0.4485 2.1603,-0.7871 3.1306,-1.016 0.9702,-0.2288 1.8579,-0.3431 2.6634,-0.3431zM14.0053,15.5551l2.7735,0l0,18.1643l9.9813,0l0,2.3342L14.0053,36.0536Z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="5.54109"
|
|
||||||
android:fillColor="@color/control_primary"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
</vector>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="256dp"
|
|
||||||
android:height="256dp"
|
|
||||||
android:viewportWidth="67.73333"
|
|
||||||
android:viewportHeight="67.73334">
|
|
||||||
<path
|
|
||||||
android:pathData="m27.6526,0a27.7366,27.7366 88.4859,0 0,-19.5285 8.1241,27.7366 27.7366,88.4859 0,0 0,39.2255L28.5078,67.7333 67.7333,28.5078 47.3496,8.1241A27.7366,27.7366 88.4859,0 0,27.6526 0ZM34.9017,15.1846c2.1235,0 3.8172,0.5309 5.0803,1.5927 1.2631,1.0618 1.8945,2.4804 1.8945,4.2561 0,0.8421 -0.1602,1.6432 -0.4806,2.403 -0.3112,0.7506 -0.8832,1.6383 -1.7162,2.6634 -0.2288,0.2654 -0.9563,1.034 -2.1828,2.3063 -1.2265,1.2631 -2.9565,3.0342 -5.1899,5.3134l9.6795,0l0,2.3342l-13.0157,0l0,-2.3342c1.0526,-1.0892 2.4851,-2.549 4.2974,-4.3796 1.8215,-1.8398 2.9655,-3.025 3.4323,-3.5559 0.8879,-0.9977 1.5058,-1.84 1.8536,-2.5265 0.357,-0.6956 0.5354,-1.3777 0.5354,-2.0459 0,-1.0892 -0.3846,-1.9769 -1.1534,-2.6634 -0.7597,-0.6865 -1.7526,-1.0299 -2.9791,-1.0299 -0.8695,-0 -1.7898,0.1511 -2.76,0.4532 -0.9611,0.3021 -1.9908,0.7598 -3.0892,1.373L29.1078,16.5437c1.1167,-0.4485 2.1603,-0.7871 3.1306,-1.016 0.9702,-0.2288 1.8579,-0.3431 2.6634,-0.3431zM14.0053,15.5551l2.7735,0l0,18.1643l9.9813,0l0,2.3342L14.0053,36.0536Z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="5.54109"
|
|
||||||
android:fillColor="@color/control_pressed"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
</vector>
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue