mirror of
https://github.com/barry-ran/QtScrcpy.git
synced 2025-04-20 11:35:56 +00:00
Compare commits
109 commits
Author | SHA1 | Date | |
---|---|---|---|
|
02ffb264c9 | ||
|
314bd4f613 | ||
|
6f0f9447da | ||
|
cfe79c7d5a | ||
|
dbf25166ea | ||
|
f5cccac0df | ||
|
224f04ffa0 | ||
|
a8d3609c19 | ||
|
790f422f99 | ||
|
c1faff820d | ||
|
5fa18219b6 | ||
|
8196e46648 | ||
|
f863a91f94 | ||
|
ae1523b2a0 | ||
|
96fc6bfdf7 | ||
|
c1cb2dad3b | ||
|
7d1fdf4965 | ||
|
f143c50596 | ||
|
97be13ec82 | ||
|
072d6c7c6c | ||
|
49378f540c | ||
|
ec5f413a38 | ||
|
16d693a618 | ||
|
8e8df04213 | ||
|
0a9871290f | ||
|
723b4efb0f | ||
|
a60c2ee906 | ||
|
43f5747cfe | ||
|
9e613986bf | ||
|
7c0b32de9a | ||
|
d0d6356f37 | ||
|
9fa4b6672c | ||
|
1496b767b7 | ||
|
5354ae0173 | ||
|
076f9ba4c7 | ||
|
8baa406bc6 | ||
|
05bfbe6e5c | ||
|
bc2687df7c | ||
|
ae1851abec | ||
|
971e94b682 | ||
|
e7125d80b7 | ||
|
6147963a1c | ||
|
4f50092fb1 | ||
|
44575ff658 | ||
|
9bfe67455b | ||
|
5b7b54bad9 | ||
|
1aa191764e | ||
|
5537a15a48 | ||
|
00a4882263 | ||
|
1107980188 | ||
|
e07a69737e | ||
|
332e36972b | ||
|
035dc51d46 | ||
|
9a3cfc62f9 | ||
|
4aa8cebcd9 | ||
|
a09a59b52d | ||
|
e701d959ca | ||
|
587c1f5872 | ||
|
f1e24fe81a | ||
|
bdcd9c227f | ||
|
0d0a7edf27 | ||
|
3658ebbef0 | ||
|
1f776d842f | ||
|
4b0e42c285 | ||
|
79a184145e | ||
|
fa943f75a3 | ||
|
8c0b3d4870 | ||
|
ab4fea7b03 | ||
|
5a31a8fa48 | ||
|
bd51fa19a0 | ||
|
2dc9189473 | ||
|
01bbff8fe4 | ||
|
c0ac2de19e | ||
|
f5380bc514 | ||
|
ca55caa0f0 | ||
|
ef2e822c13 | ||
|
b44edf076a | ||
|
c237a17b06 | ||
|
3fd25c367f | ||
|
5a20373c88 | ||
|
45fbbbf813 | ||
|
ab3541a8a8 | ||
|
cb73720d74 | ||
|
5e328a7be7 | ||
|
56b7c03748 | ||
|
1af89bcb04 | ||
|
c7e0727e5d | ||
|
eb3579482d | ||
|
b66b1b6e71 | ||
|
2e2423c6a1 | ||
|
e8d073dfae | ||
|
26b0c896ad | ||
|
90dfcbd075 | ||
|
29e7654c38 | ||
|
21f67a0aa5 | ||
|
ab4175489d | ||
|
d1d2f0454f | ||
|
e253e63b00 | ||
|
86d20e653b | ||
|
0158707fb5 | ||
|
d6896e30df | ||
|
d956c1fa68 | ||
|
44e815f373 | ||
|
6bddc2de95 | ||
|
2a4093a487 | ||
|
d5e915d404 | ||
|
bb43261872 | ||
|
44d2825a0d | ||
|
fc400fabd4 |
96 changed files with 1185 additions and 5694 deletions
43
.github/workflows/macos.yml
vendored
43
.github/workflows/macos.yml
vendored
|
@ -13,12 +13,23 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: macos-10.15
|
||||
# install-qt-action在arm上执行macdeployqt会报parse otool错误,所以在intel mac上执行:
|
||||
# 用qt6时在arm mac上编译arm和intel都没有问题
|
||||
# qt5+intel mac编译intel没问题
|
||||
# qt5+arm mac编译intel会报错
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
|
||||
runs-on: macos-13
|
||||
strategy:
|
||||
matrix:
|
||||
qt-ver: [5.15.1]
|
||||
qt-arch-install: [clang_64]
|
||||
clang-arch: [x64]
|
||||
qt-ver: [5.15.2, 6.5.3]
|
||||
# 配置qt-ver的额外设置qt-arch-install,build-arch
|
||||
include:
|
||||
- qt-ver: 5.15.2
|
||||
qt-arch-install: clang_64
|
||||
build-arch: x64
|
||||
- qt-ver: 6.5.3
|
||||
qt-arch-install: arm64
|
||||
build-arch: arm64
|
||||
env:
|
||||
target-name: QtScrcpy
|
||||
qt-install-path: ${{ github.workspace }}/${{ matrix.qt-ver }}
|
||||
|
@ -26,15 +37,23 @@ jobs:
|
|||
steps:
|
||||
- name: Cache Qt
|
||||
id: cache-qt
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }}
|
||||
key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch-install }}
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v2.13.0
|
||||
- name: Install Qt5
|
||||
if: startsWith(matrix.qt-ver, '5.')
|
||||
uses: jurplel/install-qt-action@v4.1.1
|
||||
with:
|
||||
version: ${{ matrix.qt-ver }}
|
||||
cached: ${{ steps.cache-qt.outputs.cache-hit }}
|
||||
- name: Install Qt6
|
||||
if: startsWith(matrix.qt-ver, '6.')
|
||||
uses: jurplel/install-qt-action@v4.1.1
|
||||
with:
|
||||
version: ${{ matrix.qt-ver }}
|
||||
modules: qtmultimedia
|
||||
cached: ${{ steps.cache-qt.outputs.cache-hit }}
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
@ -46,7 +65,7 @@ jobs:
|
|||
ENV_QT_PATH: ${{ env.qt-install-path }}
|
||||
run: |
|
||||
python ci/generate-version.py
|
||||
ci/mac/build_for_mac.sh RelWithDebInfo
|
||||
ci/mac/build_for_mac.sh RelWithDebInfo ${{ matrix.build-arch }}
|
||||
# 获取ref最后一个/后的内容
|
||||
- name: Get the version
|
||||
shell: bash
|
||||
|
@ -58,17 +77,17 @@ jobs:
|
|||
id: package
|
||||
env:
|
||||
ENV_QT_PATH: ${{ env.qt-install-path }}
|
||||
publish_name: ${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.clang-arch }}-${{ steps.get-version.outputs.version }}
|
||||
publish_name: ${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.build-arch }}-Qt${{matrix.qt-ver}}-${{ steps.get-version.outputs.version }}
|
||||
run: |
|
||||
ci/mac/publish_for_mac.sh ../build
|
||||
ci/mac/publish_for_mac.sh ../build ${{ matrix.build-arch }}
|
||||
ci/mac/package_for_mac.sh
|
||||
mv ci/build/QtScrcpy.app ci/build/${{ env.publish_name }}.app
|
||||
mv ci/build/QtScrcpy.dmg ci/build/${{ env.publish_name }}.dmg
|
||||
echo "::set-output name=package-name::${{ env.publish_name }}"
|
||||
- uses: actions/upload-artifact@v1
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.package.outputs.package-name }}.zip
|
||||
path: ci/build/${{ steps.package.outputs.package-name }}.app
|
||||
path: ci/build/${{ steps.package.outputs.package-name }}.dmg
|
||||
# Upload to release
|
||||
- name: Upload Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
|
54
.github/workflows/ubuntu.yml
vendored
54
.github/workflows/ubuntu.yml
vendored
|
@ -1,5 +1,4 @@
|
|||
name: Ubuntu
|
||||
# Qt官方没有linux平台的x86包
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
|
@ -19,7 +18,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-22.04]
|
||||
qt-ver: [5.15.1]
|
||||
qt-ver: [5.15.2]
|
||||
qt-arch-install: [gcc_64]
|
||||
gcc-arch: [x64]
|
||||
env:
|
||||
|
@ -27,27 +26,56 @@ jobs:
|
|||
qt-install-path: ${{ github.workspace }}/${{ matrix.qt-ver }}
|
||||
plantform-des: ubuntu
|
||||
steps:
|
||||
- name: Cache Qt
|
||||
id: cache-qt
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }}
|
||||
key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch-install }}
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v2.13.0
|
||||
uses: jurplel/install-qt-action@v4.1.1
|
||||
with:
|
||||
version: ${{ matrix.qt-ver }}
|
||||
cached: ${{ steps.cache-qt.outputs.cache-hit }}
|
||||
- name: Ubuntu install GL library
|
||||
- name: Cache Qt
|
||||
id: cache-qt
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }}
|
||||
key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch-install }}
|
||||
- name: Install GL library
|
||||
run: sudo apt-get install -y libglew-dev libglfw3-dev
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'true'
|
||||
ssh-key: ${{ secrets.BOT_SSH_KEY }}
|
||||
- name: Build Ubuntu
|
||||
- name: Build RelWithDebInfo
|
||||
env:
|
||||
ENV_QT_PATH: ${{ env.qt-install-path }}
|
||||
run: |
|
||||
python ci/generate-version.py
|
||||
ci/linux/build_for_ubuntu.sh RelWithDebInfo
|
||||
ci/linux/build_for_linux.sh "RelWithDebInfo"
|
||||
- name: Upload RelWithDebInfo
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}-RelWithDebInfo
|
||||
path: output/x64/RelWithDebInfo/*
|
||||
- name: Build Release
|
||||
env:
|
||||
ENV_QT_PATH: ${{ env.qt-install-path }}
|
||||
run: |
|
||||
ci/linux/build_for_linux.sh "Release"
|
||||
- name: Upload Release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}-Release
|
||||
path: output/x64/Release/*
|
||||
- name: Install the zip utility
|
||||
run: |
|
||||
sudo apt install zip -y
|
||||
- name: Zip the Artifacts
|
||||
run: |
|
||||
zip -r QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}.zip output/x64/Release
|
||||
- name: Upload to Releases
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: svenstaro/upload-release-action@2.3.0
|
||||
with:
|
||||
file: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}.zip
|
||||
asset_name: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}.zip
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
|
8
.github/workflows/windows.yml
vendored
8
.github/workflows/windows.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
# 矩阵配置 https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix
|
||||
strategy:
|
||||
matrix:
|
||||
qt-ver: [5.15.1]
|
||||
qt-ver: [5.15.2]
|
||||
qt-arch: [win64_msvc2019_64, win32_msvc2019]
|
||||
# 配置qt-arch的额外设置msvc-arch,qt-arch-install
|
||||
include:
|
||||
|
@ -47,14 +47,14 @@ jobs:
|
|||
steps:
|
||||
- name: Cache Qt
|
||||
id: cache-qt
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }}
|
||||
key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch }}
|
||||
# 安装Qt
|
||||
- name: Install Qt
|
||||
# 使用外部action。这个action专门用来安装Qt
|
||||
uses: jurplel/install-qt-action@v2.13.0
|
||||
uses: jurplel/install-qt-action@v4.1.1
|
||||
with:
|
||||
# Version of Qt to install
|
||||
version: ${{ matrix.qt-ver }}
|
||||
|
@ -100,7 +100,7 @@ jobs:
|
|||
echo "::set-output name=package-name::${{ env.publish_name }}"
|
||||
# 上传artifacts
|
||||
# https://help.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts
|
||||
- uses: actions/upload-artifact@v1
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.package.outputs.package-name }}.zip
|
||||
path: ci\build\${{ steps.package.outputs.package-name }}
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -13,4 +13,5 @@
|
|||
build-*
|
||||
*.DS_Store
|
||||
userdata.ini
|
||||
Info_Mac.plist
|
||||
Info_Mac.plist
|
||||
/ci/build_temp
|
|
@ -11,7 +11,7 @@ cmake_minimum_required(VERSION 3.19 FATAL_ERROR)
|
|||
# QC Custom config
|
||||
set(QC_PROJECT_NAME "QtScrcpy")
|
||||
# Read version numbers from file
|
||||
file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/version QC_FILE_VERSION)
|
||||
file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/appversion QC_FILE_VERSION)
|
||||
set(QC_PROJECT_VERSION ${QC_FILE_VERSION})
|
||||
|
||||
# Project declare
|
||||
|
@ -26,6 +26,19 @@ if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
|||
else()
|
||||
set(QC_CPU_ARCH x86)
|
||||
endif()
|
||||
|
||||
# MacOS
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
# mac default arch arm64
|
||||
if(NOT CMAKE_OSX_ARCHITECTURES)
|
||||
set(CMAKE_OSX_ARCHITECTURES arm64)
|
||||
endif()
|
||||
|
||||
if (CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
|
||||
set(QC_CPU_ARCH arm64)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
message(STATUS "[${PROJECT_NAME}] CPU_ARCH:${QC_CPU_ARCH}")
|
||||
|
||||
# CMake set
|
||||
|
@ -52,8 +65,9 @@ if (MSVC)
|
|||
add_compile_options(/W3 /WX /wd4566)
|
||||
|
||||
# avoid warning C4819
|
||||
add_compile_options(-source-charset:utf-8)
|
||||
#add_compile_options(/utf-8)
|
||||
#add_compile_options(-source-charset:utf-8)
|
||||
# /utf-8 will set source charset and execution charset to utf-8, so we don't need to set source-charset:utf-8
|
||||
add_compile_options(/utf-8)
|
||||
|
||||
# ensure we use minimal "windows.h" lib without the crazy min max macros
|
||||
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN)
|
||||
|
@ -75,18 +89,49 @@ endif()
|
|||
# Qt
|
||||
#
|
||||
|
||||
# Find Qt version
|
||||
if (NOT QT_DESIRED_VERSION)
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core)
|
||||
message(" >>> Found Qt version: ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}.${QT_VERSION_PATCH}")
|
||||
set(QT_DESIRED_VERSION ${QT_VERSION_MAJOR})
|
||||
endif()
|
||||
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Network Multimedia REQUIRED)
|
||||
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Network Multimedia REQUIRED)
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
find_package(QT NAMES Qt6 Qt5 COMPONENTS X11Extras REQUIRED)
|
||||
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS X11Extras REQUIRED)
|
||||
set(qt_required_components Widgets Network Multimedia)
|
||||
|
||||
if (QT_DESIRED_VERSION MATCHES 6)
|
||||
# list(APPEND qt_required_components Core5Compat)
|
||||
list(APPEND qt_required_components OpenGL)
|
||||
list(APPEND qt_required_components OpenGLWidgets)
|
||||
else()
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
list(APPEND qt_required_components X11Extras )
|
||||
endif()
|
||||
endif()
|
||||
|
||||
message(STATUS "[${PROJECT_NAME}] Qt version is: ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}")
|
||||
find_package(Qt${QT_DESIRED_VERSION} REQUIRED COMPONENTS ${qt_required_components})
|
||||
|
||||
set(LINK_LIBS
|
||||
Qt${QT_DESIRED_VERSION}::Widgets
|
||||
Qt${QT_DESIRED_VERSION}::Network
|
||||
Qt${QT_DESIRED_VERSION}::Multimedia
|
||||
)
|
||||
|
||||
if (QT_DESIRED_VERSION MATCHES 6)
|
||||
# list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::Core5Compat)
|
||||
list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::GuiPrivate)
|
||||
list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::OpenGL)
|
||||
list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::OpenGLWidgets)
|
||||
else()
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::X11Extras)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
message(STATUS "[${PROJECT_NAME}] Qt version is: ${QT_DESIRED_VERSION}")
|
||||
|
||||
#
|
||||
# Sources
|
||||
|
@ -149,6 +194,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
|||
set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES}
|
||||
util/mousetap/winmousetap.h
|
||||
util/mousetap/winmousetap.cpp
|
||||
util/winutils.h
|
||||
util/winutils.cpp
|
||||
)
|
||||
endif()
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
|
@ -161,6 +208,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
|||
set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES}
|
||||
util/mousetap/cocoamousetap.h
|
||||
util/mousetap/cocoamousetap.mm
|
||||
util/path.h
|
||||
util/path.mm
|
||||
)
|
||||
endif()
|
||||
source_group(util FILES ${QC_UTIL_SOURCES})
|
||||
|
@ -259,6 +308,9 @@ endif()
|
|||
|
||||
# MacOS
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
# qt6 need 10.15 or later
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15")
|
||||
|
||||
# copy bundle file
|
||||
get_target_property(MACOS_BUNDLE_PATH ${PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY)
|
||||
set(MACOS_BUNDLE_PATH ${MACOS_BUNDLE_PATH}/${PROJECT_NAME}.app/Contents)
|
||||
|
@ -308,8 +360,6 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
|||
find_package(Threads REQUIRED)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||
# qx11
|
||||
Qt${QT_VERSION_MAJOR}::X11Extras
|
||||
# xcb https://doc.qt.io/qt-5/linux-requirements.html
|
||||
xcb
|
||||
# pthread
|
||||
|
@ -327,8 +377,6 @@ add_subdirectory(QtScrcpyCore)
|
|||
|
||||
# Qt
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||
Qt${QT_VERSION_MAJOR}::Widgets
|
||||
Qt${QT_VERSION_MAJOR}::Network
|
||||
Qt${QT_VERSION_MAJOR}::Multimedia
|
||||
${LINK_LIBS}
|
||||
QtScrcpyCore
|
||||
)
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 3004e63935fe8a3e57b91e117a91c1a6aa68ae42
|
||||
Subproject commit 19e1ba8fb5c59c5a85c3c6a79967fab4c84739c7
|
|
@ -4,11 +4,23 @@
|
|||
#include <QTime>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
#include <QAudioSink>
|
||||
#include <QAudioDevice>
|
||||
#include <QMediaDevices>
|
||||
#endif
|
||||
|
||||
#include "audiooutput.h"
|
||||
|
||||
AudioOutput::AudioOutput(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_running = false;
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
m_audioOutput = nullptr;
|
||||
#else
|
||||
m_audioSink = nullptr;
|
||||
#endif
|
||||
connect(&m_sndcpy, &QProcess::readyReadStandardOutput, this, [this]() {
|
||||
qInfo() << QString("AudioOutput::") << QString(m_sndcpy.readAllStandardOutput());
|
||||
});
|
||||
|
@ -69,15 +81,10 @@ bool AudioOutput::runSndcpyProcess(const QString &serial, int port, bool wait)
|
|||
}
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
QStringList params;
|
||||
params << serial;
|
||||
params << QString("%1").arg(port);
|
||||
QStringList params{serial, QString::number(port)};
|
||||
m_sndcpy.start("sndcpy.bat", params);
|
||||
#else
|
||||
QStringList params;
|
||||
params << "sndcpy.sh";
|
||||
params << serial;
|
||||
params << QString("%1").arg(port);
|
||||
QStringList params{"sndcpy.sh", serial, QString::number(port)};
|
||||
m_sndcpy.start("bash", params);
|
||||
#endif
|
||||
|
||||
|
@ -86,11 +93,11 @@ bool AudioOutput::runSndcpyProcess(const QString &serial, int port, bool wait)
|
|||
}
|
||||
|
||||
if (!m_sndcpy.waitForStarted()) {
|
||||
qWarning() << "AudioOutput::start sndcpy.bat failed";
|
||||
qWarning() << "AudioOutput::start sndcpy process failed";
|
||||
return false;
|
||||
}
|
||||
if (!m_sndcpy.waitForFinished()) {
|
||||
qWarning() << "AudioOutput::sndcpy.bat crashed";
|
||||
qWarning() << "AudioOutput::sndcpy process crashed";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -99,6 +106,7 @@ bool AudioOutput::runSndcpyProcess(const QString &serial, int port, bool wait)
|
|||
|
||||
void AudioOutput::startAudioOutput()
|
||||
{
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
if (m_audioOutput) {
|
||||
return;
|
||||
}
|
||||
|
@ -110,8 +118,8 @@ void AudioOutput::startAudioOutput()
|
|||
format.setCodec("audio/pcm");
|
||||
format.setByteOrder(QAudioFormat::LittleEndian);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
|
||||
QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
|
||||
|
||||
if (!info.isFormatSupported(format)) {
|
||||
qWarning() << "AudioOutput::audio format not supported, cannot play audio.";
|
||||
return;
|
||||
|
@ -123,17 +131,47 @@ void AudioOutput::startAudioOutput()
|
|||
});
|
||||
m_audioOutput->setBufferSize(48000*2*15/1000 * 20);
|
||||
m_outputDevice = m_audioOutput->start();
|
||||
#else
|
||||
if (m_audioSink) {
|
||||
return;
|
||||
}
|
||||
|
||||
QAudioFormat format;
|
||||
format.setSampleRate(48000);
|
||||
format.setChannelCount(2);
|
||||
format.setSampleFormat(QAudioFormat::Int16);
|
||||
QAudioDevice defaultDevice = QMediaDevices::defaultAudioOutput();
|
||||
if (!defaultDevice.isFormatSupported(format)) {
|
||||
qWarning() << "AudioOutput::audio format not supported, cannot play audio.";
|
||||
return;
|
||||
}
|
||||
m_audioSink = new QAudioSink(defaultDevice, format, this);
|
||||
m_outputDevice = m_audioSink->start();
|
||||
if (!m_outputDevice) {
|
||||
qWarning() << "AudioOutput::audio output device not available, cannot play audio.";
|
||||
delete m_audioSink;
|
||||
m_audioSink = nullptr;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void AudioOutput::stopAudioOutput()
|
||||
{
|
||||
if (!m_audioOutput) {
|
||||
return;
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
if (m_audioOutput) {
|
||||
m_audioOutput->stop();
|
||||
delete m_audioOutput;
|
||||
m_audioOutput = nullptr;
|
||||
}
|
||||
|
||||
m_audioOutput->stop();
|
||||
delete m_audioOutput;
|
||||
m_audioOutput = nullptr;
|
||||
#else
|
||||
if (m_audioSink) {
|
||||
m_audioSink->stop();
|
||||
delete m_audioSink;
|
||||
m_audioSink = nullptr;
|
||||
}
|
||||
#endif
|
||||
m_outputDevice = nullptr;
|
||||
}
|
||||
|
||||
void AudioOutput::startRecvData(int port)
|
||||
|
@ -165,7 +203,7 @@ void AudioOutput::startRecvData(int port)
|
|||
m_buffer.reserve(recv);
|
||||
}
|
||||
|
||||
qint64 count = audioSocket->read(m_buffer.data(), audioSocket->bytesAvailable());
|
||||
qint64 count = audioSocket->read(m_buffer.data(), recv);
|
||||
m_outputDevice->write(m_buffer.data(), count);
|
||||
});
|
||||
connect(audioSocket, &QTcpSocket::stateChanged, audioSocket, [](QAbstractSocket::SocketState state) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <QPointer>
|
||||
#include <QVector>
|
||||
|
||||
class QAudioSink;
|
||||
class QAudioOutput;
|
||||
class QIODevice;
|
||||
class AudioOutput : public QObject
|
||||
|
@ -30,12 +31,16 @@ signals:
|
|||
void connectTo(int port);
|
||||
|
||||
private:
|
||||
QAudioOutput* m_audioOutput = nullptr;
|
||||
QPointer<QIODevice> m_outputDevice;
|
||||
QThread m_workerThread;
|
||||
QProcess m_sndcpy;
|
||||
QVector<char> m_buffer;
|
||||
bool m_running = false;
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
QAudioOutput* m_audioOutput = nullptr;
|
||||
#else
|
||||
QAudioSink *m_audioSink = nullptr;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // AUDIOOUTPUT_H
|
||||
|
|
|
@ -262,7 +262,7 @@ void GroupController::postCut()
|
|||
}
|
||||
}
|
||||
|
||||
void GroupController::setScreenPowerMode(bool open)
|
||||
void GroupController::setDisplayPower(bool on)
|
||||
{
|
||||
for (const auto& serial : m_devices) {
|
||||
if (true == isHost(serial)) {
|
||||
|
@ -273,7 +273,7 @@ void GroupController::setScreenPowerMode(bool open)
|
|||
continue;
|
||||
}
|
||||
|
||||
device->setScreenPowerMode(open);
|
||||
device->setDisplayPower(on);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ private:
|
|||
void postVolumeDown() override;
|
||||
void postCopy() override;
|
||||
void postCut() override;
|
||||
void setScreenPowerMode(bool open) override;
|
||||
void setDisplayPower(bool on) override;
|
||||
void expandNotificationPanel() override;
|
||||
void collapsePanel() override;
|
||||
void postBackOrScreenOn(bool down) override;
|
||||
|
|
|
@ -55,10 +55,12 @@ int main(int argc, char *argv[])
|
|||
QApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
|
||||
}
|
||||
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5,14,0))
|
||||
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
QSurfaceFormat varFormat = QSurfaceFormat::defaultFormat();
|
||||
|
@ -113,6 +115,14 @@ int main(int argc, char *argv[])
|
|||
"following address:");
|
||||
qInfo() << QString("QtScrcpy %1 <https://github.com/barry-ran/QtScrcpy>").arg(QCoreApplication::applicationVersion());
|
||||
|
||||
qInfo() << QObject::tr("If you need more professional batch control mirror software, you can try the following software:");
|
||||
qInfo() << QString(QObject::tr("QuickMirror") + " <https://lrbnfell4p.feishu.cn/drive/folder/KviYfz5uFlpUT8dXgdjccmfUnse>");
|
||||
|
||||
qInfo() << QObject::tr("If you need more professional game keymap mirror software, you can try the following software:");
|
||||
qInfo() << QString(QObject::tr("QuickAssistant") + " <https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh>");
|
||||
|
||||
qInfo() << QObject::tr("You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1>");
|
||||
|
||||
int ret = a.exec();
|
||||
delete g_mainDlg;
|
||||
|
||||
|
@ -127,7 +137,13 @@ void installTranslator()
|
|||
static QTranslator translator;
|
||||
QLocale locale;
|
||||
QLocale::Language language = locale.language();
|
||||
//language = QLocale::English;
|
||||
|
||||
if (Config::getInstance().getLanguage() == "zh_CN") {
|
||||
language = QLocale::Chinese;
|
||||
} else if (Config::getInstance().getLanguage() == "en_US") {
|
||||
language = QLocale::English;
|
||||
}
|
||||
|
||||
QString languagePath = ":/i18n/";
|
||||
switch (language) {
|
||||
case QLocale::Chinese:
|
||||
|
@ -139,7 +155,10 @@ void installTranslator()
|
|||
break;
|
||||
}
|
||||
|
||||
translator.load(languagePath);
|
||||
auto loaded = translator.load(languagePath);
|
||||
if (!loaded) {
|
||||
qWarning() << "Failed to load translation file:" << languagePath;
|
||||
}
|
||||
qApp->installTranslator(&translator);
|
||||
}
|
||||
|
||||
|
|
|
@ -157,6 +157,8 @@ void QYUVOpenGLWidget::initializeGL()
|
|||
|
||||
void QYUVOpenGLWidget::paintGL()
|
||||
{
|
||||
m_shaderProgram.bind();
|
||||
|
||||
if (m_needUpdate) {
|
||||
deInitTextures();
|
||||
initTextures();
|
||||
|
@ -175,6 +177,8 @@ void QYUVOpenGLWidget::paintGL()
|
|||
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
m_shaderProgram.release();
|
||||
}
|
||||
|
||||
void QYUVOpenGLWidget::resizeGL(int width, int height)
|
||||
|
|
Binary file not shown.
|
@ -31,6 +31,10 @@
|
|||
<source>select path</source>
|
||||
<translation>select path</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Clear History</source>
|
||||
<translation>Clear History</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QObject</name>
|
||||
|
@ -38,6 +42,26 @@
|
|||
<source>This software is completely open source and free. Use it at your own risk. You can download it at the following address:</source>
|
||||
<translation>This software is completely open source and free. Use it at your own risk. You can download it at the following address:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>QuickMirror</source>
|
||||
<translation>QuickMirror</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If you need more professional batch control mirror software, you can try the following software:</source>
|
||||
<translation>If you need more professional batch control mirror software, you can try the following software:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If you need more professional game keymap mirror software, you can try the following software:</source>
|
||||
<translation>If you need more professional game keymap mirror software, you can try the following software:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>QuickAssistant</source>
|
||||
<translation>QuickAssistant</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1></source>
|
||||
<translation>You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ToolForm</name>
|
||||
|
@ -127,10 +151,6 @@
|
|||
<source>Start Config</source>
|
||||
<translation>Start Config</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>record save path:</source>
|
||||
<translation>record save path:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>select path</source>
|
||||
<translation>select path</translation>
|
||||
|
@ -289,5 +309,13 @@
|
|||
<source>auto update</source>
|
||||
<translation>auto update</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>show toolbar</source>
|
||||
<translation>show toolbar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>record save path:</source>
|
||||
<translation>record save path:</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
|
Binary file not shown.
|
@ -31,6 +31,10 @@
|
|||
<source>select path</source>
|
||||
<translation>选择路径</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Clear History</source>
|
||||
<translation>清理历史</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QObject</name>
|
||||
|
@ -38,6 +42,26 @@
|
|||
<source>This software is completely open source and free. Use it at your own risk. You can download it at the following address:</source>
|
||||
<translation>本软件完全开源免费,作者不对使用该软件产生的一切后果负责。你可以在以下地址下载:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>QuickMirror</source>
|
||||
<translation>极限投屏</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If you need more professional batch control mirror software, you can try the following software:</source>
|
||||
<translation>如果你需要更专业的批量控制投屏软件,你可以尝试下面软件:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If you need more professional game keymap mirror software, you can try the following software:</source>
|
||||
<translation>如果你需要更专业的游戏映射投屏软件,你可以尝试下面软件:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>QuickAssistant</source>
|
||||
<translation>极限手游助手</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1></source>
|
||||
<translation>你可以通过QQ群联系我 <901736468></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ToolForm</name>
|
||||
|
@ -127,10 +151,6 @@
|
|||
<source>Start Config</source>
|
||||
<translation>启动配置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>record save path:</source>
|
||||
<translation>录像保存路径:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>select path</source>
|
||||
<translation>选择路径</translation>
|
||||
|
@ -289,5 +309,13 @@
|
|||
<source>auto update</source>
|
||||
<translation>自动刷新</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>show toolbar</source>
|
||||
<translation>显示工具栏</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>record save path:</source>
|
||||
<translation>录像保存路径</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QKeyEvent>
|
||||
#include <QRandomGenerator>
|
||||
#include <QTime>
|
||||
#include <QTimer>
|
||||
|
||||
|
@ -11,6 +12,10 @@
|
|||
#include "videoform.h"
|
||||
#include "../groupcontroller/groupcontroller.h"
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#include "../util/winutils.h"
|
||||
#endif
|
||||
|
||||
QString s_keyMapPath = "";
|
||||
|
||||
const QString &getKeyMapPath()
|
||||
|
@ -77,21 +82,21 @@ Dialog::Dialog(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
|
|||
log = "ip not find, connect to wifi?";
|
||||
break;
|
||||
}
|
||||
ui->deviceIpEdt->setText(ip);
|
||||
ui->deviceIpEdt->setEditText(ip);
|
||||
} else if (args.contains("ifconfig") && args.contains("wlan0")) {
|
||||
QString ip = m_adb.getDeviceIPFromStdOut();
|
||||
if (ip.isEmpty()) {
|
||||
log = "ip not find, connect to wifi?";
|
||||
break;
|
||||
}
|
||||
ui->deviceIpEdt->setText(ip);
|
||||
ui->deviceIpEdt->setEditText(ip);
|
||||
} else if (args.contains("ip -o a")) {
|
||||
QString ip = m_adb.getDeviceIPByIpFromStdOut();
|
||||
if (ip.isEmpty()) {
|
||||
log = "ip not find, connect to wifi?";
|
||||
break;
|
||||
}
|
||||
ui->deviceIpEdt->setText(ip);
|
||||
ui->deviceIpEdt->setEditText(ip);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -137,6 +142,10 @@ void Dialog::initUI()
|
|||
|
||||
setWindowTitle(Config::getInstance().getTitle());
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
WinUtils::setDarkBorderToWindow((HWND)this->winId(), true);
|
||||
#endif
|
||||
|
||||
ui->bitRateEdit->setValidator(new QIntValidator(1, 99999, this));
|
||||
|
||||
ui->maxSizeBox->addItem("640");
|
||||
|
@ -155,6 +164,16 @@ void Dialog::initUI()
|
|||
ui->lockOrientationBox->addItem("180");
|
||||
ui->lockOrientationBox->addItem("270");
|
||||
ui->lockOrientationBox->setCurrentIndex(0);
|
||||
|
||||
// 加载IP历史记录
|
||||
loadIpHistory();
|
||||
|
||||
// 为deviceIpEdt添加右键菜单
|
||||
if (ui->deviceIpEdt->lineEdit()) {
|
||||
ui->deviceIpEdt->lineEdit()->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(ui->deviceIpEdt->lineEdit(), &QWidget::customContextMenuRequested,
|
||||
this, &Dialog::showIpEditMenu);
|
||||
}
|
||||
}
|
||||
|
||||
void Dialog::updateBootConfig(bool toView)
|
||||
|
@ -186,6 +205,7 @@ void Dialog::updateBootConfig(bool toView)
|
|||
ui->stayAwakeCheck->setChecked(config.keepAlive);
|
||||
ui->useSingleModeCheck->setChecked(config.simpleMode);
|
||||
ui->autoUpdatecheckBox->setChecked(config.autoUpdateDevice);
|
||||
ui->showToolbar->setChecked(config.showToolbar);
|
||||
} else {
|
||||
UserBootConfig config;
|
||||
|
||||
|
@ -204,6 +224,14 @@ void Dialog::updateBootConfig(bool toView)
|
|||
config.keepAlive = ui->stayAwakeCheck->isChecked();
|
||||
config.simpleMode = ui->useSingleModeCheck->isChecked();
|
||||
config.autoUpdateDevice = ui->autoUpdatecheckBox->isChecked();
|
||||
config.showToolbar = ui->showToolbar->isChecked();
|
||||
|
||||
// 保存当前IP到历史记录
|
||||
QString currentIp = ui->deviceIpEdt->currentText().trimmed();
|
||||
if (!currentIp.isEmpty()) {
|
||||
saveIpHistory(currentIp);
|
||||
}
|
||||
|
||||
Config::getInstance().setUserBootConfig(config);
|
||||
}
|
||||
}
|
||||
|
@ -264,10 +292,13 @@ void Dialog::slotActivated(QSystemTrayIcon::ActivationReason reason)
|
|||
void Dialog::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
this->hide();
|
||||
m_hideIcon->showMessage(tr("Notice"),
|
||||
tr("Hidden here!"),
|
||||
QSystemTrayIcon::Information,
|
||||
3000);
|
||||
if (!Config::getInstance().getTrayMessageShown()) {
|
||||
Config::getInstance().setTrayMessageShown(true);
|
||||
m_hideIcon->showMessage(tr("Notice"),
|
||||
tr("Hidden here!"),
|
||||
QSystemTrayIcon::Information,
|
||||
3000);
|
||||
}
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
|
@ -284,7 +315,7 @@ void Dialog::on_startServerBtn_clicked()
|
|||
{
|
||||
outLog("start server...", false);
|
||||
|
||||
// this is ok that "native" toUshort is 0
|
||||
// this is ok that "original" toUshort is 0
|
||||
quint16 videoSize = ui->maxSizeBox->currentText().trimmed().toUShort();
|
||||
qsc::DeviceParams params;
|
||||
params.serial = ui->serialBox->currentText().trimmed();
|
||||
|
@ -296,8 +327,12 @@ void Dialog::on_startServerBtn_clicked()
|
|||
params.useReverse = ui->useReverseCheck->isChecked();
|
||||
params.display = !ui->notDisplayCheck->isChecked();
|
||||
params.renderExpiredFrames = Config::getInstance().getRenderExpiredFrames();
|
||||
params.lockVideoOrientation = ui->lockOrientationBox->currentIndex() - 1;
|
||||
if (ui->lockOrientationBox->currentIndex() > 0) {
|
||||
params.captureOrientationLock = 1;
|
||||
params.captureOrientation = (ui->lockOrientationBox->currentIndex() - 1) * 90;
|
||||
}
|
||||
params.stayAwake = ui->stayAwakeCheck->isChecked();
|
||||
params.recordFile = ui->recordScreenCheck->isChecked();
|
||||
params.recordPath = ui->recordPathEdt->text().trimmed();
|
||||
params.recordFileFormat = ui->formatBox->currentText().trimmed();
|
||||
params.serverLocalPath = getServerPath();
|
||||
|
@ -308,6 +343,7 @@ void Dialog::on_startServerBtn_clicked()
|
|||
params.logLevel = Config::getInstance().getLogLevel();
|
||||
params.codecOptions = Config::getInstance().getCodecOptions();
|
||||
params.codecName = Config::getInstance().getCodecName();
|
||||
params.scid = QRandomGenerator::global()->bounded(1, 10000) & 0x7FFFFFFF;
|
||||
|
||||
qsc::IDeviceManage::getInstance().connectDevice(params);
|
||||
}
|
||||
|
@ -324,7 +360,7 @@ void Dialog::on_wirelessConnectBtn_clicked()
|
|||
if (checkAdbRun()) {
|
||||
return;
|
||||
}
|
||||
QString addr = ui->deviceIpEdt->text().trimmed();
|
||||
QString addr = ui->deviceIpEdt->currentText().trimmed();
|
||||
if (!ui->devicePortEdt->text().isEmpty()) {
|
||||
addr += ":";
|
||||
addr += ui->devicePortEdt->text().trimmed();
|
||||
|
@ -336,6 +372,12 @@ void Dialog::on_wirelessConnectBtn_clicked()
|
|||
return;
|
||||
}
|
||||
|
||||
// 保存IP历史记录 - 只保存IP部分,不包含端口
|
||||
QString ip = addr.split(":").first();
|
||||
if (!ip.isEmpty()) {
|
||||
saveIpHistory(ip);
|
||||
}
|
||||
|
||||
outLog("wireless connect...", false);
|
||||
QStringList adbArgs;
|
||||
adbArgs << "connect";
|
||||
|
@ -433,20 +475,23 @@ void Dialog::onDeviceConnected(bool success, const QString &serial, const QStrin
|
|||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto videoForm = new VideoForm(ui->framelessCheck->isChecked(), Config::getInstance().getSkin());
|
||||
auto videoForm = new VideoForm(ui->framelessCheck->isChecked(), Config::getInstance().getSkin(), ui->showToolbar->isChecked());
|
||||
videoForm->setSerial(serial);
|
||||
|
||||
qsc::IDeviceManage::getInstance().getDevice(serial)->setUserData(static_cast<void*>(videoForm));
|
||||
qsc::IDeviceManage::getInstance().getDevice(serial)->registerDeviceObserver(videoForm);
|
||||
|
||||
|
||||
videoForm->showFPS(ui->fpsCheck->isChecked());
|
||||
|
||||
if (ui->alwaysTopCheck->isChecked()) {
|
||||
videoForm->staysOnTop();
|
||||
}
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
// must be show before updateShowSize
|
||||
videoForm->show();
|
||||
#endif
|
||||
QString name = Config::getInstance().getNickName(serial);
|
||||
if (name.isEmpty()) {
|
||||
name = Config::getInstance().getTitle();
|
||||
|
@ -464,6 +509,11 @@ void Dialog::onDeviceConnected(bool success, const QString &serial, const QStrin
|
|||
videoForm->setGeometry(rc);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// windows是show太早可以看到resize的过程
|
||||
QTimer::singleShot(200, videoForm, [videoForm](){videoForm->show();});
|
||||
#endif
|
||||
|
||||
GroupController::instance().addDevice(serial);
|
||||
}
|
||||
|
||||
|
@ -488,7 +538,7 @@ void Dialog::on_wirelessDisConnectBtn_clicked()
|
|||
if (checkAdbRun()) {
|
||||
return;
|
||||
}
|
||||
QString addr = ui->deviceIpEdt->text().trimmed();
|
||||
QString addr = ui->deviceIpEdt->currentText().trimmed();
|
||||
outLog("wireless disconnect...", false);
|
||||
QStringList adbArgs;
|
||||
adbArgs << "disconnect";
|
||||
|
@ -590,9 +640,18 @@ void Dialog::on_usbConnectBtn_clicked()
|
|||
|
||||
int Dialog::findDeviceFromeSerialBox(bool wifi)
|
||||
{
|
||||
QRegExp regIP("\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\:([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])\\b");
|
||||
QString regStr = "\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\:([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])\\b";
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
QRegExp regIP(regStr);
|
||||
#else
|
||||
QRegularExpression regIP(regStr);
|
||||
#endif
|
||||
for (int i = 0; i < ui->serialBox->count(); ++i) {
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
bool isWifi = regIP.exactMatch(ui->serialBox->itemText(i));
|
||||
#else
|
||||
bool isWifi = regIP.match(ui->serialBox->itemText(i)).hasMatch();
|
||||
#endif
|
||||
bool found = wifi ? isWifi : !isWifi;
|
||||
if (found) {
|
||||
return i;
|
||||
|
@ -730,3 +789,45 @@ void Dialog::on_autoUpdatecheckBox_toggled(bool checked)
|
|||
m_autoUpdatetimer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void Dialog::loadIpHistory()
|
||||
{
|
||||
QStringList ipList = Config::getInstance().getIpHistory();
|
||||
ui->deviceIpEdt->clear();
|
||||
ui->deviceIpEdt->addItems(ipList);
|
||||
ui->deviceIpEdt->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
if (ui->deviceIpEdt->lineEdit()) {
|
||||
ui->deviceIpEdt->lineEdit()->setMaxLength(128);
|
||||
ui->deviceIpEdt->lineEdit()->setPlaceholderText("192.168.0.1");
|
||||
}
|
||||
}
|
||||
|
||||
void Dialog::saveIpHistory(const QString &ip)
|
||||
{
|
||||
if (ip.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Config::getInstance().saveIpHistory(ip);
|
||||
|
||||
// 更新ComboBox
|
||||
loadIpHistory();
|
||||
ui->deviceIpEdt->setCurrentText(ip);
|
||||
}
|
||||
|
||||
void Dialog::showIpEditMenu(const QPoint &pos)
|
||||
{
|
||||
QMenu *menu = ui->deviceIpEdt->lineEdit()->createStandardContextMenu();
|
||||
menu->addSeparator();
|
||||
|
||||
QAction *clearHistoryAction = new QAction(tr("Clear History"), menu);
|
||||
connect(clearHistoryAction, &QAction::triggered, this, [this]() {
|
||||
Config::getInstance().clearIpHistory();
|
||||
loadIpHistory();
|
||||
});
|
||||
|
||||
menu->addAction(clearHistoryAction);
|
||||
menu->exec(ui->deviceIpEdt->lineEdit()->mapToGlobal(pos));
|
||||
delete menu;
|
||||
}
|
||||
|
|
|
@ -67,6 +67,8 @@ private slots:
|
|||
|
||||
void on_autoUpdatecheckBox_toggled(bool checked);
|
||||
|
||||
void showIpEditMenu(const QPoint &pos);
|
||||
|
||||
private:
|
||||
bool checkAdbRun();
|
||||
void initUI();
|
||||
|
@ -78,6 +80,8 @@ private:
|
|||
int findDeviceFromeSerialBox(bool wifi);
|
||||
quint32 getBitRate();
|
||||
const QString &getServerPath();
|
||||
void loadIpHistory();
|
||||
void saveIpHistory(const QString &ip);
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1293</width>
|
||||
<height>454</height>
|
||||
<height>502</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -255,7 +255,7 @@
|
|||
</sizepolicy>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::ClickFocus</enum>
|
||||
<enum>Qt::NoFocus</enum>
|
||||
</property>
|
||||
<property name="documentTitle">
|
||||
<string/>
|
||||
|
@ -626,6 +626,80 @@
|
|||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="4">
|
||||
<widget class="QCheckBox" name="fpsCheck">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>show fps</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="notDisplayCheck">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>background record</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="alwaysTopCheck">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>always on top</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="recordScreenCheck">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>record screen</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QCheckBox" name="useReverseCheck">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>reverse connection</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="closeScreenCheck">
|
||||
<property name="sizePolicy">
|
||||
|
@ -652,80 +726,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="alwaysTopCheck">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>always on top</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="notDisplayCheck">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>background record</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QCheckBox" name="useReverseCheck">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>reverse connection</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="recordScreenCheck">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>record screen</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QCheckBox" name="fpsCheck">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>show fps</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="4">
|
||||
<widget class="QCheckBox" name="stayAwakeCheck">
|
||||
<property name="sizePolicy">
|
||||
|
@ -739,6 +739,13 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="showToolbar">
|
||||
<property name="text">
|
||||
<string>show toolbar</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -1052,22 +1059,25 @@
|
|||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="deviceIpEdt">
|
||||
<widget class="QComboBox" name="deviceIpEdt">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="currentText">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="maxLength">
|
||||
<number>128</number>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string notr="true">192.168.0.1</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -1140,7 +1150,7 @@
|
|||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
|
@ -1148,10 +1158,13 @@
|
|||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Expanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
|
|
|
@ -68,7 +68,11 @@ void ToolForm::updateGroupControl()
|
|||
void ToolForm::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
m_dragPosition = event->globalPos() - frameGeometry().topLeft();
|
||||
#else
|
||||
m_dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft();
|
||||
#endif
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +85,11 @@ void ToolForm::mouseReleaseEvent(QMouseEvent *event)
|
|||
void ToolForm::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->buttons() & Qt::LeftButton) {
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
move(event->globalPos() - m_dragPosition);
|
||||
#else
|
||||
move(event->globalPosition().toPoint() - m_dragPosition);
|
||||
#endif
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
@ -186,7 +194,7 @@ void ToolForm::on_closeScreenBtn_clicked()
|
|||
if (!device) {
|
||||
return;
|
||||
}
|
||||
device->setScreenPowerMode(false);
|
||||
device->setDisplayPower(false);
|
||||
}
|
||||
|
||||
void ToolForm::on_expandNotifyBtn_clicked()
|
||||
|
@ -221,5 +229,5 @@ void ToolForm::on_openScreenBtn_clicked()
|
|||
if (!device) {
|
||||
return;
|
||||
}
|
||||
device->setScreenPowerMode(true);
|
||||
device->setDisplayPower(true);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#include <QDesktopWidget>
|
||||
// #include <QDesktopWidget>
|
||||
#include <QFileInfo>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
|
@ -13,6 +13,10 @@
|
|||
#include <QWindow>
|
||||
#include <QtWidgets/QHBoxLayout>
|
||||
|
||||
#if defined(Q_OS_WIN32)
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include "config.h"
|
||||
#include "iconhelper.h"
|
||||
#include "qyuvopenglwidget.h"
|
||||
|
@ -21,13 +25,14 @@
|
|||
#include "ui_videoform.h"
|
||||
#include "videoform.h"
|
||||
|
||||
VideoForm::VideoForm(bool framelessWindow, bool skin, QWidget *parent) : QWidget(parent), ui(new Ui::videoForm), m_skin(skin)
|
||||
VideoForm::VideoForm(bool framelessWindow, bool skin, bool showToolbar, QWidget *parent) : QWidget(parent), ui(new Ui::videoForm), m_skin(skin)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
initUI();
|
||||
installShortcut();
|
||||
updateShowSize(size());
|
||||
bool vertical = size().height() > size().width();
|
||||
this->show_toolbar = showToolbar;
|
||||
if (m_skin) {
|
||||
updateStyleSheet(vertical);
|
||||
}
|
||||
|
@ -289,7 +294,7 @@ void VideoForm::installShortcut()
|
|||
if (!device) {
|
||||
return;
|
||||
}
|
||||
emit device->setScreenPowerMode(false);
|
||||
emit device->setDisplayPower(false);
|
||||
});
|
||||
|
||||
// expandNotificationPanel
|
||||
|
@ -362,21 +367,18 @@ void VideoForm::installShortcut()
|
|||
QRect VideoForm::getScreenRect()
|
||||
{
|
||||
QRect screenRect;
|
||||
QWidget *win = window();
|
||||
if (!win) {
|
||||
return screenRect;
|
||||
}
|
||||
|
||||
QWindow *winHandle = win->windowHandle();
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (winHandle) {
|
||||
screen = winHandle->screen();
|
||||
}
|
||||
if (!screen) {
|
||||
return screenRect;
|
||||
QWidget *win = window();
|
||||
if (win) {
|
||||
QWindow *winHandle = win->windowHandle();
|
||||
if (winHandle) {
|
||||
screen = winHandle->screen();
|
||||
}
|
||||
}
|
||||
|
||||
screenRect = screen->availableGeometry();
|
||||
if (screen) {
|
||||
screenRect = screen->availableGeometry();
|
||||
}
|
||||
return screenRect;
|
||||
}
|
||||
|
||||
|
@ -467,6 +469,8 @@ void VideoForm::switchFullScreen()
|
|||
}
|
||||
|
||||
showNormal();
|
||||
// back to normal size.
|
||||
resize(m_normalSize);
|
||||
// fullscreen window will move (0,0). qt bug?
|
||||
move(m_fullScreenBeforePos);
|
||||
|
||||
|
@ -477,7 +481,7 @@ void VideoForm::switchFullScreen()
|
|||
if (m_skin) {
|
||||
updateStyleSheet(m_frameSize.height() > m_frameSize.width());
|
||||
}
|
||||
showToolForm(true);
|
||||
showToolForm(this->show_toolbar);
|
||||
#ifdef Q_OS_WIN32
|
||||
::SetThreadExecutionState(ES_CONTINUOUS);
|
||||
#endif
|
||||
|
@ -487,6 +491,9 @@ void VideoForm::switchFullScreen()
|
|||
ui->keepRatioWidget->setWidthHeightRatio(-1.0f);
|
||||
}
|
||||
|
||||
// record current size before fullscreen, it will be used to rollback size after exit fullscreen.
|
||||
m_normalSize = size();
|
||||
|
||||
m_fullScreenBeforePos = pos();
|
||||
// 这种临时增加标题栏再全屏的方案会导致收不到mousemove事件,导致setmousetrack失效
|
||||
// mac fullscreen must show title bar
|
||||
|
@ -508,6 +515,9 @@ void VideoForm::switchFullScreen()
|
|||
|
||||
bool VideoForm::isHost()
|
||||
{
|
||||
if (!m_toolForm) {
|
||||
return false;
|
||||
}
|
||||
return m_toolForm->isHost();
|
||||
}
|
||||
|
||||
|
@ -563,23 +573,32 @@ void VideoForm::mousePressEvent(QMouseEvent *event)
|
|||
}
|
||||
}
|
||||
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
QPointF localPos = event->localPos();
|
||||
QPointF globalPos = event->globalPos();
|
||||
#else
|
||||
QPointF localPos = event->position();
|
||||
QPointF globalPos = event->globalPosition();
|
||||
#endif
|
||||
|
||||
if (m_videoWidget->geometry().contains(event->pos())) {
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
|
||||
emit device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
|
||||
QPointF mappedPos = m_videoWidget->mapFrom(this, localPos.toPoint());
|
||||
QMouseEvent newEvent(event->type(), mappedPos, globalPos, event->button(), event->buttons(), event->modifiers());
|
||||
emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size());
|
||||
|
||||
// debug keymap pos
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
qreal x = event->localPos().x() / m_videoWidget->size().width();
|
||||
qreal y = event->localPos().y() / m_videoWidget->size().height();
|
||||
qreal x = localPos.x() / m_videoWidget->size().width();
|
||||
qreal y = localPos.y() / m_videoWidget->size().height();
|
||||
QString posTip = QString(R"("pos": {"x": %1, "y": %2})").arg(x).arg(y);
|
||||
qInfo() << posTip.toStdString().c_str();
|
||||
}
|
||||
} else {
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
m_dragPosition = event->globalPos() - frameGeometry().topLeft();
|
||||
m_dragPosition = globalPos.toPoint() - frameGeometry().topLeft();
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
@ -592,9 +611,15 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event)
|
|||
if (!device) {
|
||||
return;
|
||||
}
|
||||
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
QPointF localPos = event->localPos();
|
||||
QPointF globalPos = event->globalPos();
|
||||
#else
|
||||
QPointF localPos = event->position();
|
||||
QPointF globalPos = event->globalPosition();
|
||||
#endif
|
||||
// local check
|
||||
QPointF local = event->localPos();
|
||||
QPointF local = m_videoWidget->mapFrom(this, localPos.toPoint());
|
||||
if (local.x() < 0) {
|
||||
local.setX(0);
|
||||
}
|
||||
|
@ -607,8 +632,8 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event)
|
|||
if (local.y() > m_videoWidget->height()) {
|
||||
local.setY(m_videoWidget->height());
|
||||
}
|
||||
event->setLocalPos(local);
|
||||
emit device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
|
||||
QMouseEvent newEvent(event->type(), local, globalPos, event->button(), event->buttons(), event->modifiers());
|
||||
emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size());
|
||||
} else {
|
||||
m_dragPosition = QPoint(0, 0);
|
||||
}
|
||||
|
@ -616,16 +641,24 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event)
|
|||
|
||||
void VideoForm::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
QPointF localPos = event->localPos();
|
||||
QPointF globalPos = event->globalPos();
|
||||
#else
|
||||
QPointF localPos = event->position();
|
||||
QPointF globalPos = event->globalPosition();
|
||||
#endif
|
||||
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
|
||||
if (m_videoWidget->geometry().contains(event->pos())) {
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
|
||||
emit device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
|
||||
QPointF mappedPos = m_videoWidget->mapFrom(this, localPos.toPoint());
|
||||
QMouseEvent newEvent(event->type(), mappedPos, globalPos, event->button(), event->buttons(), event->modifiers());
|
||||
emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size());
|
||||
} else if (!m_dragPosition.isNull()) {
|
||||
if (event->buttons() & Qt::LeftButton) {
|
||||
move(event->globalPos() - m_dragPosition);
|
||||
move(globalPos.toPoint() - m_dragPosition);
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
@ -648,8 +681,16 @@ void VideoForm::mouseDoubleClickEvent(QMouseEvent *event)
|
|||
if (!device) {
|
||||
return;
|
||||
}
|
||||
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
|
||||
emit device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
QPointF localPos = event->localPos();
|
||||
QPointF globalPos = event->globalPos();
|
||||
#else
|
||||
QPointF localPos = event->position();
|
||||
QPointF globalPos = event->globalPosition();
|
||||
#endif
|
||||
QPointF mappedPos = m_videoWidget->mapFrom(this, localPos.toPoint());
|
||||
QMouseEvent newEvent(event->type(), mappedPos, globalPos, event->button(), event->buttons(), event->modifiers());
|
||||
emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -705,7 +746,11 @@ void VideoForm::paintEvent(QPaintEvent *paint)
|
|||
{
|
||||
Q_UNUSED(paint)
|
||||
QStyleOption opt;
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
opt.init(this);
|
||||
#else
|
||||
opt.initFrom(this);
|
||||
#endif
|
||||
QPainter p(this);
|
||||
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
|
||||
}
|
||||
|
@ -713,8 +758,10 @@ void VideoForm::paintEvent(QPaintEvent *paint)
|
|||
void VideoForm::showEvent(QShowEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
if (!isFullScreen()) {
|
||||
showToolForm();
|
||||
if (!isFullScreen() && this->show_toolbar) {
|
||||
QTimer::singleShot(500, this, [this](){
|
||||
showToolForm(this->show_toolbar);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ class VideoForm : public QWidget, public qsc::DeviceObserver
|
|||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VideoForm(bool framelessWindow = false, bool skin = true, QWidget *parent = 0);
|
||||
explicit VideoForm(bool framelessWindow = false, bool skin = true, bool showToolBar = true, QWidget *parent = 0);
|
||||
~VideoForm();
|
||||
|
||||
void staysOnTop(bool top = true);
|
||||
|
@ -32,7 +32,6 @@ public:
|
|||
void removeBlackRect();
|
||||
void showFPS(bool show);
|
||||
void switchFullScreen();
|
||||
|
||||
bool isHost();
|
||||
|
||||
private:
|
||||
|
@ -79,11 +78,15 @@ private:
|
|||
|
||||
//inside member
|
||||
QSize m_frameSize;
|
||||
QSize m_normalSize;
|
||||
QPoint m_dragPosition;
|
||||
float m_widthHeightRatio = 0.5f;
|
||||
bool m_skin = true;
|
||||
QPoint m_fullScreenBeforePos;
|
||||
QString m_serial;
|
||||
|
||||
//Whether to display the toolbar when connecting a device.
|
||||
bool show_toolbar = true;
|
||||
};
|
||||
|
||||
#endif // VIDEOFORM_H
|
||||
|
|
|
@ -4,10 +4,16 @@
|
|||
#include <QDebug>
|
||||
|
||||
#include "config.h"
|
||||
#ifdef Q_OS_OSX
|
||||
#include "path.h"
|
||||
#endif
|
||||
|
||||
#define GROUP_COMMON "common"
|
||||
|
||||
// config
|
||||
#define COMMON_LANGUAGE_KEY "Language"
|
||||
#define COMMON_LANGUAGE_DEF "Auto"
|
||||
|
||||
#define COMMON_TITLE_KEY "WindowTitle"
|
||||
#define COMMON_TITLE_DEF QCoreApplication::applicationName()
|
||||
|
||||
|
@ -15,13 +21,13 @@
|
|||
#define COMMON_PUSHFILE_DEF "/sdcard/"
|
||||
|
||||
#define COMMON_SERVER_VERSION_KEY "ServerVersion"
|
||||
#define COMMON_SERVER_VERSION_DEF "1.21"
|
||||
#define COMMON_SERVER_VERSION_DEF "3.1"
|
||||
|
||||
#define COMMON_SERVER_PATH_KEY "ServerPath"
|
||||
#define COMMON_SERVER_PATH_DEF "/data/local/tmp/scrcpy-server.jar"
|
||||
|
||||
#define COMMON_MAX_FPS_KEY "MaxFps"
|
||||
#define COMMON_MAX_FPS_DEF 60
|
||||
#define COMMON_MAX_FPS_DEF 0
|
||||
|
||||
#define COMMON_DESKTOP_OPENGL_KEY "UseDesktopOpenGL"
|
||||
#define COMMON_DESKTOP_OPENGL_DEF -1
|
||||
|
@ -90,6 +96,12 @@
|
|||
#define COMMON_AUTO_UPDATE_DEVICE_KEY "AutoUpdateDevice"
|
||||
#define COMMON_AUTO_UPDATE_DEVICE_DEF true
|
||||
|
||||
#define COMMON_TRAY_MESSAGE_SHOWN_KEY "TrayMessageShown"
|
||||
#define COMMON_TRAY_MESSAGE_SHOWN_DEF false
|
||||
|
||||
#define COMMON_SHOW_TOOLBAR_KEY "showToolbar"
|
||||
#define COMMON_SHOW_TOOLBAR_DEF true
|
||||
|
||||
// device config
|
||||
#define SERIAL_WINDOW_RECT_KEY_X "WindowRectX"
|
||||
#define SERIAL_WINDOW_RECT_KEY_Y "WindowRectY"
|
||||
|
@ -99,15 +111,21 @@
|
|||
#define SERIAL_NICK_NAME_KEY "NickName"
|
||||
#define SERIAL_NICK_NAME_DEF "Phone"
|
||||
|
||||
// IP history
|
||||
#define IP_HISTORY_KEY "IpHistory"
|
||||
#define IP_HISTORY_DEF ""
|
||||
#define IP_HISTORY_MAX 10
|
||||
|
||||
QString Config::s_configPath = "";
|
||||
|
||||
Config::Config(QObject *parent) : QObject(parent)
|
||||
{
|
||||
m_settings = new QSettings(getConfigPath() + "/config.ini", QSettings::IniFormat);
|
||||
m_settings->setIniCodec("UTF-8");
|
||||
|
||||
m_userData = new QSettings(getConfigPath() + "/userdata.ini", QSettings::IniFormat);
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
m_settings->setIniCodec("UTF-8");
|
||||
m_userData->setIniCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
qDebug()<<m_userData->childGroups();
|
||||
}
|
||||
|
@ -125,7 +143,15 @@ const QString &Config::getConfigPath()
|
|||
QFileInfo fileInfo(s_configPath);
|
||||
if (s_configPath.isEmpty() || !fileInfo.isDir()) {
|
||||
// default application dir
|
||||
// mac系统当从finder打开app时,默认工作目录不再是可执行程序的目录了,而是"/"
|
||||
// 而Qt的获取工作目录的api都依赖QCoreApplication的初始化,所以使用mac api获取当前目录
|
||||
#ifdef Q_OS_OSX
|
||||
// get */QtScrcpy.app path
|
||||
s_configPath = Path::GetCurrentPath();
|
||||
s_configPath += "/Contents/MacOS/config";
|
||||
#else
|
||||
s_configPath = "config";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
return s_configPath;
|
||||
|
@ -149,6 +175,7 @@ void Config::setUserBootConfig(const UserBootConfig &config)
|
|||
m_userData->setValue(COMMON_KEEP_ALIVE_KEY, config.keepAlive);
|
||||
m_userData->setValue(COMMON_SIMPLE_MODE_KEY, config.simpleMode);
|
||||
m_userData->setValue(COMMON_AUTO_UPDATE_DEVICE_KEY, config.autoUpdateDevice);
|
||||
m_userData->setValue(COMMON_SHOW_TOOLBAR_KEY, config.showToolbar);
|
||||
m_userData->endGroup();
|
||||
m_userData->sync();
|
||||
}
|
||||
|
@ -172,10 +199,28 @@ UserBootConfig Config::getUserBootConfig()
|
|||
config.keepAlive = m_userData->value(COMMON_KEEP_ALIVE_KEY, COMMON_KEEP_ALIVE_DEF).toBool();
|
||||
config.simpleMode = m_userData->value(COMMON_SIMPLE_MODE_KEY, COMMON_SIMPLE_MODE_DEF).toBool();
|
||||
config.autoUpdateDevice = m_userData->value(COMMON_AUTO_UPDATE_DEVICE_KEY, COMMON_AUTO_UPDATE_DEVICE_DEF).toBool();
|
||||
config.showToolbar =m_userData->value(COMMON_SHOW_TOOLBAR_KEY,COMMON_SHOW_TOOLBAR_DEF).toBool();
|
||||
m_userData->endGroup();
|
||||
return config;
|
||||
}
|
||||
|
||||
void Config::setTrayMessageShown(bool shown)
|
||||
{
|
||||
m_userData->beginGroup(GROUP_COMMON);
|
||||
m_userData->setValue(COMMON_TRAY_MESSAGE_SHOWN_KEY, shown);
|
||||
m_userData->endGroup();
|
||||
m_userData->sync();
|
||||
}
|
||||
|
||||
bool Config::getTrayMessageShown()
|
||||
{
|
||||
bool shown;
|
||||
m_userData->beginGroup(GROUP_COMMON);
|
||||
shown = m_userData->value(COMMON_TRAY_MESSAGE_SHOWN_KEY, COMMON_TRAY_MESSAGE_SHOWN_DEF).toBool();
|
||||
m_userData->endGroup();
|
||||
return shown;
|
||||
}
|
||||
|
||||
void Config::setRect(const QString &serial, const QRect &rc)
|
||||
{
|
||||
m_userData->beginGroup(serial);
|
||||
|
@ -227,7 +272,7 @@ QString Config::getServerVersion()
|
|||
|
||||
int Config::getMaxFps()
|
||||
{
|
||||
int fps = 60;
|
||||
int fps = 0;
|
||||
m_settings->beginGroup(GROUP_COMMON);
|
||||
fps = m_settings->value(COMMON_MAX_FPS_KEY, COMMON_MAX_FPS_DEF).toInt();
|
||||
m_settings->endGroup();
|
||||
|
@ -327,6 +372,15 @@ void Config::deleteGroup(const QString &serial)
|
|||
m_userData->remove(serial);
|
||||
}
|
||||
|
||||
QString Config::getLanguage()
|
||||
{
|
||||
QString language;
|
||||
m_settings->beginGroup(GROUP_COMMON);
|
||||
language = m_settings->value(COMMON_LANGUAGE_KEY, COMMON_LANGUAGE_DEF).toString();
|
||||
m_settings->endGroup();
|
||||
return language;
|
||||
}
|
||||
|
||||
QString Config::getTitle()
|
||||
{
|
||||
QString title;
|
||||
|
@ -335,3 +389,34 @@ QString Config::getTitle()
|
|||
m_settings->endGroup();
|
||||
return title;
|
||||
}
|
||||
|
||||
void Config::saveIpHistory(const QString &ip)
|
||||
{
|
||||
QStringList ipList = getIpHistory();
|
||||
|
||||
// 移除已存在的相同IP(避免重复)
|
||||
ipList.removeAll(ip);
|
||||
|
||||
// 将新IP添加到开头
|
||||
ipList.prepend(ip);
|
||||
|
||||
// 限制历史记录数量
|
||||
while (ipList.size() > IP_HISTORY_MAX) {
|
||||
ipList.removeLast();
|
||||
}
|
||||
|
||||
m_userData->setValue(IP_HISTORY_KEY, ipList);
|
||||
m_userData->sync();
|
||||
}
|
||||
|
||||
QStringList Config::getIpHistory()
|
||||
{
|
||||
QStringList ipList = m_userData->value(IP_HISTORY_KEY, IP_HISTORY_DEF).toStringList();
|
||||
return ipList;
|
||||
}
|
||||
|
||||
void Config::clearIpHistory()
|
||||
{
|
||||
m_userData->remove(IP_HISTORY_KEY);
|
||||
m_userData->sync();
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ struct UserBootConfig
|
|||
bool keepAlive = false;
|
||||
bool simpleMode = false;
|
||||
bool autoUpdateDevice = true;
|
||||
bool showToolbar = true;
|
||||
};
|
||||
|
||||
class QSettings;
|
||||
|
@ -33,6 +34,7 @@ public:
|
|||
static Config &getInstance();
|
||||
|
||||
// config
|
||||
QString getLanguage();
|
||||
QString getTitle();
|
||||
QString getServerVersion();
|
||||
int getMaxFps();
|
||||
|
@ -50,6 +52,8 @@ public:
|
|||
// user data:common
|
||||
void setUserBootConfig(const UserBootConfig &config);
|
||||
UserBootConfig getUserBootConfig();
|
||||
void setTrayMessageShown(bool shown);
|
||||
bool getTrayMessageShown();
|
||||
|
||||
// user data:device
|
||||
void setNickName(const QString &serial, const QString &name);
|
||||
|
@ -59,6 +63,11 @@ public:
|
|||
|
||||
void deleteGroup(const QString &serial);
|
||||
|
||||
// IP history methods
|
||||
void saveIpHistory(const QString &ip);
|
||||
QStringList getIpHistory();
|
||||
void clearIpHistory();
|
||||
|
||||
private:
|
||||
explicit Config(QObject *parent = nullptr);
|
||||
const QString &getConfigPath();
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
#include <QX11Info>
|
||||
#include <QtGlobal>
|
||||
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
#include <QtX11Extras/QX11Info>
|
||||
#else
|
||||
#include <QtGui/private/qtx11extras_p.h>
|
||||
#endif
|
||||
|
||||
#include <xcb/xproto.h>
|
||||
#include <stdlib.h>
|
||||
|
|
6
QtScrcpy/util/path.h
Normal file
6
QtScrcpy/util/path.h
Normal file
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
class Path {
|
||||
public:
|
||||
static const char* GetCurrentPath();
|
||||
};
|
7
QtScrcpy/util/path.mm
Normal file
7
QtScrcpy/util/path.mm
Normal file
|
@ -0,0 +1,7 @@
|
|||
#include "path.h"
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
const char* Path::GetCurrentPath() {
|
||||
return [[[NSBundle mainBundle] bundlePath] UTF8String];
|
||||
}
|
28
QtScrcpy/util/winutils.cpp
Normal file
28
QtScrcpy/util/winutils.cpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
#include <QDebug>
|
||||
#include <Windows.h>
|
||||
#include <dwmapi.h>
|
||||
#pragma comment(lib, "dwmapi")
|
||||
|
||||
#include "winutils.h"
|
||||
|
||||
enum : WORD
|
||||
{
|
||||
DwmwaUseImmersiveDarkMode = 20,
|
||||
DwmwaUseImmersiveDarkModeBefore20h1 = 19
|
||||
};
|
||||
|
||||
WinUtils::WinUtils(){};
|
||||
|
||||
WinUtils::~WinUtils(){};
|
||||
|
||||
// Set dark border to window
|
||||
// Reference: qt/qtbase.git/tree/src/plugins/platforms/windows/qwindowswindow.cpp
|
||||
bool WinUtils::setDarkBorderToWindow(const HWND &hwnd, const bool &d)
|
||||
{
|
||||
const BOOL darkBorder = d ? TRUE : FALSE;
|
||||
const bool ok = SUCCEEDED(DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkMode, &darkBorder, sizeof(darkBorder)))
|
||||
|| SUCCEEDED(DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkModeBefore20h1, &darkBorder, sizeof(darkBorder)));
|
||||
if (!ok)
|
||||
qWarning("%s: Unable to set dark window border.", __FUNCTION__);
|
||||
return ok;
|
||||
}
|
16
QtScrcpy/util/winutils.h
Normal file
16
QtScrcpy/util/winutils.h
Normal file
|
@ -0,0 +1,16 @@
|
|||
#ifndef WINUTILS_H
|
||||
#define WINUTILS_H
|
||||
|
||||
#include <QApplication>
|
||||
#include <Windows.h>
|
||||
|
||||
class WinUtils
|
||||
{
|
||||
public:
|
||||
WinUtils();
|
||||
~WinUtils();
|
||||
|
||||
static bool setDarkBorderToWindow(const HWND &hwnd, const bool &d);
|
||||
};
|
||||
|
||||
#endif // WINUTILS_H
|
178
README.md
178
README.md
|
@ -1,27 +1,28 @@
|
|||
# QtScrcpy
|
||||
|
||||
[](https://opencollective.com/QtScrcpy)
|
||||
[](https://opencollective.com/QtScrcpy)
|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
[中文介绍](README_zh.md)
|
||||
[中文用户?点我查看中文介绍](README_zh.md)
|
||||
|
||||
QtScrcpy connects to Android devices via USB (or via TCP/IP) for display and control. It does NOT require the root privileges.
|
||||
QtScrcpy supports displaying and controlling Android devices via USB or over network. It does NOT require root privileges.
|
||||
|
||||
It supports three major platforms: GNU/Linux, Windows and MacOS.
|
||||
It supports three major platforms: GNU/Linux, Windows and macOS.
|
||||
|
||||
It focuses on:
|
||||
|
||||
- **lightness** (native, displays only the device screen)
|
||||
- **performance** (30~60fps)
|
||||
- **lightness** (displays only the device screen)
|
||||
- **performance** (30~60 fps)
|
||||
- **quality** (1920×1080 or above)
|
||||
- **low latency** ([35~70ms][lowlatency])
|
||||
- **low startup time** (~1 second to display the first image)
|
||||
- **non-intrusiveness** (nothing is left installed on the device)
|
||||
- **low startup time** (only about 1 second to display the first frame)
|
||||
- **non-intrusiveness** (nothing will be installed on the device)
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
|
@ -31,37 +32,73 @@ It focuses on:
|
|||
|
||||

|
||||
|
||||
## Customized key mapping
|
||||
You can write your own script to map keyboard and mouse actions to touches and clicks of the mobile phone according to your needs. [Here](docs/KeyMapDes.md) are the rules.
|
||||
## The author has developed a more professional screen casting software called `QuickMirror`
|
||||
QuickMirror function&features:
|
||||
- Equipment screen casting&control: batch screen casting, individual control, batch control
|
||||
- Group management
|
||||
- WiFi screen mirroring/OTG screen mirroring
|
||||
- Adb shell shortcut command
|
||||
- File transfer, apk installation
|
||||
- Multiple screen mirroring: In OTG mirroring mode, with low resolution and smoothness settings, a single computer can manage 500+phones simultaneously
|
||||
- Low latency: USB screen mirroring 1080p latency is within 30ms, which is lower than all screen mirroring software on the market in terms of latency at the same resolution and smoothness
|
||||
- Low CPU usage: pure C++development, high-performance GPU video rendering
|
||||
- High resolution: adjustable, maximum support for native resolution of Android terminals
|
||||
- Perfect Chinese input: Supports Xianyu app, supports Samsung phones
|
||||
- The free version can cast up to 10 screens, with unlimited functionality (except for automatic screen mirroring)
|
||||
- QuickMirror tutorial: https://lrbnfell4p.feishu.cn/docx/EMkvdfIvDowy3UxsXUCcpPV8nDh
|
||||
- QuickMirror Telegram communication group: https://t.me/+Ylf_5V_rDCMyODQ1
|
||||
- Preview of QuickMirror Interface:
|
||||

|
||||
|
||||
A script for "PUBG mobile" and TikTok mapping is provided by default. Once enabled, you can play the game with your keyboard and mouse as the PC version. or you can use up/down/left/right direction keys to simulate up/down/left/right sliding. You can also write your own mapping files for other games according to [writing rules](docs/KeyMapDes.md). The default key mapping is as follows:
|
||||
## Mapping Keys
|
||||
You can write your script to map keyboard and mouse actions to touches and clicks of the mobile phone according to your needs. [Here](docs/KeyMapDes.md) are the script writing rules.
|
||||
|
||||

|
||||
Script for TikTok and some other games are provided by default. Once enabled, you can play the game with your keyboard and mouse. The default key mapping for PUBG Mobile is as follows:
|
||||
|
||||
[Here is a video demonstration of playing "PUBG mobile"](http://mp.weixin.qq.com/mp/video?__biz=MzU1NTg5MjYyNw==&mid=100000015&sn=3e301fdc5a364bd16d6207fa674bc8b3&vid=wxv_968792362971430913&idx=1&vidsn=eec329cc13c3e24c187dc9b4d5eb8760&fromid=1&scene=20&xtrack=1&clicktime=1567346543&sessionid=1567346375&subscene=92&ascene=0&fasttmpl_type=0&fasttmpl_fullversion=4730859-zh_CN-zip&fasttmpl_flag=0&realreporttime=1567346543910#wechat_redirect)
|
||||

|
||||
|
||||
Here is the instruction of adding new customized mapping files.
|
||||
Instruction for adding new customized mapping files.
|
||||
|
||||
- Write a customized script and put it in the `keymap` directory
|
||||
- Click `refresh script` to check whether it can be found
|
||||
- Click `refresh script` to show it
|
||||
- Select your script
|
||||
- Connect your phone, start service and click `apply`
|
||||
- Press `~` key (left side of the number key 1) to switch to the custom mapping mode (It can be changed in the script as `switchkey`)
|
||||
- Connect to your phone, start service and click `apply`
|
||||
- Press `~` key (the SwitchKey in the key map script) to switch to custom mapping mode
|
||||
- Press the ~ key again to switch back to normal mode
|
||||
- (For PUBG and similar games) If you want to drive cars with WASD, you need to check the `single rocker mode` in the game setting.
|
||||
- (For games such as PUBG Mobile) If you want to move vehicles with the STEER_WHEEL keys, you need to set the move mode to `single rocker mode`.
|
||||
|
||||
If you don't know how to manually write mapping rules, you can also use the `QuickAssistant` developed by the author
|
||||
QuickAssistant Features&Functions:
|
||||
- Play Android mobile games smoothly through keyboard and mouse
|
||||
- Interface based editing of key mapping script
|
||||
- Support pausing the computer screen and using only keyboard and mouse operations
|
||||
- Screenshot&Recording of Mobile Screen
|
||||
- Simple batch control
|
||||
- Android 11+supports playing mobile audio on computers (under development...)
|
||||
- Mobile app installation free
|
||||
- Fast and instant connection
|
||||
- Low latency: USB screen mirroring 1080p latency is within 30ms, which is lower than all screen mirroring software on the market in terms of latency at the same resolution and smoothness
|
||||
- Low CPU usage: pure C++development, high-performance GPU video rendering
|
||||
- High resolution: adjustable, maximum support for native resolution of Android terminals
|
||||
- Telegram Group:https://t.me/+EnQNmb47C_liYmRl
|
||||
- [QuickAssistant](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh)
|
||||
|
||||
## Group control
|
||||
You can control all your phones at the same time.
|
||||
|
||||

|
||||

|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#barry-ran/QtScrcpy&Date)
|
||||
|
||||
## Thanks
|
||||
|
||||
QtScrcpy is based on [Genymobile's](https://github.com/Genymobile) [scrcpy](https://github.com/Genymobile/scrcpy) project. Thanks
|
||||
QtScrcpy is based on [Genymobile](https://github.com/Genymobile)'s [scrcpy](https://github.com/Genymobile/scrcpy) project. Thanks a lot!
|
||||
|
||||
The difference between QtScrcpy and the original scrcpy is as follows:
|
||||
|
||||
keys|scrcpy|QtScrcpy
|
||||
key points|scrcpy|QtScrcpy
|
||||
--|:--:|:--:
|
||||
ui|sdl|qt
|
||||
video encode|ffmpeg|ffmpeg
|
||||
|
@ -81,19 +118,19 @@ build|meson+gradle|qmake or CMake
|
|||
## Learn
|
||||
|
||||
If you are interested in it and want to learn how it works but do not know how to get started, you can choose to purchase my recorded video lessons.
|
||||
It details the development architecture and the development process of the entire software, and help you develop QtScrcpy from scratch.
|
||||
It details the development architecture and the development process of the entire software and helps you develop QtScrcpy from scratch.
|
||||
|
||||
Course introduction:[https://blog.csdn.net/rankun1/article/details/87970523](https://blog.csdn.net/rankun1/article/details/87970523)
|
||||
|
||||
You can join my QQ group for QtScrcpy and exchange ideas with like-minded friends.:
|
||||
You can join Telegram Group for QtScrcpy and exchange ideas with like-minded friends.:
|
||||
|
||||
QQ Group number:901736468
|
||||
Telegram Group:https://t.me/+EnQNmb47C_liYmRl
|
||||
|
||||
|
||||
## Requirements
|
||||
Android API >= 21 (Android 5.0).
|
||||
|
||||
Make sure you enabled [adb debugging][enable-adb] on your device(s).
|
||||
Make sure you have enabled [ADB debugging][enable-adb] on your device(s).
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
|
@ -104,63 +141,70 @@ Make sure you enabled [adb debugging][enable-adb] on your device(s).
|
|||
[github-download]: https://github.com/barry-ran/QtScrcpy/releases
|
||||
|
||||
### Windows
|
||||
For Windows, for simplicity, prebuilt archives with all the dependencies (including adb) are available:
|
||||
On Windows, for simplicity, prebuilt archives with all the dependencies (including ADB) are available at Releases:
|
||||
|
||||
- [`QtScrcpy`][github-download]
|
||||
|
||||
or you can [build it by yourself](##Build)
|
||||
or you can [build it yourself](#Build)
|
||||
|
||||
### Mac OS
|
||||
For Mac OS, for simplicity, prebuilt archives with all the dependencies (including adb) are available:
|
||||
On Mac OS, for simplicity, prebuilt archives with all the dependencies (including ADB) are available at Releases:
|
||||
|
||||
- [`QtScrcpy`][github-download]
|
||||
|
||||
or you can [build it by yourself](##Build)
|
||||
or you can [build it yourself](#Build)
|
||||
|
||||
### Linux
|
||||
you can [build it by yourself](##Build)(just ubuntu test)
|
||||
For Arch Linux Users, you can use AUR to install: `yay -Syu qtscrcpy` (may be outdated; maintainer: [yochananmarqos](https://aur.archlinux.org/account/yochananmarqos))
|
||||
|
||||
For users in other distros, you can use the prebuilt archives from Releases:
|
||||
|
||||
- [`QtScrcpy`][github-download]
|
||||
|
||||
or you can get it at [GitHub Actions](https://github.com/barry-ran/QtScrcpy/actions/workflows/ubuntu.yml), in branch `dev` and download the latest artifact.
|
||||
|
||||
or you can [build it yourself](#Build) (not recommended, get it in Actions if you can)
|
||||
|
||||
## Run
|
||||
Connect to your Android device on your computer, then run the program and click `USB connect` or `WiFi connect`
|
||||
|
||||
### Wireless connection steps (ensure that the mobile phone and PC are in the same LAN):
|
||||
### Wireless connection steps (ensure that the mobile phone and PC are on the same LAN):
|
||||
1. Enable USB debugging in developer options on the Android device
|
||||
2. Connect the Android device to computer via USB
|
||||
2. Connect the Android device to the computer via USB
|
||||
3. Click update device, and you will see that the device number is updated
|
||||
4. Click get device IP
|
||||
5. Click start adbd
|
||||
6. Click wireless connect
|
||||
7. Click update device again, and another device with IP address will be found. Select this device.
|
||||
7. Click update device again, and another device with an IP address will be found. Select this device.
|
||||
8. Click start service
|
||||
|
||||
|
||||
Note: it is not necessary to keep you Android device connected via USB after you start adbd.
|
||||
Note: it is not necessary to keep your Android device connected via USB after you start adbd.
|
||||
|
||||
## Interface button introduction:
|
||||
|
||||
- Start config: function parameter settings before starting the service
|
||||
|
||||
You can set the bit rate, resolution, recording format, and video save path of the local recorded video.
|
||||
You can set the bit rate, resolution, recording format, and video save path of the locally recorded video.
|
||||
|
||||
- Background record: the Android device screen is not displayed after starting the service. It is recorded in background.
|
||||
- Always on top: the video window for Android device will be kept on the top
|
||||
- Background record: the Android device screen is not displayed after starting the service. It is recorded in the background.
|
||||
- Always on top: the video window for Android devices will be kept on the top
|
||||
- Close screen: automatically turn off the Android device screen to save power after starting the service
|
||||
- Reverse connection: service startup mode. You can uncheck it if you experience connection failure with message `more than one device`
|
||||
- Reverse connection: service startup mode. You can uncheck it if you experience connection failure with a message `more than one device`
|
||||
|
||||
- Refresh devices: Refresh the currently connected device
|
||||
- Start service: connect to the Android device
|
||||
- Stop service: disconnect from Android device
|
||||
- Stop service: disconnect from the Android device
|
||||
- Stop all services: disconnect all connected Android devices
|
||||
- Get device IP: Get the IP address of the Android device and update it to the "Wireless" area for the ease of wireless connection setting.
|
||||
- Start adbd: Start the adbd service of the Android device. You must start it before the wireless connection.
|
||||
- Wireless connect: Connect to Android devices wirelessly
|
||||
- Wireless disconnect: Disconnect wirelessly connected Android devices
|
||||
- adb command: execute customized adb commands (blocking commands are not supported now, such as shell)
|
||||
- adb command: execute customized ADB commands (blocking commands are not supported now, such as a shell)
|
||||
|
||||
|
||||
## The main function
|
||||
- Display Android device screens in real time
|
||||
- Display Android device screens in real-time
|
||||
- Real-time mouse and keyboard control of Android devices
|
||||
- Screen recording
|
||||
- Screenshot to png
|
||||
|
@ -176,10 +220,9 @@ Note: it is not necessary to keep you Android device connected via USB after you
|
|||
It is possible to synchronize clipboards between the computer and the device, in
|
||||
both directions:
|
||||
|
||||
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
|
||||
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard;
|
||||
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
|
||||
breaks non-ASCII characters).
|
||||
- `Ctrl + c` copies the device clipboard to the computer clipboard;
|
||||
- `Ctrl + Shift + v` copies the computer clipboard to the device clipboard;
|
||||
- `Ctrl + v` _pastes_ the computer clipboard as a sequence of text events (non-ASCII characters does not yet work).
|
||||
- Group control
|
||||
- Sync device speaker sound to the computer (based on [sndcpy](https://github.com/rom1v/sndcpy), Android 10+ only)
|
||||
|
||||
|
@ -222,39 +265,44 @@ _³Only on Android >= 7._
|
|||
[DEVELOP](docs/DEVELOP.md)
|
||||
|
||||
Everyone is welcome to maintain this project and contribute your own code, but please follow these requirements:
|
||||
1. pr please mention the dev branch, not the master branch
|
||||
2. Please rebase dev before mentioning pr
|
||||
3. pr please submit on the principle of a small number of times (a small function point is recommended to mention a pr)
|
||||
4. Please keep the code style consistent with the existing style
|
||||
1. Please open PRs to the dev branch instead of the master branch
|
||||
2. Please rebase the original project before opening PRs
|
||||
3. Please submit PRs on the principle of "small amounts, many times" (one PR for a change is recommended)
|
||||
4. Please keep the code style consistent with the existing style.
|
||||
|
||||
## Why develop QtScrcpy?
|
||||
There are several reasons listed as below according to importance (high to low).
|
||||
1. In the process of learning Qt, I need a real project to try
|
||||
2. I have some background skill about audio and video and I am interested at them
|
||||
There are several reasons listed below according to importance (high to low).
|
||||
1. In the process of learning Qt, I need a real project to try.
|
||||
2. I have some background skills in audio and video and I am interested in them.
|
||||
3. I have some Android development skills. But I have used it for a long time. I want to consolidate it.
|
||||
4. I found scrcpy and decided to re-make it with the new technology stack (C++ + Qt + Opengl + ffmpeg)
|
||||
4. I found scrcpy and decided to re-make it with the new technology stack (C++ + Qt + Opengl + FFmpeg).
|
||||
|
||||
|
||||
## Build
|
||||
All the dependencies are provided and it is easy to compile.
|
||||
|
||||
### PC client
|
||||
1. Set up the Qt development environment on the target platform.
|
||||
Qt version>=5.12 (use MSVC 2019 on Windows)
|
||||
2. Clone the project (git clone --recursive git@github.com:barry-ran/QtScrcpy.git)
|
||||
3. Open the project root directory `CMakeLists.txt` with QtCreator
|
||||
4. Compile and run
|
||||
### QtScrcpy
|
||||
#### Non-Arch Linux Users
|
||||
1. Set up the Qt development environment with the official Qt installer or third-party tools such as [aqt](https://github.com/miurahr/aqtinstall) on the target platform.
|
||||
Qt version bigger than 5.12 is required. (use MSVC 2019 on Windows)
|
||||
2. Clone the project with `git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git`
|
||||
3. For Windows, open CMakeLists.txt with QtCreator and compile Release
|
||||
4. For Linux, directly run `./ci/linux/build_for_linux.sh "Release"`
|
||||
Note: compiled artifacts are located at `output/x64/Release`
|
||||
|
||||
### Android (If you do not have special requirements, you can directly use the built-in scrcpy-server.jar)
|
||||
#### Arch Linux Users
|
||||
1. Install packages: `base-devel cmake qt5-base qt5-multimedia qt5-x11extras` (`qtcreator` is recommended)
|
||||
2. Clone the project with `git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git`
|
||||
3. Run `./ci/linux/build_for_linux.sh "Release"`
|
||||
|
||||
1. Set up an Android development environment on the target platform
|
||||
### Scrcpy-Server
|
||||
1. Set up Android development environment on the target platform
|
||||
2. Open server project in project root with Android Studio
|
||||
3. The first time you open it, if you do not have the corresponding version of gradle, you will be prompted to find gradle, whether to upgrade gradle and create it. Select Cancel. After canceling, you will be prompted to select the location of the existing gradle. You can also cancel it (it will download automatically).
|
||||
4. Edit the code as needed, but of course you do n’t need to.
|
||||
3. The first time you open it, if you do not have the corresponding version of Gradle, you will be prompted to find Gradle, whether to upgrade Gradle or create it. Select Cancel. After cancelling, you will be prompted to select the location of existing Gradle. Cancel it too and it will download automatically.
|
||||
4. After compiling the apk, rename it to scrcpy-server and replace QtScrcpy/QtScrcpyCore/src/third_party/scrcpy-server.
|
||||
|
||||
## Licence
|
||||
Since it is based on scrcpy, respect its Licence
|
||||
Since it is based on scrcpy, it uses the same license as scrcpy
|
||||
|
||||
Copyright (C) 2025 Rankun
|
||||
|
||||
|
@ -274,7 +322,7 @@ Since it is based on scrcpy, respect its Licence
|
|||
|
||||
[Barry CSDN](https://blog.csdn.net/rankun1)
|
||||
|
||||
An ordinary programmer, working mainly in C++ for desktop client development, graduated from Shandong for more than a year of steel simulation education software, and later moved to Shanghai to work in security, online education related fields, familiar with audio and video. I have an understanding of audio and video fields such as voice calls, live education, video conferencing and other related solutions. I also have experience in Android, Linux server and other kinds of development.
|
||||
An ordinary programmer, working mainly in C++ for desktop client development, graduated from Shandong for more than a year of steel simulation education software, and later moved to Shanghai to work in security, online education-related fields, familiar with audio and video. I have an understanding of audio and video fields such as voice calls, live education, video conferencing and other related solutions. I also have experience in Android, Linux servers and other kinds of development.
|
||||
|
||||
## Contributors
|
||||
|
||||
|
|
235
README_zh.md
235
README_zh.md
|
@ -6,12 +6,14 @@
|
|||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
[English introduction](README.md)
|
||||
[Speaks English? Click me for English introduction.](README.md)
|
||||
|
||||
QtScrcpy可以通过USB(或通过TCP/IP)连接Android设备,并进行显示和控制。不需要root权限。
|
||||
QtScrcpy 可以通过 USB / 网络连接Android设备,并进行显示和控制。无需root权限。
|
||||
|
||||
同时支持GNU/Linux,Windows和MacOS三大主流桌面平台
|
||||
同时支持 GNU/Linux ,Windows 和 MacOS 三大主流桌面平台。
|
||||
|
||||
它专注于:
|
||||
|
||||
|
@ -19,7 +21,7 @@ QtScrcpy可以通过USB(或通过TCP/IP)连接Android设备,并进行显示和
|
|||
- **性能** (30~60fps)
|
||||
- **质量** (1920×1080以上)
|
||||
- **低延迟** ([35~70ms][低延迟])
|
||||
- **快速启动** (1s内就可以看到第一帧图像)
|
||||
- **快速启动** (1s 内就可以看到第一帧图像)
|
||||
- **非侵入性** (不在设备上安装任何软件)
|
||||
|
||||
[低延迟]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
@ -30,43 +32,81 @@ QtScrcpy可以通过USB(或通过TCP/IP)连接Android设备,并进行显示和
|
|||
|
||||

|
||||
|
||||
## 作者开发了更加专业的投屏软件`极限投屏`
|
||||
极限投屏功能&特点:
|
||||
- 设备投屏&控制:批量投屏、单个控制、批量控制
|
||||
- 分组管理
|
||||
- wifi投屏/OTG投屏
|
||||
- adb shell快捷指令
|
||||
- 文件传输、apk安装
|
||||
- 投屏数量多:在OTG投屏模式,设置分辨率和流畅度为低的情况下,单台电脑可以同时管理500+台手机
|
||||
- 低延迟:usb投屏1080p延迟在30ms以内,在相同分辨率流畅度情况下,比市面上所有投屏软件延迟都低
|
||||
- cpu占用率低:纯C++开发,高性能GPU视频渲染
|
||||
- 高分辨率:可调节,最大支持安卓终端的原生分辨率
|
||||
- 完美中文输入:支持闲鱼app,支持三星手机
|
||||
- 免费版最多投屏10台,功能无限制(除了自动重新投屏)
|
||||
- 极限投屏使用教程:https://lrbnfell4p.feishu.cn/docx/QRMhd9nImorAGgxVLlmczxSdnYf
|
||||
- 极限投屏qq交流群:822464342
|
||||
- 极限投屏界面预览:
|
||||

|
||||
|
||||
## 自定义按键映射
|
||||
可以根据需要,自己编写脚本将PC键盘按键映射为手机的触摸点击,编写规则在[这里](docs/KeyMapDes_zh.md)。
|
||||
可以根据需要,自己编写脚本将键盘按键映射为手机的触摸点击,编写规则在[这里](docs/KeyMapDes_zh.md)。
|
||||
|
||||
默认自带了针对和平精英手游和抖音进行键鼠映射的映射脚本,开启平精英手游后可以用键鼠像玩端游一样玩和平精英手游,开启抖音映射以后可以使用上下左右方向键模拟上下左右滑动,你也可以按照[编写规则](docs/KeyMapDes_zh.md)编写其他游戏的映射文件,默认按键映射如下:
|
||||
|
||||

|
||||
|
||||
[这里有玩和平精英的视频演示](http://mp.weixin.qq.com/mp/video?__biz=MzU1NTg5MjYyNw==&mid=100000015&sn=3e301fdc5a364bd16d6207fa674bc8b3&vid=wxv_968792362971430913&idx=1&vidsn=eec329cc13c3e24c187dc9b4d5eb8760&fromid=1&scene=20&xtrack=1&clicktime=1567346543&sessionid=1567346375&subscene=92&ascene=0&fasttmpl_type=0&fasttmpl_fullversion=4730859-zh_CN-zip&fasttmpl_flag=0&realreporttime=1567346543910#wechat_redirect)
|
||||

|
||||
|
||||
自定义按键映射操作方法如下:
|
||||
- 编写自定义脚本放入keymap目录
|
||||
- 编写自定义脚本放入 keymap 目录
|
||||
- 点击刷新脚本,确保脚本可以被检测到
|
||||
- 选择需要的脚本
|
||||
- 连接手机并启动服务之后,点击应用脚本
|
||||
- 按~键(数字键1左边)切换为自定义映射模式即可体验(具体按什么键要看你按键脚本定义的switchKey)
|
||||
- 按`~`(即脚本中定义的 SwitchKey)键切换为自定义映射模式即可启用
|
||||
- 再次按~键切换为正常控制模式
|
||||
- 要想wasd控制开车记得在载具设置中设置为单摇杆模式
|
||||
- (对于和平精英等游戏)若想使用方向盘控制载具,记得在载具设置中设置为单摇杆模式
|
||||
|
||||
## 群控
|
||||
如果不会自己手写映射规则,也可以去使用作者开发的`极限手游助手`
|
||||
极限手游助手功能&特点:
|
||||
- 通过键盘鼠标畅玩安卓手机游戏
|
||||
- 按键映射脚本界面化编辑
|
||||
- 支持暂停电脑端画面,只使用键鼠操作
|
||||
- 截图&录制手机画面
|
||||
- 简单批量控制
|
||||
- 安卓11+支持电脑播放手机音频(开发中...)
|
||||
- 手机端免安装App
|
||||
- 极速秒连接
|
||||
- 低延迟:usb投屏1080p延迟在30ms以内,在相同分辨率流畅度情况下,比市面上所有投屏软件延迟都低
|
||||
- cpu占用率低:纯C++开发,高性能GPU视频渲染
|
||||
- 高分辨率:可调节,最大支持安卓终端的原生分辨率
|
||||
- [QQ交流群:901736468](https://qm.qq.com/q/wRJJaWLWc8)
|
||||
- [极限手游助手说明文档](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh)
|
||||
|
||||
## 批量操作
|
||||
你可以同时控制所有的手机
|
||||
|
||||
## Star历史
|
||||
|
||||
[](https://star-history.com/#barry-ran/QtScrcpy&Date)
|
||||
|
||||

|
||||
|
||||
## 感谢
|
||||
|
||||
基于[Genymobile](https://github.com/Genymobile)的[scrcpy](https://github.com/Genymobile/scrcpy)项目进行复刻,重构,非常感谢。QtScrcpy和原版scrcpy区别如下:
|
||||
基于[Genymobile](https://github.com/Genymobile)的[scrcpy](https://github.com/Genymobile/scrcpy)项目进行复刻,重构,非常感谢。
|
||||
|
||||
## 比较
|
||||
QtScrcpy 和 Scrcpy 区别如下:
|
||||
关键点|scrcpy|QtScrcpy
|
||||
--|:--:|:--:
|
||||
界面|sdl|qt
|
||||
视频解码|ffmpeg|ffmpeg
|
||||
视频渲染|sdl|opengl
|
||||
跨平台基础设施|自己封装|Qt提供
|
||||
跨平台基础设施|自己封装|Qt
|
||||
编程语言|C|C++
|
||||
编程方式|同步|异步
|
||||
按键映射|不支持自定义|支持自定义按键映射
|
||||
编译方式|meson+gradle|qmake or CMake
|
||||
编译方式|Meson+Gradle|CMake
|
||||
|
||||
- 使用Qt可以非常容易的定制自己的界面
|
||||
- 基于Qt的信号槽机制的异步编程提高性能
|
||||
|
@ -76,111 +116,116 @@ QtScrcpy可以通过USB(或通过TCP/IP)连接Android设备,并进行显示和
|
|||
|
||||
## 学习它
|
||||
如果你对它感兴趣,想学习它的实现原理而又感觉无从下手,可以选择购买我录制的视频课程,
|
||||
里面详细介绍了整个软件的开发架构以及开发流程,带你从无到有的开发QtScrcpy:
|
||||
里面详细介绍了整个软件的开发架构以及开发流程,带你从无到有的开发 QtScrcpy:
|
||||
|
||||
课程介绍:[https://blog.csdn.net/rankun1/article/details/87970523](https://blog.csdn.net/rankun1/article/details/87970523)
|
||||
|
||||
或者你也可以加入我的QtScrcpy QQ群,和志同道合的朋友一块互相交流技术:
|
||||
或者你也可以加入我的 QtScrcpy QQ 群,和志同道合的朋友一块互相交流技术:
|
||||
|
||||
QQ群号:901736468
|
||||
|
||||
|
||||
## 要求
|
||||
Android部分至少需要API 21(Android 5.0)。
|
||||
Android 部分至少需要 API 21(Android 5.0)。
|
||||
|
||||
您要确保在Android设备上[启用adb调试][enable-adb]。
|
||||
您要确保在 Android 设备上[启用adb调试][enable-adb]。
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
|
||||
## 下载这个软件
|
||||
## 下载
|
||||
|
||||
[gitee-download]: https://gitee.com/Barryda/QtScrcpy/releases
|
||||
[github-download]: https://github.com/barry-ran/QtScrcpy/releases
|
||||
|
||||
### Windows
|
||||
|
||||
Windows平台,你可以直接使用我编译好的可执行程序:
|
||||
Windows 平台,你可以直接使用我编译好的可执行程序:
|
||||
|
||||
- [国内下载][gitee-download]
|
||||
- [国外下载][github-download]
|
||||
|
||||
你也可以[自己编译](##如何编译)
|
||||
你也可以[自己编译](##编译)
|
||||
|
||||
### Mac OS
|
||||
|
||||
Mac OS平台,你可以直接使用我编译好的可执行程序:
|
||||
Mac OS 平台,你可以直接使用我编译好的可执行程序:
|
||||
|
||||
- [国内下载][gitee-download]
|
||||
- [国外下载][github-download]
|
||||
|
||||
你也可以[自己编译](##如何编译)
|
||||
你也可以[自己编译](##编译)
|
||||
|
||||
### Linux
|
||||
|
||||
目前只提供了windows和mac平台的可执行程序,如果需要linux平台的可执行程序,
|
||||
对于 Arch Linux 用户,可以使用 AUR 安装:`yay -Syu qtscrcpy`(可能版本并非最新;维护者:[yochananmarqos](https://aur.archlinux.org/account/yochananmarqos))
|
||||
|
||||
您通常需要[自己编译](##如何编译)。别担心,这并不难。
|
||||
其他发行版的用户可以直接使用我编译好的可执行程序:
|
||||
|
||||
目前只在ubuntu上测试过
|
||||
- [国外下载][github-download]
|
||||
|
||||
你也可以从 [GitHub Actions](https://github.com/UjhhgtgTeams/QtScrcpy/actions/workflows/ubuntu.yml) 获取最新的自动编译好的软件
|
||||
|
||||
当然,你也可以[自己编译](##编译)(不推荐,需要准备环境)
|
||||
|
||||
目前只在 Ubuntu 和 Arch Linux 上测试过编译过程
|
||||
|
||||
## 运行
|
||||
在你的电脑上接入Android设备,然后运行程序,点击`一键USB连接`或者`一键WIFI连接`
|
||||
在你的电脑上接入Android设备,然后运行程序,点击 `一键USB连接` 或者 `一键WIFI连接`
|
||||
|
||||
### 无线连接步骤(保证手机和电脑在同一个局域网):
|
||||
1. 安卓手机端在开发者选项中打开usb调试
|
||||
2. 通过usb连接安卓手机到电脑
|
||||
3. 点击刷新设备,会看到有设备号更新出来
|
||||
4. 点击获取设备IP
|
||||
5. 点击启动adbd
|
||||
6. 无线连接
|
||||
7. 再次点击刷新设备,发现多出了一个IP地址开头的设备,选择这个设备
|
||||
8. 启动服务
|
||||
### 无线连接步骤
|
||||
1. 将手机和电脑连接到同一局域网
|
||||
2. 安卓手机端在开发者选项中打开 USB 调试
|
||||
3. 通过 USB 连接安卓手机到电脑
|
||||
4. 点击刷新设备,会看到有设备号更新出来
|
||||
5. 点击获取设备 IP
|
||||
6. 点击启动 adbd
|
||||
7. 无线连接
|
||||
8. 再次点击刷新设备,发现多出了一个 IP 地址开头的设备,选择这个设备
|
||||
9. 启动服务
|
||||
|
||||
备注:启动adbd以后不用再连着usb线了,以后连接断开都不再需要,除非安卓adbd停了需要重新启动
|
||||
备注:启动 adbd 以后无需继续连接 USB 线,以后连接断开都不再需要,除非 adbd 停止运行
|
||||
|
||||
## 界面按钮介绍:
|
||||
## 界面解释
|
||||
|
||||
- 启动配置:启动服务前的功能参数设置
|
||||
|
||||
分别可以设置本地录制视频的比特率、分辨率、录制格式、录像保存路径等。
|
||||
|
||||
- 仅后台录制:启动服务不现实界面,只是录制Android设备屏幕
|
||||
- 窗口置顶:Android设备视频窗口置顶显示
|
||||
- 自动息屏:启动服务以后,自动关闭Android设备屏幕节省电量
|
||||
- 使用reverse:服务启动模式,出现服务启动失败报错more than one device可以去掉这个勾选尝试连接
|
||||
- 仅后台录制:启动服务不显示界面,只录制 Android 设备屏幕
|
||||
- 窗口置顶:Android 设备显示窗口置顶
|
||||
- 自动息屏:启动服务以后,自动关闭 Android 设备屏幕以节省电量
|
||||
- 使用 Reverse:服务启动模式,出现服务启动失败报错 "more than one device" 可以去掉这个勾选尝试连接
|
||||
|
||||
- 刷新设备列表:刷新当前连接的设备
|
||||
- 启动服务:连接到Android设备
|
||||
- 停止服务:断开与Android设备的连接
|
||||
- 停止所有服务:断开所有已连接的Android设备
|
||||
- 获取设备ip:获取到Android设备的ip地址,更新到“无线”区域中,方便进行无线连接
|
||||
- 启动adbd:启动Android设备的adbd服务,无线连接之前,必须要启动。
|
||||
- 无线连接:使用无线方式连接Android设备
|
||||
- 无线断开:断开无线方式连接的Android设备
|
||||
- adb命令行:方便执行自定义adb命令(目前不支持阻塞命令,例如shell)
|
||||
- 启动服务:连接到 Android 设备
|
||||
- 停止服务:断开与 Android 设备的连接
|
||||
- 停止所有服务:断开所有已连接的 Android 设备
|
||||
- 获取设备ip:获取到 Android 设备的 IP 地址,更新到无线区域中,方便进行无线连接
|
||||
- 启动adbd:启动 Android 设备的 adbd 服务,无线连接之前,必须要启动
|
||||
- 无线连接:使用无线方式连接 Android 设备
|
||||
- 无线断开:断开无线方式连接的 Android 设备
|
||||
- 命令行:执行自定义 adb 命令(目前不支持阻塞命令,例如shell)
|
||||
|
||||
|
||||
## 主要功能
|
||||
- 实时显示Android设备屏幕
|
||||
## 功能
|
||||
- 实时显示 Android 设备屏幕
|
||||
- 实时键鼠控制Android设备
|
||||
- 屏幕录制
|
||||
- 截图为png
|
||||
- 截图
|
||||
- 无线连接
|
||||
- 支持多台设备连接
|
||||
- 多设备连接与批量操作
|
||||
- 全屏显示
|
||||
- 窗口置顶
|
||||
- 安装apk:拖拽apk到视频窗口即可安装
|
||||
- 传输文件:拖拽文件到视频窗口即可发送文件到Android设备
|
||||
- 后台录制:只录制,不显示界面
|
||||
- 复制粘贴
|
||||
|
||||
在计算机和设备之间双向同步剪贴板:
|
||||
- `Ctrl` + `c`将设备剪贴板复制到计算机剪贴板;
|
||||
- `Ctrl` + `Shift` + `v`将计算机剪贴板复制到设备剪贴板;
|
||||
- `Ctrl` +`v` 将计算机剪贴板作为一系列文本事件发送到设备(不支持非ASCII字符)。
|
||||
- 群控
|
||||
- 同步设备扬声器声音到电脑(基于[sndcpy](https://github.com/rom1v/sndcpy),仅支持安卓10+)
|
||||
- 安装 apk:拖拽apk到显示窗口即可安装
|
||||
- 传输文件:拖拽文件到显示窗口即可发送文件到 Android 设备
|
||||
- 后台录制:只录制屏幕,不显示界面
|
||||
- 剪贴板同步:
|
||||
在计算机和设备之间同步剪贴板:
|
||||
- `Ctrl + c`将设备剪贴板复制到计算机剪贴板;
|
||||
- `Ctrl + Shift + v`将计算机剪贴板复制到设备剪贴板;
|
||||
- `Ctrl + v` 将计算机剪贴板作为一系列文本事件发送到设备(不支持非ASCII字符)
|
||||
- 同步设备扬声器声音到电脑(基于[sndcpy](https://github.com/rom1v/sndcpy),仅支持安卓10级以上,目前不推荐使用,可使用蓝牙连接替代)
|
||||
|
||||
## 快捷键
|
||||
|
||||
|
@ -216,41 +261,49 @@ Mac OS平台,你可以直接使用我编译好的可执行程序:
|
|||
[常见问题说明](docs/FAQ.md)
|
||||
|
||||
## 开发者
|
||||
[开发者相关](docs/DEVELOP.md)
|
||||
[开发相关](docs/DEVELOP.md)
|
||||
|
||||
欢迎大家一起维护这个项目,贡献自己的代码,不过请遵循一下几点要求:
|
||||
1. pr请提到dev分支,不要提到master分支
|
||||
2. 提pr之前请先rebase dev
|
||||
3. pr请以少量多次的原则提交(建议一个小的功能点提一个pr)
|
||||
4. 代码风格请保持和已有风格一致
|
||||
欢迎大家一起维护这个项目,贡献自己的代码,不过请遵循以下几点要求:
|
||||
1. PR 请推向 dev 分支,不要推向 master 分支
|
||||
2. 提交 PR 之前请先变基原项目
|
||||
3. PR 请以少量多次的原则提交(即一个功能点提交一个 PR)
|
||||
4. 代码风格请保持和原有风格一致
|
||||
|
||||
## 为什么开发QtScrcpy?
|
||||
## 为什么开发 QtScrcpy?
|
||||
综合起来有以下几个原因,比重从大到小排列:
|
||||
1. 学习Qt的过程中需要一个项目实战一下
|
||||
2. 本身具有音视频相关技能,对音视频很感兴趣
|
||||
3. 本身具有Android开发技能,好久没用有点生疏,需要巩固一下
|
||||
4. 发现了scrcpy,决定用新的技术栈(C++ + Qt + Opengl + ffmpeg)复刻一下
|
||||
3. 本身具有 Android 开发技能,好久没用有点生疏,需要巩固一下
|
||||
4. 发现了 Scrcpy,决定用新的技术栈(C++ + Qt + Opengl + FFmpeg)进行复刻
|
||||
|
||||
|
||||
## 如何编译
|
||||
## 编译
|
||||
尽量提供了所有依赖资源,方便傻瓜式编译。
|
||||
|
||||
### PC端
|
||||
1. 在目标平台上搭建Qt开发环境
|
||||
Qt版本>=5.12(在Windows上使用MSVC 2019)
|
||||
2. 克隆该项目(git clone --recursive git@github.com:barry-ran/QtScrcpy.git)
|
||||
3. 使用QtCreator打开项目根目录`CMakeLists.txt`
|
||||
4. 编译,运行
|
||||
### QtScrcpy
|
||||
#### 非 Arch Linux
|
||||
1. 使用官方 Qt Installer 或非官方工具(如 [aqt](https://github.com/miurahr/aqtinstall))在目标平台上搭建Qt开发环境。
|
||||
需要 5.12 以上版本 Qt(在 Windows 上使用 MSVC 2019)
|
||||
2. 克隆该项目:`git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git`
|
||||
3. Windows 使用 QtCreator 打开项目下 CMakeLists.txt 并编译 Release
|
||||
4. Linux 用终端执行 `./ci/linux/build_for_linux.sh "Release"`
|
||||
注:编译结果位于 `output/x64/Release` 中
|
||||
|
||||
### Android端 (没有修改需求的话直接使用自带的scrcpy-server即可)
|
||||
1. 目标平台上搭建Android开发环境
|
||||
2. 使用Android Studio打开项目根目录中的server项目
|
||||
3. 第一次打开如果你没有对应版本的gradle会提示找不到gradle,是否升级gradle并创建,选择取消,取消后会弹出选择已有gradle的位置,同样取消即可(会自动下载)
|
||||
4. 按需编辑代码即可,当然也可以不编辑
|
||||
4. 编译出apk以后改名为scrcpy-server并替换third_party/scrcpy-server即可
|
||||
#### Arch Linux
|
||||
1. 安装以下包:`qt5-base qt5-multimedia qt5-x11extras`(推荐安装 `qtcreator`)
|
||||
2. 克隆该项目:`git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git`
|
||||
3. 用终端执行 `./ci/linux/build_for_linux.sh "Release"`
|
||||
注:编译结果位于 `output/x64/Release` 中
|
||||
|
||||
### Scrcpy-Server
|
||||
1. 目标平台上搭建 Android 开发环境
|
||||
2. 使用 Android Studio 打开项目根目录中的 server
|
||||
3. 第一次打开时,如果你没有对应版本的 Gradle,Studio 会提示找不到 Gradle,是否升级 Gradle 并创建,选择取消,取消后会提示选择 Gradle 的位置,同样取消即可。Studio 会随后自动下载。
|
||||
4. 按需编辑代码
|
||||
5. 编译出 apk 以后改名为 scrcpy-server 并替换 `third_party/scrcpy-server` 即可
|
||||
|
||||
## Licence
|
||||
由于是复刻的scrcpy,尊重它的Licence
|
||||
由于是复刻的 Scrcpy,尊重它的 Licence
|
||||
|
||||
Copyright (C) 2025 Rankun
|
||||
|
||||
|
@ -268,6 +321,6 @@ Qt版本>=5.12(在Windows上使用MSVC 2019)
|
|||
|
||||
## 关于作者
|
||||
|
||||
[Barry的CSDN](https://blog.csdn.net/rankun1)
|
||||
[Barry 的 CSDN](https://blog.csdn.net/rankun1)
|
||||
|
||||
一枚普通的程序员,工作中主要使用C++进行桌面客户端开发,一毕业在山东做过一年多钢铁仿真教育软件,后来转战上海先后从事安防,在线教育相关领域工作,对音视频比较熟悉,对音视频领域如语音通话,直播教育,视频会议等相关解决方案有所了解。同时具有Android,Linux服务器等开发经验。
|
||||
一枚普通的程序员,工作中主要使用 C++ 进行桌面客户端开发,一毕业在山东做过一年多钢铁仿真教育软件,后来转战上海先后从事安防,在线教育相关领域工作,对音视频比较熟悉,对音视频领域如语音通话,直播教育,视频会议等相关解决方案有所了解。同时具有Android,Linux服务器等开发经验。
|
||||
|
|
|
@ -13,7 +13,7 @@ if __name__ == '__main__':
|
|||
# print('get tag:', tag)
|
||||
|
||||
version = str(tag[1:])
|
||||
version_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "../QtScrcpy/version"))
|
||||
version_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "../QtScrcpy/appversion"))
|
||||
file=open(version_file, 'w')
|
||||
file.write(version)
|
||||
file.close()
|
||||
|
|
67
ci/linux/build_for_linux.sh
Executable file
67
ci/linux/build_for_linux.sh
Executable file
|
@ -0,0 +1,67 @@
|
|||
echo ---------------------------------------------------------------
|
||||
echo Check \& Set Environment Variables
|
||||
echo ---------------------------------------------------------------
|
||||
|
||||
# Get Qt path
|
||||
# ENV_QT_PATH example: /home/barry/Qt5.9.6/5.9.6
|
||||
echo Current ENV_QT_PATH: $ENV_QT_PATH
|
||||
echo Current directory: $(pwd)
|
||||
# Set variables
|
||||
qt_cmake_path=$ENV_QT_PATH/gcc_64/lib/cmake/Qt5
|
||||
export PATH=$qt_gcc_path/bin:$PATH
|
||||
|
||||
# Remember working directory
|
||||
old_cd=$(pwd)
|
||||
|
||||
# Set working dir to the script's path
|
||||
cd $(dirname "$0")/.../
|
||||
|
||||
echo
|
||||
echo
|
||||
echo ---------------------------------------------------------------
|
||||
echo Check Build Parameters
|
||||
echo ---------------------------------------------------------------
|
||||
echo Possible build modes: Debug/Release/MinSizeRel/RelWithDebInfo
|
||||
|
||||
build_mode="$1"
|
||||
if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinSizeRel" && $build_mode != "RelWithDebInfo" ]]; then
|
||||
echo "error: unknown build mode, exiting......"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo Current build mode: $build_mode
|
||||
|
||||
echo
|
||||
echo
|
||||
echo ---------------------------------------------------------------
|
||||
echo CMake Build Begins
|
||||
echo ---------------------------------------------------------------
|
||||
|
||||
# Remove output folder
|
||||
output_path=./output
|
||||
if [ -d "$output_path" ]; then
|
||||
rm -rf $output_path
|
||||
fi
|
||||
|
||||
cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode"
|
||||
cmake $cmake_params .
|
||||
if [ $? -ne 0 ] ;then
|
||||
echo "error: CMake failed, exiting......"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmake --build . --config "$build_mode" -j8
|
||||
if [ $? -ne 0 ] ;then
|
||||
echo "error: CMake build failed, exiting......"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo
|
||||
echo ---------------------------------------------------------------
|
||||
echo CMake Build Succeeded
|
||||
echo ---------------------------------------------------------------
|
||||
|
||||
# Resume current directory
|
||||
cd $old_cd
|
||||
exit 0
|
|
@ -1,85 +0,0 @@
|
|||
|
||||
echo
|
||||
echo
|
||||
echo ---------------------------------------------------------------
|
||||
echo check ENV
|
||||
echo ---------------------------------------------------------------
|
||||
|
||||
# 从环境变量获取必要参数
|
||||
# 例如 /home/barry/Qt5.9.6/5.9.6
|
||||
echo ENV_QT_PATH $ENV_QT_PATH
|
||||
qt_cmake_path=$ENV_QT_PATH/gcc_64/lib/cmake/Qt5
|
||||
|
||||
# 获取绝对路径,保证其他目录执行此脚本依然正确
|
||||
{
|
||||
cd $(dirname "$0")
|
||||
script_path=$(pwd)
|
||||
cd -
|
||||
} &> /dev/null # disable output
|
||||
# 设置当前目录,cd的目录影响接下来执行程序的工作目录
|
||||
old_cd=$(pwd)
|
||||
cd $(dirname "$0")
|
||||
|
||||
# 启动参数声明
|
||||
build_mode=RelWithDebInfo
|
||||
|
||||
echo
|
||||
echo
|
||||
echo ---------------------------------------------------------------
|
||||
echo check build param[Debug/Release/MinSizeRel/RelWithDebInfo]
|
||||
echo ---------------------------------------------------------------
|
||||
|
||||
# 编译参数检查
|
||||
build_mode=$(echo $1)
|
||||
if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinSizeRel" && $build_mode != "RelWithDebInfo" ]]; then
|
||||
echo "error: unkonow build mode -- $1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 提示
|
||||
echo current build mode: $build_mode
|
||||
|
||||
# 环境变量设置
|
||||
#export PATH=$qt_gcc_path/bin:$PATH
|
||||
|
||||
echo
|
||||
echo
|
||||
echo ---------------------------------------------------------------
|
||||
echo begin cmake build
|
||||
echo ---------------------------------------------------------------
|
||||
|
||||
# 删除输出目录
|
||||
output_path=$script_path../../output
|
||||
if [ -d "$output_path" ]; then
|
||||
rm -rf $output_path
|
||||
fi
|
||||
# 删除临时目录
|
||||
build_path=$script_path/../build_temp
|
||||
if [ -d "$build_path" ]; then
|
||||
rm -rf $build_path
|
||||
fi
|
||||
mkdir $build_path
|
||||
cd $build_path
|
||||
|
||||
cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode"
|
||||
cmake $cmake_params ../..
|
||||
if [ $? -ne 0 ] ;then
|
||||
echo "cmake failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmake --build . --config $build_mode -j8
|
||||
if [ $? -ne 0 ] ;then
|
||||
echo "cmake build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo
|
||||
echo ---------------------------------------------------------------
|
||||
echo finish!!!
|
||||
echo ---------------------------------------------------------------
|
||||
|
||||
# 恢复当前目录
|
||||
cd $old_cd
|
||||
exit 0
|
|
@ -8,7 +8,6 @@ echo ---------------------------------------------------------------
|
|||
# 从环境变量获取必要参数
|
||||
# 例如 /Users/barry/Qt5.12.5/5.12.5
|
||||
echo ENV_QT_PATH $ENV_QT_PATH
|
||||
qt_cmake_path=$ENV_QT_PATH/clang_64/lib/cmake/Qt5
|
||||
|
||||
# 获取绝对路径,保证其他目录执行此脚本依然正确
|
||||
{
|
||||
|
@ -22,6 +21,7 @@ cd $(dirname "$0")
|
|||
|
||||
# 启动参数声明
|
||||
build_mode=RelWithDebInfo
|
||||
cpu_arch=arm64
|
||||
|
||||
echo
|
||||
echo
|
||||
|
@ -36,8 +36,30 @@ if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinS
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo
|
||||
echo ---------------------------------------------------------------
|
||||
echo check cpu arch[x64/arm64]
|
||||
echo ---------------------------------------------------------------
|
||||
|
||||
cpu_arch=$(echo $2)
|
||||
if [[ $cpu_arch != "x64" && $cpu_arch != "arm64" ]]; then
|
||||
echo "error: unkonow cpu mode -- $2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 提示
|
||||
echo current build mode: $build_mode
|
||||
echo current cpu mode: $cpu_arch
|
||||
|
||||
cmake_arch=x86_64
|
||||
if [ $cpu_arch == "x64" ]; then
|
||||
qt_cmake_path=$ENV_QT_PATH/clang_64/lib/cmake/Qt5
|
||||
cmake_arch=x86_64
|
||||
else
|
||||
qt_cmake_path=$ENV_QT_PATH/macos/lib/cmake/Qt6
|
||||
cmake_arch=arm64
|
||||
fi
|
||||
|
||||
echo
|
||||
echo
|
||||
|
@ -58,7 +80,7 @@ fi
|
|||
mkdir $build_path
|
||||
cd $build_path
|
||||
|
||||
cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode -G Xcode"
|
||||
cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode -DCMAKE_OSX_ARCHITECTURES=$cmake_arch"
|
||||
cmake $cmake_params ../..
|
||||
if [ $? -ne 0 ] ;then
|
||||
echo "cmake failed"
|
||||
|
|
|
@ -7,7 +7,6 @@ echo ---------------------------------------------------------------
|
|||
# 从环境变量获取必要参数
|
||||
# 例如 /Users/barry/Qt5.12.5/5.12.5
|
||||
echo ENV_QT_PATH $ENV_QT_PATH
|
||||
qt_clang_path=$ENV_QT_PATH/clang_64
|
||||
|
||||
# 获取绝对路径,保证其他目录执行此脚本依然正确
|
||||
{
|
||||
|
@ -21,6 +20,27 @@ cd $(dirname "$0")
|
|||
|
||||
# 启动参数声明
|
||||
publish_dir=$1
|
||||
cpu_arch=$2
|
||||
|
||||
echo
|
||||
echo
|
||||
echo ---------------------------------------------------------------
|
||||
echo check cpu arch[x64/arm64]
|
||||
echo ---------------------------------------------------------------
|
||||
|
||||
if [[ $cpu_arch != "x64" && $cpu_arch != "arm64" ]]; then
|
||||
echo "error: unkonow cpu mode -- $2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 提示
|
||||
echo current cpu mode: $cpu_arch
|
||||
|
||||
if [ $cpu_arch == "x64" ]; then
|
||||
qt_clang_path=$ENV_QT_PATH/clang_64
|
||||
else
|
||||
qt_clang_path=$ENV_QT_PATH/macos
|
||||
fi
|
||||
|
||||
# 提示
|
||||
echo current publish dir: $publish_dir
|
||||
|
@ -30,7 +50,7 @@ keymap_path=$script_path/../../keymap
|
|||
# config_path=$script_path/../../config
|
||||
|
||||
publish_path=$script_path/$publish_dir
|
||||
release_path=$script_path/../../output/x64/RelWithDebInfo
|
||||
release_path=$script_path/../../output/$cpu_arch/RelWithDebInfo
|
||||
|
||||
export PATH=$qt_clang_path/bin:$PATH
|
||||
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
[common]
|
||||
# 语言 Auto=自动,zh_CN=简体中文,en_US=English
|
||||
Language=Auto
|
||||
# 窗口标题
|
||||
WindowTitle=QtScrcpy
|
||||
# 推送到安卓设备的文件保存路径(必须以/结尾)
|
||||
PushFilePath=/sdcard/
|
||||
# 最大fps(仅支持Android 10以上)
|
||||
MaxFps=60
|
||||
MaxFps=0
|
||||
# 是否渲染过期视频帧(跳过过期视频帧意味着更低的延迟)
|
||||
RenderExpiredFrames=0
|
||||
# 视频解码方式:-1 自动,0 软解,1 dx硬解,2 opengl硬解
|
||||
UseDesktopOpenGL=-1
|
||||
# scrcpy-server的版本号(不要修改)
|
||||
ServerVersion=1.21
|
||||
ServerVersion=3.1
|
||||
# scrcpy-server推送到安卓设备的路径
|
||||
ServerPath=/data/local/tmp/scrcpy-server.jar
|
||||
# 自定义adb路径,例如D:/android/tools/adb.exe
|
||||
|
@ -23,5 +25,5 @@ CodecOptions=""
|
|||
# 例如 CodecName="OMX.qcom.video.encoder.avc"
|
||||
CodecName=""
|
||||
|
||||
# Set the log level (debug, info, warn, error)
|
||||
LogLevel=info
|
||||
# Set the log level (verbose, debug, info, warn, error)
|
||||
LogLevel=verbose
|
||||
|
|
|
@ -60,6 +60,8 @@ Description of the unique attributes of different key mapping types:
|
|||
-key The key code to be mapped
|
||||
-startPos Simulate the start position of touch drag
|
||||
-endPos Simulate the end position of touch drag
|
||||
-dragSpeed Speed of the drag movement (range 0-1, default 1.0). Higher values result in faster movements
|
||||
-startDelay Optional delay in milliseconds to wait after the initial touch before starting the drag movement
|
||||
|
||||
-KMT_STEER_WHEEL
|
||||
-centerPos steering wheel center point
|
||||
|
@ -71,3 +73,22 @@ Description of the unique attributes of different key mapping types:
|
|||
-rightOffset After pressing the right direction key, drag it to the right offset of the center to the right of the centerPos position
|
||||
-upOffset After pressing the up arrow key, drag it to the upper offset position horizontally relative to the centerPos position
|
||||
-downOffset Press the down arrow key and drag it to the downOffset position horizontally relative to the centerPos position
|
||||
|
||||
## Visual Key Mapping Tool
|
||||
|
||||
1. Just use [QuickAssistant](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh)
|
||||
|
||||

|
||||
|
||||
2. A web-based GUI tool is available to help you create and manage key mappings visually: [ScrcpyKeyMapper](https://github.com/w4po/ScrcpyKeyMapper)
|
||||
|
||||

|
||||
|
||||
You can use this tool to:
|
||||
- Create key mappings visually
|
||||
- Test your mappings in real-time
|
||||
- Export mappings as JSON files
|
||||
- Import existing mappings for editing
|
||||
|
||||
Try it online: [ScrcpyKeyMapper Web App](https://w4po.github.io/ScrcpyKeyMapper)
|
||||
|
||||
|
|
|
@ -60,6 +60,8 @@
|
|||
- key 要映射的按键码
|
||||
- startPos 模拟触摸拖动的开始位置
|
||||
- endPos 模拟触摸拖动的结束位置
|
||||
- dragSpeed 拖动移动的速度(范围0-1,默认1.0)。数值越大,移动越快
|
||||
- startDelay 可选的延迟时间(毫秒),在开始拖动移动之前等待指定的时间
|
||||
|
||||
- KMT_STEER_WHEEL
|
||||
- centerPos 方向盘中心点
|
||||
|
@ -72,5 +74,19 @@
|
|||
- upOffset 按下上方向键后模拟拖动到相对centerPos位置水平偏上upOffset处
|
||||
- downOffset 按下下方向键后模拟拖动到相对centerPos位置水平偏下downOffset处
|
||||
|
||||
## 可视化按键映射工具
|
||||
1. 直接使用[QuickAssistant](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh)
|
||||
|
||||
|
||||

|
||||
|
||||
2. 还有一个基于Web的GUI工具可以帮助你直观地创建和管理按键映射:[ScrcpyKeyMapper](https://github.com/w4po/ScrcpyKeyMapper)
|
||||
|
||||

|
||||
|
||||
你可以使用这个工具来:
|
||||
- 直观地创建按键映射
|
||||
- 实时测试你的映射
|
||||
- 导出映射为JSON文件
|
||||
- 导入现有映射进行编辑
|
||||
|
||||
在线试用:[ScrcpyKeyMapper网页应用](https://w4po.github.io/ScrcpyKeyMapper)
|
||||
|
|
BIN
docs/image/quickmirror.png
Normal file
BIN
docs/image/quickmirror.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
Before Width: | Height: | Size: 60 KiB |
BIN
screenshot/game.png
Normal file
BIN
screenshot/game.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 823 KiB |
8
server/.gitignore
vendored
8
server/.gitignore
vendored
|
@ -1,8 +0,0 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
|
@ -1,26 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
defaultConfig {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
versionCode 12100
|
||||
versionName "1.21"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
}
|
||||
|
||||
apply from: "$project.rootDir/config/android-checkstyle.gradle"
|
|
@ -1,88 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# This script generates the scrcpy binary "manually" (without gradle).
|
||||
#
|
||||
# Adapt Android platform and build tools versions (via ANDROID_PLATFORM and
|
||||
# ANDROID_BUILD_TOOLS environment variables).
|
||||
#
|
||||
# Then execute:
|
||||
#
|
||||
# BUILD_DIR=my_build_dir ./build_without_gradle.sh
|
||||
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=1.21
|
||||
|
||||
PLATFORM_VERSION=31
|
||||
PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0}
|
||||
|
||||
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
||||
CLASSES_DIR="$BUILD_DIR/classes"
|
||||
SERVER_DIR=$(dirname "$0")
|
||||
SERVER_BINARY=scrcpy-server
|
||||
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
|
||||
|
||||
echo "Platform: android-$PLATFORM"
|
||||
echo "Build-tools: $BUILD_TOOLS"
|
||||
echo "Build dir: $BUILD_DIR"
|
||||
|
||||
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
|
||||
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
|
||||
|
||||
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
public final class BuildConfig {
|
||||
public static final boolean DEBUG = $SCRCPY_DEBUG;
|
||||
public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME";
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Generating java from aidl..."
|
||||
cd "$SERVER_DIR/src/main/aidl"
|
||||
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
|
||||
android/view/IRotationWatcher.aidl
|
||||
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
|
||||
android/content/IOnPrimaryClipChangedListener.aidl
|
||||
|
||||
echo "Compiling java sources..."
|
||||
cd ../java
|
||||
javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \
|
||||
-source 1.8 -target 1.8 \
|
||||
com/genymobile/scrcpy/*.java \
|
||||
com/genymobile/scrcpy/wrappers/*.java
|
||||
|
||||
echo "Dexing..."
|
||||
cd "$CLASSES_DIR"
|
||||
|
||||
if [[ $PLATFORM_VERSION -lt 31 ]]
|
||||
then
|
||||
# use dx
|
||||
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
|
||||
--output "$BUILD_DIR/classes.dex" \
|
||||
android/view/*.class \
|
||||
android/content/*.class \
|
||||
com/genymobile/scrcpy/*.class \
|
||||
com/genymobile/scrcpy/wrappers/*.class
|
||||
|
||||
echo "Archiving..."
|
||||
cd "$BUILD_DIR"
|
||||
jar cvf "$SERVER_BINARY" classes.dex
|
||||
rm -rf classes.dex classes
|
||||
else
|
||||
# use d8
|
||||
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \
|
||||
--output "$BUILD_DIR/classes.zip" \
|
||||
android/view/*.class \
|
||||
android/content/*.class \
|
||||
com/genymobile/scrcpy/*.class \
|
||||
com/genymobile/scrcpy/wrappers/*.class
|
||||
|
||||
cd "$BUILD_DIR"
|
||||
mv classes.zip "$SERVER_BINARY"
|
||||
rm -rf classes
|
||||
fi
|
||||
|
||||
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"
|
|
@ -1,28 +0,0 @@
|
|||
apply plugin: 'checkstyle'
|
||||
check.dependsOn 'checkstyle'
|
||||
|
||||
checkstyle {
|
||||
toolVersion = '6.19'
|
||||
}
|
||||
|
||||
task checkstyle(type: Checkstyle) {
|
||||
description = "Check Java style with Checkstyle"
|
||||
configFile = rootProject.file("config/checkstyle/checkstyle.xml")
|
||||
source = javaSources()
|
||||
classpath = files()
|
||||
ignoreFailures = true
|
||||
}
|
||||
|
||||
def javaSources() {
|
||||
def files = []
|
||||
android.sourceSets.each { sourceSet ->
|
||||
sourceSet.java.each { javaSource ->
|
||||
javaSource.getSrcDirs().each {
|
||||
if (it.exists()) {
|
||||
files.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
# It may be useful to use a prebuilt server, so that no Android SDK is required
|
||||
# to build. If the 'prebuilt_server' option is set, just copy the file as is.
|
||||
prebuilt_server = get_option('prebuilt_server')
|
||||
if prebuilt_server == ''
|
||||
custom_target('scrcpy-server',
|
||||
# gradle is responsible for tracking source changes
|
||||
build_by_default: true,
|
||||
build_always_stale: true,
|
||||
output: 'scrcpy-server',
|
||||
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
|
||||
console: true,
|
||||
install: true,
|
||||
install_dir: 'share/scrcpy')
|
||||
else
|
||||
if not prebuilt_server.startswith('/')
|
||||
# relative path needs some trick
|
||||
prebuilt_server = meson.source_root() + '/' + prebuilt_server
|
||||
endif
|
||||
custom_target('scrcpy-server-prebuilt',
|
||||
input: prebuilt_server,
|
||||
output: 'scrcpy-server',
|
||||
command: ['cp', '@INPUT@', '@OUTPUT@'],
|
||||
install: true,
|
||||
install_dir: 'share/scrcpy')
|
||||
endif
|
21
server/proguard-rules.pro
vendored
21
server/proguard-rules.pro
vendored
|
@ -1,21 +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
|
|
@ -1,2 +0,0 @@
|
|||
<!-- not a real Android application, it is run by app_process manually -->
|
||||
<manifest package="com.genymobile.scrcpy"/>
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2008, The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.content;
|
||||
|
||||
/**
|
||||
* {@hide}
|
||||
*/
|
||||
oneway interface IOnPrimaryClipChangedListener {
|
||||
void dispatchPrimaryClipChanged();
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/* //device/java/android/android/hardware/ISensorListener.aidl
|
||||
**
|
||||
** Copyright 2008, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
|
||||
package android.view;
|
||||
|
||||
/**
|
||||
* {@hide}
|
||||
*/
|
||||
interface IRotationWatcher {
|
||||
oneway void onRotationChanged(int rotation);
|
||||
}
|
|
@ -1,197 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Base64;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Handle the cleanup of scrcpy, even if the main process is killed.
|
||||
* <p>
|
||||
* This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process).
|
||||
*/
|
||||
public final class CleanUp {
|
||||
|
||||
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
|
||||
|
||||
// A simple struct to be passed from the main process to the cleanup process
|
||||
public static class Config implements Parcelable {
|
||||
|
||||
public static final Creator<Config> CREATOR = new Creator<Config>() {
|
||||
@Override
|
||||
public Config createFromParcel(Parcel in) {
|
||||
return new Config(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config[] newArray(int size) {
|
||||
return new Config[size];
|
||||
}
|
||||
};
|
||||
|
||||
private static final int FLAG_DISABLE_SHOW_TOUCHES = 1;
|
||||
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2;
|
||||
private static final int FLAG_POWER_OFF_SCREEN = 4;
|
||||
|
||||
private int displayId;
|
||||
|
||||
// Restore the value (between 0 and 7), -1 to not restore
|
||||
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
|
||||
private int restoreStayOn = -1;
|
||||
|
||||
private boolean disableShowTouches;
|
||||
private boolean restoreNormalPowerMode;
|
||||
private boolean powerOffScreen;
|
||||
|
||||
public Config() {
|
||||
// Default constructor, the fields are initialized by CleanUp.configure()
|
||||
}
|
||||
|
||||
protected Config(Parcel in) {
|
||||
displayId = in.readInt();
|
||||
restoreStayOn = in.readInt();
|
||||
byte options = in.readByte();
|
||||
disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0;
|
||||
restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0;
|
||||
powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(displayId);
|
||||
dest.writeInt(restoreStayOn);
|
||||
byte options = 0;
|
||||
if (disableShowTouches) {
|
||||
options |= FLAG_DISABLE_SHOW_TOUCHES;
|
||||
}
|
||||
if (restoreNormalPowerMode) {
|
||||
options |= FLAG_RESTORE_NORMAL_POWER_MODE;
|
||||
}
|
||||
if (powerOffScreen) {
|
||||
options |= FLAG_POWER_OFF_SCREEN;
|
||||
}
|
||||
dest.writeByte(options);
|
||||
}
|
||||
|
||||
private boolean hasWork() {
|
||||
return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte[] serialize() {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
writeToParcel(parcel, 0);
|
||||
byte[] bytes = parcel.marshall();
|
||||
parcel.recycle();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static Config deserialize(byte[] bytes) {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
parcel.unmarshall(bytes, 0, bytes.length);
|
||||
parcel.setDataPosition(0);
|
||||
return CREATOR.createFromParcel(parcel);
|
||||
}
|
||||
|
||||
static Config fromBase64(String base64) {
|
||||
byte[] bytes = Base64.decode(base64, Base64.NO_WRAP);
|
||||
return deserialize(bytes);
|
||||
}
|
||||
|
||||
String toBase64() {
|
||||
byte[] bytes = serialize();
|
||||
return Base64.encodeToString(bytes, Base64.NO_WRAP);
|
||||
}
|
||||
}
|
||||
|
||||
private CleanUp() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen)
|
||||
throws IOException {
|
||||
Config config = new Config();
|
||||
config.displayId = displayId;
|
||||
config.disableShowTouches = disableShowTouches;
|
||||
config.restoreStayOn = restoreStayOn;
|
||||
config.restoreNormalPowerMode = restoreNormalPowerMode;
|
||||
config.powerOffScreen = powerOffScreen;
|
||||
|
||||
if (config.hasWork()) {
|
||||
startProcess(config);
|
||||
} else {
|
||||
// There is no additional clean up to do when scrcpy dies
|
||||
unlinkSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private static void startProcess(Config config) throws IOException {
|
||||
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
builder.environment().put("CLASSPATH", SERVER_PATH);
|
||||
builder.start();
|
||||
}
|
||||
|
||||
private static void unlinkSelf() {
|
||||
try {
|
||||
new File(SERVER_PATH).delete();
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not unlink server", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
unlinkSelf();
|
||||
|
||||
try {
|
||||
// Wait for the server to die
|
||||
System.in.read();
|
||||
} catch (IOException e) {
|
||||
// Expected when the server is dead
|
||||
}
|
||||
|
||||
Ln.i("Cleaning up");
|
||||
|
||||
Config config = Config.fromBase64(args[0]);
|
||||
|
||||
if (config.disableShowTouches || config.restoreStayOn != -1) {
|
||||
ServiceManager serviceManager = new ServiceManager();
|
||||
Settings settings = new Settings(serviceManager);
|
||||
if (config.disableShowTouches) {
|
||||
Ln.i("Disabling \"show touches\"");
|
||||
try {
|
||||
settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not restore \"show_touches\"", e);
|
||||
}
|
||||
}
|
||||
if (config.restoreStayOn != -1) {
|
||||
Ln.i("Restoring \"stay awake\"");
|
||||
try {
|
||||
settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Device.isScreenOn()) {
|
||||
if (config.powerOffScreen) {
|
||||
Ln.i("Power off screen");
|
||||
Device.powerOffScreen(config.displayId);
|
||||
} else if (config.restoreNormalPowerMode) {
|
||||
Ln.i("Restoring normal power mode");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CodecOption {
|
||||
private String key;
|
||||
private Object value;
|
||||
|
||||
public CodecOption(String key, Object value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static List<CodecOption> parse(String codecOptions) {
|
||||
if (codecOptions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<CodecOption> result = new ArrayList<>();
|
||||
|
||||
boolean escape = false;
|
||||
StringBuilder buf = new StringBuilder();
|
||||
|
||||
for (char c : codecOptions.toCharArray()) {
|
||||
switch (c) {
|
||||
case '\\':
|
||||
if (escape) {
|
||||
buf.append('\\');
|
||||
escape = false;
|
||||
} else {
|
||||
escape = true;
|
||||
}
|
||||
break;
|
||||
case ',':
|
||||
if (escape) {
|
||||
buf.append(',');
|
||||
escape = false;
|
||||
} else {
|
||||
// This comma is a separator between codec options
|
||||
String codecOption = buf.toString();
|
||||
result.add(parseOption(codecOption));
|
||||
// Clear buf
|
||||
buf.setLength(0);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
buf.append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (buf.length() > 0) {
|
||||
String codecOption = buf.toString();
|
||||
result.add(parseOption(codecOption));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CodecOption parseOption(String option) {
|
||||
int equalSignIndex = option.indexOf('=');
|
||||
if (equalSignIndex == -1) {
|
||||
throw new IllegalArgumentException("'=' expected");
|
||||
}
|
||||
String keyAndType = option.substring(0, equalSignIndex);
|
||||
if (keyAndType.length() == 0) {
|
||||
throw new IllegalArgumentException("Key may not be null");
|
||||
}
|
||||
|
||||
String key;
|
||||
String type;
|
||||
|
||||
int colonIndex = keyAndType.indexOf(':');
|
||||
if (colonIndex != -1) {
|
||||
key = keyAndType.substring(0, colonIndex);
|
||||
type = keyAndType.substring(colonIndex + 1);
|
||||
} else {
|
||||
key = keyAndType;
|
||||
type = "int"; // assume int by default
|
||||
}
|
||||
|
||||
Object value;
|
||||
String valueString = option.substring(equalSignIndex + 1);
|
||||
switch (type) {
|
||||
case "int":
|
||||
value = Integer.parseInt(valueString);
|
||||
break;
|
||||
case "long":
|
||||
value = Long.parseLong(valueString);
|
||||
break;
|
||||
case "float":
|
||||
value = Float.parseFloat(valueString);
|
||||
break;
|
||||
case "string":
|
||||
value = valueString;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type);
|
||||
}
|
||||
|
||||
return new CodecOption(key, value);
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Scanner;
|
||||
|
||||
public final class Command {
|
||||
private Command() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void exec(String... cmd) throws IOException, InterruptedException {
|
||||
Process process = Runtime.getRuntime().exec(cmd);
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static String execReadLine(String... cmd) throws IOException, InterruptedException {
|
||||
String result = null;
|
||||
Process process = Runtime.getRuntime().exec(cmd);
|
||||
Scanner scanner = new Scanner(process.getInputStream());
|
||||
if (scanner.hasNextLine()) {
|
||||
result = scanner.nextLine();
|
||||
}
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
/**
|
||||
* Union of all supported event types, identified by their {@code type}.
|
||||
*/
|
||||
public final class ControlMessage {
|
||||
|
||||
public static final int TYPE_INJECT_KEYCODE = 0;
|
||||
public static final int TYPE_INJECT_TEXT = 1;
|
||||
public static final int TYPE_INJECT_TOUCH_EVENT = 2;
|
||||
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
|
||||
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
|
||||
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
|
||||
public static final int TYPE_EXPAND_SETTINGS_PANEL = 6;
|
||||
public static final int TYPE_COLLAPSE_PANELS = 7;
|
||||
public static final int TYPE_GET_CLIPBOARD = 8;
|
||||
public static final int TYPE_SET_CLIPBOARD = 9;
|
||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
|
||||
public static final int TYPE_ROTATE_DEVICE = 11;
|
||||
|
||||
public static final long SEQUENCE_INVALID = 0;
|
||||
|
||||
public static final int COPY_KEY_NONE = 0;
|
||||
public static final int COPY_KEY_COPY = 1;
|
||||
public static final int COPY_KEY_CUT = 2;
|
||||
|
||||
private int type;
|
||||
private String text;
|
||||
private int metaState; // KeyEvent.META_*
|
||||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
|
||||
private int keycode; // KeyEvent.KEYCODE_*
|
||||
private int buttons; // MotionEvent.BUTTON_*
|
||||
private long pointerId;
|
||||
private float pressure;
|
||||
private Position position;
|
||||
private int hScroll;
|
||||
private int vScroll;
|
||||
private int copyKey;
|
||||
private boolean paste;
|
||||
private int repeat;
|
||||
private long sequence;
|
||||
|
||||
private ControlMessage() {
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_KEYCODE;
|
||||
msg.action = action;
|
||||
msg.keycode = keycode;
|
||||
msg.repeat = repeat;
|
||||
msg.metaState = metaState;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectText(String text) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_TEXT;
|
||||
msg.text = text;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_TOUCH_EVENT;
|
||||
msg.action = action;
|
||||
msg.pointerId = pointerId;
|
||||
msg.pressure = pressure;
|
||||
msg.position = position;
|
||||
msg.buttons = buttons;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_SCROLL_EVENT;
|
||||
msg.position = position;
|
||||
msg.hScroll = hScroll;
|
||||
msg.vScroll = vScroll;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createBackOrScreenOn(int action) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_BACK_OR_SCREEN_ON;
|
||||
msg.action = action;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createGetClipboard(int copyKey) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_GET_CLIPBOARD;
|
||||
msg.copyKey = copyKey;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_SET_CLIPBOARD;
|
||||
msg.sequence = sequence;
|
||||
msg.text = text;
|
||||
msg.paste = paste;
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
|
||||
*/
|
||||
public static ControlMessage createSetScreenPowerMode(int mode) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_SET_SCREEN_POWER_MODE;
|
||||
msg.action = mode;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createEmpty(int type) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = type;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public int getMetaState() {
|
||||
return metaState;
|
||||
}
|
||||
|
||||
public int getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public int getKeycode() {
|
||||
return keycode;
|
||||
}
|
||||
|
||||
public int getButtons() {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
public long getPointerId() {
|
||||
return pointerId;
|
||||
}
|
||||
|
||||
public float getPressure() {
|
||||
return pressure;
|
||||
}
|
||||
|
||||
public Position getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public int getHScroll() {
|
||||
return hScroll;
|
||||
}
|
||||
|
||||
public int getVScroll() {
|
||||
return vScroll;
|
||||
}
|
||||
|
||||
public int getCopyKey() {
|
||||
return copyKey;
|
||||
}
|
||||
|
||||
public boolean getPaste() {
|
||||
return paste;
|
||||
}
|
||||
|
||||
public int getRepeat() {
|
||||
return repeat;
|
||||
}
|
||||
|
||||
public long getSequence() {
|
||||
return sequence;
|
||||
}
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ControlMessageReader {
|
||||
|
||||
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
||||
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
|
||||
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||
static final int GET_CLIPBOARD_LENGTH = 1;
|
||||
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9;
|
||||
|
||||
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
|
||||
|
||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes
|
||||
public static final int INJECT_TEXT_MAX_LENGTH = 300;
|
||||
|
||||
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
|
||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||
|
||||
public ControlMessageReader() {
|
||||
// invariant: the buffer is always in "get" mode
|
||||
buffer.limit(0);
|
||||
}
|
||||
|
||||
public boolean isFull() {
|
||||
return buffer.remaining() == rawBuffer.length;
|
||||
}
|
||||
|
||||
public void readFrom(InputStream input) throws IOException {
|
||||
if (isFull()) {
|
||||
throw new IllegalStateException("Buffer full, call next() to consume");
|
||||
}
|
||||
buffer.compact();
|
||||
int head = buffer.position();
|
||||
int r = input.read(rawBuffer, head, rawBuffer.length - head);
|
||||
if (r == -1) {
|
||||
throw new EOFException("Controller socket closed");
|
||||
}
|
||||
buffer.position(head + r);
|
||||
buffer.flip();
|
||||
}
|
||||
|
||||
public ControlMessage next() {
|
||||
if (!buffer.hasRemaining()) {
|
||||
return null;
|
||||
}
|
||||
int savedPosition = buffer.position();
|
||||
|
||||
int type = buffer.get();
|
||||
ControlMessage msg;
|
||||
switch (type) {
|
||||
case ControlMessage.TYPE_INJECT_KEYCODE:
|
||||
msg = parseInjectKeycode();
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TEXT:
|
||||
msg = parseInjectText();
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
msg = parseInjectTouchEvent();
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
msg = parseInjectScrollEvent();
|
||||
break;
|
||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||
msg = parseBackOrScreenOnEvent();
|
||||
break;
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
msg = parseGetClipboard();
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
msg = parseSetClipboard();
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||
msg = parseSetScreenPowerMode();
|
||||
break;
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
msg = ControlMessage.createEmpty(type);
|
||||
break;
|
||||
default:
|
||||
Ln.w("Unknown event type: " + type);
|
||||
msg = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (msg == null) {
|
||||
// failure, reset savedPosition
|
||||
buffer.position(savedPosition);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectKeycode() {
|
||||
if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int action = toUnsigned(buffer.get());
|
||||
int keycode = buffer.getInt();
|
||||
int repeat = buffer.getInt();
|
||||
int metaState = buffer.getInt();
|
||||
return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
|
||||
}
|
||||
|
||||
private String parseString() {
|
||||
if (buffer.remaining() < 4) {
|
||||
return null;
|
||||
}
|
||||
int len = buffer.getInt();
|
||||
if (buffer.remaining() < len) {
|
||||
return null;
|
||||
}
|
||||
int position = buffer.position();
|
||||
// Move the buffer position to consume the text
|
||||
buffer.position(position + len);
|
||||
return new String(rawBuffer, position, len, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectText() {
|
||||
String text = parseString();
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
return ControlMessage.createInjectText(text);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectTouchEvent() {
|
||||
if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int action = toUnsigned(buffer.get());
|
||||
long pointerId = buffer.getLong();
|
||||
Position position = readPosition(buffer);
|
||||
// 16 bits fixed-point
|
||||
int pressureInt = toUnsigned(buffer.getShort());
|
||||
// convert it to a float between 0 and 1 (0x1p16f is 2^16 as float)
|
||||
float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f);
|
||||
int buttons = buffer.getInt();
|
||||
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectScrollEvent() {
|
||||
if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
Position position = readPosition(buffer);
|
||||
int hScroll = buffer.getInt();
|
||||
int vScroll = buffer.getInt();
|
||||
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
|
||||
}
|
||||
|
||||
private ControlMessage parseBackOrScreenOnEvent() {
|
||||
if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int action = toUnsigned(buffer.get());
|
||||
return ControlMessage.createBackOrScreenOn(action);
|
||||
}
|
||||
|
||||
private ControlMessage parseGetClipboard() {
|
||||
if (buffer.remaining() < GET_CLIPBOARD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int copyKey = toUnsigned(buffer.get());
|
||||
return ControlMessage.createGetClipboard(copyKey);
|
||||
}
|
||||
|
||||
private ControlMessage parseSetClipboard() {
|
||||
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
long sequence = buffer.getLong();
|
||||
boolean paste = buffer.get() != 0;
|
||||
String text = parseString();
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
return ControlMessage.createSetClipboard(sequence, text, paste);
|
||||
}
|
||||
|
||||
private ControlMessage parseSetScreenPowerMode() {
|
||||
if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int mode = buffer.get();
|
||||
return ControlMessage.createSetScreenPowerMode(mode);
|
||||
}
|
||||
|
||||
private static Position readPosition(ByteBuffer buffer) {
|
||||
int x = buffer.getInt();
|
||||
int y = buffer.getInt();
|
||||
int screenWidth = toUnsigned(buffer.getShort());
|
||||
int screenHeight = toUnsigned(buffer.getShort());
|
||||
return new Position(x, y, screenWidth, screenHeight);
|
||||
}
|
||||
|
||||
private static int toUnsigned(short value) {
|
||||
return value & 0xffff;
|
||||
}
|
||||
|
||||
private static int toUnsigned(byte value) {
|
||||
return value & 0xff;
|
||||
}
|
||||
}
|
|
@ -1,315 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Controller {
|
||||
|
||||
private static final int DEFAULT_DEVICE_ID = 0;
|
||||
|
||||
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
private final DeviceMessageSender sender;
|
||||
private final boolean clipboardAutosync;
|
||||
|
||||
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
|
||||
|
||||
private long lastTouchDown;
|
||||
private final PointersState pointersState = new PointersState();
|
||||
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
|
||||
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
|
||||
|
||||
private boolean keepPowerModeOff;
|
||||
|
||||
public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
this.clipboardAutosync = clipboardAutosync;
|
||||
initPointers();
|
||||
sender = new DeviceMessageSender(connection);
|
||||
}
|
||||
|
||||
private void initPointers() {
|
||||
for (int i = 0; i < PointersState.MAX_POINTERS; ++i) {
|
||||
MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
|
||||
props.toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
|
||||
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
|
||||
coords.orientation = 0;
|
||||
coords.size = 0;
|
||||
|
||||
pointerProperties[i] = props;
|
||||
pointerCoords[i] = coords;
|
||||
}
|
||||
}
|
||||
|
||||
public void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (!Device.isScreenOn()) {
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||
|
||||
// dirty hack
|
||||
// After POWER is injected, the device is powered on asynchronously.
|
||||
// To turn the device screen off while mirroring, the client will send a message that
|
||||
// would be handled before the device is actually powered on, so its effect would
|
||||
// be "canceled" once the device is turned back on.
|
||||
// Adding this delay prevents to handle the message before the device is actually
|
||||
// powered on.
|
||||
SystemClock.sleep(500);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
handleEvent();
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceMessageSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
private void handleEvent() throws IOException {
|
||||
ControlMessage msg = connection.receiveControlMessage();
|
||||
switch (msg.getType()) {
|
||||
case ControlMessage.TYPE_INJECT_KEYCODE:
|
||||
if (device.supportsInputEvents()) {
|
||||
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TEXT:
|
||||
if (device.supportsInputEvents()) {
|
||||
injectText(msg.getText());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
if (device.supportsInputEvents()) {
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
if (device.supportsInputEvents()) {
|
||||
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||
if (device.supportsInputEvents()) {
|
||||
pressBackOrTurnScreenOn(msg.getAction());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
Device.expandNotificationPanel();
|
||||
break;
|
||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||
Device.expandSettingsPanel();
|
||||
break;
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
Device.collapsePanels();
|
||||
break;
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
getClipboard(msg.getCopyKey());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||
if (device.supportsInputEvents()) {
|
||||
int mode = msg.getAction();
|
||||
boolean setPowerModeOk = Device.setScreenPowerMode(mode);
|
||||
if (setPowerModeOk) {
|
||||
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
|
||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
Device.rotateDevice();
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||
if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
|
||||
schedulePowerModeOff();
|
||||
}
|
||||
return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
private boolean injectChar(char c) {
|
||||
String decomposed = KeyComposition.decompose(c);
|
||||
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c};
|
||||
KeyEvent[] events = charMap.getEvents(chars);
|
||||
if (events == null) {
|
||||
return false;
|
||||
}
|
||||
for (KeyEvent event : events) {
|
||||
if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int injectText(String text) {
|
||||
int successCount = 0;
|
||||
for (char c : text.toCharArray()) {
|
||||
if (!injectChar(c)) {
|
||||
Ln.w("Could not inject char u+" + String.format("%04x", (int) c));
|
||||
continue;
|
||||
}
|
||||
successCount++;
|
||||
}
|
||||
return successCount;
|
||||
}
|
||||
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
if (point == null) {
|
||||
Ln.w("Ignore touch event, it was generated for a different device size");
|
||||
return false;
|
||||
}
|
||||
|
||||
int pointerIndex = pointersState.getPointerIndex(pointerId);
|
||||
if (pointerIndex == -1) {
|
||||
Ln.w("Too many pointers for touch event");
|
||||
return false;
|
||||
}
|
||||
Pointer pointer = pointersState.get(pointerIndex);
|
||||
pointer.setPoint(point);
|
||||
pointer.setPressure(pressure);
|
||||
pointer.setUp(action == MotionEvent.ACTION_UP);
|
||||
|
||||
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
|
||||
|
||||
if (pointerCount == 1) {
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
lastTouchDown = now;
|
||||
}
|
||||
} else {
|
||||
// secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
|
||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
||||
action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
|
||||
}
|
||||
}
|
||||
|
||||
// Right-click and middle-click only work if the source is a mouse
|
||||
boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0;
|
||||
int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN;
|
||||
if (source != InputDevice.SOURCE_MOUSE) {
|
||||
// Buttons must not be set for touch events
|
||||
buttons = 0;
|
||||
}
|
||||
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
|
||||
0);
|
||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
private boolean injectScroll(Position position, int hScroll, int vScroll) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
if (point == null) {
|
||||
// ignore event
|
||||
return false;
|
||||
}
|
||||
|
||||
MotionEvent.PointerProperties props = pointerProperties[0];
|
||||
props.id = 0;
|
||||
|
||||
MotionEvent.PointerCoords coords = pointerCoords[0];
|
||||
coords.x = point.getX();
|
||||
coords.y = point.getY();
|
||||
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
||||
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0,
|
||||
InputDevice.SOURCE_MOUSE, 0);
|
||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a call to set power mode to off after a small delay.
|
||||
*/
|
||||
private static void schedulePowerModeOff() {
|
||||
EXECUTOR.schedule(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Ln.i("Forcing screen off");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
||||
}
|
||||
}, 200, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private boolean pressBackOrTurnScreenOn(int action) {
|
||||
if (Device.isScreenOn()) {
|
||||
return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
// Screen is off
|
||||
// Only press POWER on ACTION_DOWN
|
||||
if (action != KeyEvent.ACTION_DOWN) {
|
||||
// do nothing,
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keepPowerModeOff) {
|
||||
schedulePowerModeOff();
|
||||
}
|
||||
return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
private void getClipboard(int copyKey) {
|
||||
// On Android >= 7, press the COPY or CUT key if requested
|
||||
if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
||||
int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT;
|
||||
// Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one
|
||||
device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH);
|
||||
}
|
||||
|
||||
// If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in
|
||||
// particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than
|
||||
// copying an old clipboard content.
|
||||
if (!clipboardAutosync) {
|
||||
String clipboardText = Device.getClipboardText();
|
||||
if (clipboardText != null) {
|
||||
sender.pushClipboardText(clipboardText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setClipboard(String text, boolean paste, long sequence) {
|
||||
boolean ok = device.setClipboardText(text);
|
||||
if (ok) {
|
||||
Ln.i("Device clipboard set");
|
||||
}
|
||||
|
||||
// On Android >= 7, also press the PASTE key if requested
|
||||
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
if (sequence != ControlMessage.SEQUENCE_INVALID) {
|
||||
// Acknowledgement requested
|
||||
sender.pushAckClipboard(sequence);
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.net.LocalServerSocket;
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class DesktopConnection implements Closeable {
|
||||
|
||||
private static final int DEVICE_NAME_FIELD_LENGTH = 64;
|
||||
|
||||
private static final String SOCKET_NAME = "scrcpy";
|
||||
|
||||
private final LocalSocket videoSocket;
|
||||
private final FileDescriptor videoFd;
|
||||
|
||||
private final LocalSocket controlSocket;
|
||||
private final InputStream controlInputStream;
|
||||
private final OutputStream controlOutputStream;
|
||||
|
||||
private final ControlMessageReader reader = new ControlMessageReader();
|
||||
private final DeviceMessageWriter writer = new DeviceMessageWriter();
|
||||
|
||||
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
|
||||
this.videoSocket = videoSocket;
|
||||
this.controlSocket = controlSocket;
|
||||
controlInputStream = controlSocket.getInputStream();
|
||||
controlOutputStream = controlSocket.getOutputStream();
|
||||
videoFd = videoSocket.getFileDescriptor();
|
||||
}
|
||||
|
||||
private static LocalSocket connect(String abstractName) throws IOException {
|
||||
LocalSocket localSocket = new LocalSocket();
|
||||
localSocket.connect(new LocalSocketAddress(abstractName));
|
||||
return localSocket;
|
||||
}
|
||||
|
||||
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
|
||||
LocalSocket videoSocket;
|
||||
LocalSocket controlSocket;
|
||||
if (tunnelForward) {
|
||||
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
|
||||
try {
|
||||
videoSocket = localServerSocket.accept();
|
||||
// send one byte so the client may read() to detect a connection error
|
||||
videoSocket.getOutputStream().write(0);
|
||||
try {
|
||||
controlSocket = localServerSocket.accept();
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
localServerSocket.close();
|
||||
}
|
||||
} else {
|
||||
videoSocket = connect(SOCKET_NAME);
|
||||
try {
|
||||
controlSocket = connect(SOCKET_NAME);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket);
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||
return connection;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
videoSocket.shutdownInput();
|
||||
videoSocket.shutdownOutput();
|
||||
videoSocket.close();
|
||||
controlSocket.shutdownInput();
|
||||
controlSocket.shutdownOutput();
|
||||
controlSocket.close();
|
||||
}
|
||||
|
||||
private void send(String deviceName, int width, int height) throws IOException {
|
||||
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
|
||||
|
||||
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
|
||||
int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
|
||||
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
|
||||
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
|
||||
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8);
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
|
||||
IO.writeFully(videoFd, buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
public FileDescriptor getVideoFd() {
|
||||
return videoFd;
|
||||
}
|
||||
|
||||
public ControlMessage receiveControlMessage() throws IOException {
|
||||
ControlMessage msg = reader.next();
|
||||
while (msg == null) {
|
||||
reader.readFrom(controlInputStream);
|
||||
msg = reader.next();
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
public void sendDeviceMessage(DeviceMessage msg) throws IOException {
|
||||
writer.writeTo(msg, controlOutputStream);
|
||||
}
|
||||
}
|
|
@ -1,307 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
import com.genymobile.scrcpy.wrappers.WindowManager;
|
||||
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class Device {
|
||||
|
||||
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
|
||||
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
|
||||
|
||||
public static final int INJECT_MODE_ASYNC = InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
|
||||
public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT;
|
||||
public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;
|
||||
|
||||
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
|
||||
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
|
||||
|
||||
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
|
||||
private static final Settings SETTINGS = new Settings(SERVICE_MANAGER);
|
||||
|
||||
public interface RotationListener {
|
||||
void onRotationChanged(int rotation);
|
||||
}
|
||||
|
||||
public interface ClipboardListener {
|
||||
void onClipboardTextChanged(String text);
|
||||
}
|
||||
|
||||
private ScreenInfo screenInfo;
|
||||
private RotationListener rotationListener;
|
||||
private ClipboardListener clipboardListener;
|
||||
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Logical display identifier
|
||||
*/
|
||||
private final int displayId;
|
||||
|
||||
/**
|
||||
* The surface flinger layer stack associated with this logical display
|
||||
*/
|
||||
private final int layerStack;
|
||||
|
||||
private final boolean supportsInputEvents;
|
||||
|
||||
public Device(Options options) {
|
||||
displayId = options.getDisplayId();
|
||||
DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (displayInfo == null) {
|
||||
int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds();
|
||||
throw new InvalidDisplayIdException(displayId, displayIds);
|
||||
}
|
||||
|
||||
int displayInfoFlags = displayInfo.getFlags();
|
||||
|
||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockVideoOrientation());
|
||||
layerStack = displayInfo.getLayerStack();
|
||||
|
||||
SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
synchronized (Device.this) {
|
||||
screenInfo = screenInfo.withDeviceRotation(rotation);
|
||||
|
||||
// notify
|
||||
if (rotationListener != null) {
|
||||
rotationListener.onRotationChanged(rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, displayId);
|
||||
|
||||
if (options.getControl() && options.getClipboardAutosync()) {
|
||||
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
|
||||
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
|
||||
if (clipboardManager != null) {
|
||||
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
|
||||
@Override
|
||||
public void dispatchPrimaryClipChanged() {
|
||||
if (isSettingClipboard.get()) {
|
||||
// This is a notification for the change we are currently applying, ignore it
|
||||
return;
|
||||
}
|
||||
synchronized (Device.this) {
|
||||
if (clipboardListener != null) {
|
||||
String text = getClipboardText();
|
||||
if (text != null) {
|
||||
clipboardListener.onClipboardTextChanged(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Ln.w("No clipboard manager, copy-paste between device and computer will not work");
|
||||
}
|
||||
}
|
||||
|
||||
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
||||
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
||||
}
|
||||
|
||||
// main display or any display on Android >= Q
|
||||
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
||||
if (!supportsInputEvents) {
|
||||
Ln.w("Input events are not supported for secondary displays before Android 10");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized ScreenInfo getScreenInfo() {
|
||||
return screenInfo;
|
||||
}
|
||||
|
||||
public int getLayerStack() {
|
||||
return layerStack;
|
||||
}
|
||||
|
||||
public Point getPhysicalPoint(Position position) {
|
||||
// it hides the field on purpose, to read it with a lock
|
||||
@SuppressWarnings("checkstyle:HiddenField")
|
||||
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
|
||||
|
||||
// ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation
|
||||
Size unlockedVideoSize = screenInfo.getUnlockedVideoSize();
|
||||
|
||||
int reverseVideoRotation = screenInfo.getReverseVideoRotation();
|
||||
// reverse the video rotation to apply the events
|
||||
Position devicePosition = position.rotate(reverseVideoRotation);
|
||||
|
||||
Size clientVideoSize = devicePosition.getScreenSize();
|
||||
if (!unlockedVideoSize.equals(clientVideoSize)) {
|
||||
// The client sends a click relative to a video with wrong dimensions,
|
||||
// the device may have been rotated since the event was generated, so ignore the event
|
||||
return null;
|
||||
}
|
||||
Rect contentRect = screenInfo.getContentRect();
|
||||
Point point = devicePosition.getPoint();
|
||||
int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth();
|
||||
int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight();
|
||||
return new Point(convertedX, convertedY);
|
||||
}
|
||||
|
||||
public static String getDeviceName() {
|
||||
return Build.MODEL;
|
||||
}
|
||||
|
||||
public static boolean supportsInputEvents(int displayId) {
|
||||
return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
||||
}
|
||||
|
||||
public boolean supportsInputEvents() {
|
||||
return supportsInputEvents;
|
||||
}
|
||||
|
||||
public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) {
|
||||
if (!supportsInputEvents(displayId)) {
|
||||
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
||||
}
|
||||
|
||||
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode);
|
||||
}
|
||||
|
||||
public boolean injectEvent(InputEvent event, int injectMode) {
|
||||
return injectEvent(event, displayId, injectMode);
|
||||
}
|
||||
|
||||
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||
InputDevice.SOURCE_KEYBOARD);
|
||||
return injectEvent(event, displayId, injectMode);
|
||||
}
|
||||
|
||||
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) {
|
||||
return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode);
|
||||
}
|
||||
|
||||
public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) {
|
||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode)
|
||||
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode);
|
||||
}
|
||||
|
||||
public boolean pressReleaseKeycode(int keyCode, int injectMode) {
|
||||
return pressReleaseKeycode(keyCode, displayId, injectMode);
|
||||
}
|
||||
|
||||
public static boolean isScreenOn() {
|
||||
return SERVICE_MANAGER.getPowerManager().isScreenOn();
|
||||
}
|
||||
|
||||
public synchronized void setRotationListener(RotationListener rotationListener) {
|
||||
this.rotationListener = rotationListener;
|
||||
}
|
||||
|
||||
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
|
||||
this.clipboardListener = clipboardListener;
|
||||
}
|
||||
|
||||
public static void expandNotificationPanel() {
|
||||
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel();
|
||||
}
|
||||
|
||||
public static void expandSettingsPanel() {
|
||||
SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel();
|
||||
}
|
||||
|
||||
public static void collapsePanels() {
|
||||
SERVICE_MANAGER.getStatusBarManager().collapsePanels();
|
||||
}
|
||||
|
||||
public static String getClipboardText() {
|
||||
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
|
||||
if (clipboardManager == null) {
|
||||
return null;
|
||||
}
|
||||
CharSequence s = clipboardManager.getText();
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
public boolean setClipboardText(String text) {
|
||||
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
|
||||
if (clipboardManager == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String currentClipboard = getClipboardText();
|
||||
if (currentClipboard != null && currentClipboard.equals(text)) {
|
||||
// The clipboard already contains the requested text.
|
||||
// Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause
|
||||
// the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this
|
||||
// problem, do not explicitly set the clipboard text if it already contains the expected content.
|
||||
return false;
|
||||
}
|
||||
|
||||
isSettingClipboard.set(true);
|
||||
boolean ok = clipboardManager.setText(text);
|
||||
isSettingClipboard.set(false);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mode one of the {@code POWER_MODE_*} constants
|
||||
*/
|
||||
public static boolean setScreenPowerMode(int mode) {
|
||||
IBinder d = SurfaceControl.getBuiltInDisplay();
|
||||
if (d == null) {
|
||||
Ln.e("Could not get built-in display");
|
||||
return false;
|
||||
}
|
||||
return SurfaceControl.setDisplayPowerMode(d, mode);
|
||||
}
|
||||
|
||||
public static boolean powerOffScreen(int displayId) {
|
||||
if (!isScreenOn()) {
|
||||
return true;
|
||||
}
|
||||
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
|
||||
*/
|
||||
public static void rotateDevice() {
|
||||
WindowManager wm = SERVICE_MANAGER.getWindowManager();
|
||||
|
||||
boolean accelerometerRotation = !wm.isRotationFrozen();
|
||||
|
||||
int currentRotation = wm.getRotation();
|
||||
int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0
|
||||
String newRotationString = newRotation == 0 ? "portrait" : "landscape";
|
||||
|
||||
Ln.i("Device rotation requested: " + newRotationString);
|
||||
wm.freezeRotation(newRotation);
|
||||
|
||||
// restore auto-rotate if necessary
|
||||
if (accelerometerRotation) {
|
||||
wm.thawRotation();
|
||||
}
|
||||
}
|
||||
|
||||
public static Settings getSettings() {
|
||||
return SETTINGS;
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public final class DeviceMessage {
|
||||
|
||||
public static final int TYPE_CLIPBOARD = 0;
|
||||
public static final int TYPE_ACK_CLIPBOARD = 1;
|
||||
|
||||
public static final long SEQUENCE_INVALID = ControlMessage.SEQUENCE_INVALID;
|
||||
|
||||
private int type;
|
||||
private String text;
|
||||
private long sequence;
|
||||
|
||||
private DeviceMessage() {
|
||||
}
|
||||
|
||||
public static DeviceMessage createClipboard(String text) {
|
||||
DeviceMessage event = new DeviceMessage();
|
||||
event.type = TYPE_CLIPBOARD;
|
||||
event.text = text;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static DeviceMessage createAckClipboard(long sequence) {
|
||||
DeviceMessage event = new DeviceMessage();
|
||||
event.type = TYPE_ACK_CLIPBOARD;
|
||||
event.sequence = sequence;
|
||||
return event;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public long getSequence() {
|
||||
return sequence;
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class DeviceMessageSender {
|
||||
|
||||
private final DesktopConnection connection;
|
||||
|
||||
private String clipboardText;
|
||||
|
||||
private long ack;
|
||||
|
||||
public DeviceMessageSender(DesktopConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public synchronized void pushClipboardText(String text) {
|
||||
clipboardText = text;
|
||||
notify();
|
||||
}
|
||||
|
||||
public synchronized void pushAckClipboard(long sequence) {
|
||||
ack = sequence;
|
||||
notify();
|
||||
}
|
||||
|
||||
public void loop() throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
String text;
|
||||
long sequence;
|
||||
synchronized (this) {
|
||||
while (ack == DeviceMessage.SEQUENCE_INVALID && clipboardText == null) {
|
||||
wait();
|
||||
}
|
||||
text = clipboardText;
|
||||
clipboardText = null;
|
||||
|
||||
sequence = ack;
|
||||
ack = DeviceMessage.SEQUENCE_INVALID;
|
||||
}
|
||||
|
||||
if (sequence != DeviceMessage.SEQUENCE_INVALID) {
|
||||
DeviceMessage event = DeviceMessage.createAckClipboard(sequence);
|
||||
connection.sendDeviceMessage(event);
|
||||
}
|
||||
if (text != null) {
|
||||
DeviceMessage event = DeviceMessage.createClipboard(text);
|
||||
connection.sendDeviceMessage(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DeviceMessageWriter {
|
||||
|
||||
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
|
||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes
|
||||
|
||||
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
|
||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||
|
||||
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
|
||||
buffer.clear();
|
||||
buffer.put((byte) msg.getType());
|
||||
switch (msg.getType()) {
|
||||
case DeviceMessage.TYPE_CLIPBOARD:
|
||||
String text = msg.getText();
|
||||
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
|
||||
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
|
||||
buffer.putInt(len);
|
||||
buffer.put(raw, 0, len);
|
||||
output.write(rawBuffer, 0, buffer.position());
|
||||
break;
|
||||
case DeviceMessage.TYPE_ACK_CLIPBOARD:
|
||||
buffer.putLong(msg.getSequence());
|
||||
output.write(rawBuffer, 0, buffer.position());
|
||||
break;
|
||||
default:
|
||||
Ln.w("Unknown device message: " + msg.getType());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public final class DisplayInfo {
|
||||
private final int displayId;
|
||||
private final Size size;
|
||||
private final int rotation;
|
||||
private final int layerStack;
|
||||
private final int flags;
|
||||
|
||||
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
|
||||
|
||||
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) {
|
||||
this.displayId = displayId;
|
||||
this.size = size;
|
||||
this.rotation = rotation;
|
||||
this.layerStack = layerStack;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
public int getDisplayId() {
|
||||
return displayId;
|
||||
}
|
||||
|
||||
public Size getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public int getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public int getLayerStack() {
|
||||
return layerStack;
|
||||
}
|
||||
|
||||
public int getFlags() {
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class IO {
|
||||
private IO() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
|
||||
// ByteBuffer position is not updated as expected by Os.write() on old Android versions, so
|
||||
// count the remaining bytes manually.
|
||||
// See <https://github.com/Genymobile/scrcpy/issues/291>.
|
||||
int remaining = from.remaining();
|
||||
while (remaining > 0) {
|
||||
try {
|
||||
int w = Os.write(fd, from);
|
||||
if (BuildConfig.DEBUG && w < 0) {
|
||||
// w should not be negative, since an exception is thrown on error
|
||||
throw new AssertionError("Os.write() returned a negative value (" + w + ")");
|
||||
}
|
||||
remaining -= w;
|
||||
} catch (ErrnoException e) {
|
||||
if (e.errno != OsConstants.EINTR) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException {
|
||||
writeFully(fd, ByteBuffer.wrap(buffer, offset, len));
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public class InvalidDisplayIdException extends RuntimeException {
|
||||
|
||||
private final int displayId;
|
||||
private final int[] availableDisplayIds;
|
||||
|
||||
public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) {
|
||||
super("There is no display having id " + displayId);
|
||||
this.displayId = displayId;
|
||||
this.availableDisplayIds = availableDisplayIds;
|
||||
}
|
||||
|
||||
public int getDisplayId() {
|
||||
return displayId;
|
||||
}
|
||||
|
||||
public int[] getAvailableDisplayIds() {
|
||||
return availableDisplayIds;
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.media.MediaCodecInfo;
|
||||
|
||||
public class InvalidEncoderException extends RuntimeException {
|
||||
|
||||
private final String name;
|
||||
private final MediaCodecInfo[] availableEncoders;
|
||||
|
||||
public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) {
|
||||
super("There is no encoder having name '" + name + '"');
|
||||
this.name = name;
|
||||
this.availableEncoders = availableEncoders;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public MediaCodecInfo[] getAvailableEncoders() {
|
||||
return availableEncoders;
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Decompose accented characters.
|
||||
* <p>
|
||||
* For example, {@link #decompose(char) decompose('é')} returns {@code "\u0301e"}.
|
||||
* <p>
|
||||
* This is useful for injecting key events to generate the expected character ({@link android.view.KeyCharacterMap#getEvents(char[])}
|
||||
* KeyCharacterMap.getEvents()} returns {@code null} with input {@code "é"} but works with input {@code "\u0301e"}).
|
||||
* <p>
|
||||
* See <a href="https://source.android.com/devices/input/key-character-map-files#behaviors">diacritical dead key characters</a>.
|
||||
*/
|
||||
public final class KeyComposition {
|
||||
|
||||
private static final String KEY_DEAD_GRAVE = "\u0300";
|
||||
private static final String KEY_DEAD_ACUTE = "\u0301";
|
||||
private static final String KEY_DEAD_CIRCUMFLEX = "\u0302";
|
||||
private static final String KEY_DEAD_TILDE = "\u0303";
|
||||
private static final String KEY_DEAD_UMLAUT = "\u0308";
|
||||
|
||||
private static final Map<Character, String> COMPOSITION_MAP = createDecompositionMap();
|
||||
|
||||
private KeyComposition() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static String decompose(char c) {
|
||||
return COMPOSITION_MAP.get(c);
|
||||
}
|
||||
|
||||
private static String grave(char c) {
|
||||
return KEY_DEAD_GRAVE + c;
|
||||
}
|
||||
|
||||
private static String acute(char c) {
|
||||
return KEY_DEAD_ACUTE + c;
|
||||
}
|
||||
|
||||
private static String circumflex(char c) {
|
||||
return KEY_DEAD_CIRCUMFLEX + c;
|
||||
}
|
||||
|
||||
private static String tilde(char c) {
|
||||
return KEY_DEAD_TILDE + c;
|
||||
}
|
||||
|
||||
private static String umlaut(char c) {
|
||||
return KEY_DEAD_UMLAUT + c;
|
||||
}
|
||||
|
||||
private static Map<Character, String> createDecompositionMap() {
|
||||
Map<Character, String> map = new HashMap<>();
|
||||
map.put('À', grave('A'));
|
||||
map.put('È', grave('E'));
|
||||
map.put('Ì', grave('I'));
|
||||
map.put('Ò', grave('O'));
|
||||
map.put('Ù', grave('U'));
|
||||
map.put('à', grave('a'));
|
||||
map.put('è', grave('e'));
|
||||
map.put('ì', grave('i'));
|
||||
map.put('ò', grave('o'));
|
||||
map.put('ù', grave('u'));
|
||||
map.put('Ǹ', grave('N'));
|
||||
map.put('ǹ', grave('n'));
|
||||
map.put('Ẁ', grave('W'));
|
||||
map.put('ẁ', grave('w'));
|
||||
map.put('Ỳ', grave('Y'));
|
||||
map.put('ỳ', grave('y'));
|
||||
|
||||
map.put('Á', acute('A'));
|
||||
map.put('É', acute('E'));
|
||||
map.put('Í', acute('I'));
|
||||
map.put('Ó', acute('O'));
|
||||
map.put('Ú', acute('U'));
|
||||
map.put('Ý', acute('Y'));
|
||||
map.put('á', acute('a'));
|
||||
map.put('é', acute('e'));
|
||||
map.put('í', acute('i'));
|
||||
map.put('ó', acute('o'));
|
||||
map.put('ú', acute('u'));
|
||||
map.put('ý', acute('y'));
|
||||
map.put('Ć', acute('C'));
|
||||
map.put('ć', acute('c'));
|
||||
map.put('Ĺ', acute('L'));
|
||||
map.put('ĺ', acute('l'));
|
||||
map.put('Ń', acute('N'));
|
||||
map.put('ń', acute('n'));
|
||||
map.put('Ŕ', acute('R'));
|
||||
map.put('ŕ', acute('r'));
|
||||
map.put('Ś', acute('S'));
|
||||
map.put('ś', acute('s'));
|
||||
map.put('Ź', acute('Z'));
|
||||
map.put('ź', acute('z'));
|
||||
map.put('Ǵ', acute('G'));
|
||||
map.put('ǵ', acute('g'));
|
||||
map.put('Ḉ', acute('Ç'));
|
||||
map.put('ḉ', acute('ç'));
|
||||
map.put('Ḱ', acute('K'));
|
||||
map.put('ḱ', acute('k'));
|
||||
map.put('Ḿ', acute('M'));
|
||||
map.put('ḿ', acute('m'));
|
||||
map.put('Ṕ', acute('P'));
|
||||
map.put('ṕ', acute('p'));
|
||||
map.put('Ẃ', acute('W'));
|
||||
map.put('ẃ', acute('w'));
|
||||
|
||||
map.put('Â', circumflex('A'));
|
||||
map.put('Ê', circumflex('E'));
|
||||
map.put('Î', circumflex('I'));
|
||||
map.put('Ô', circumflex('O'));
|
||||
map.put('Û', circumflex('U'));
|
||||
map.put('â', circumflex('a'));
|
||||
map.put('ê', circumflex('e'));
|
||||
map.put('î', circumflex('i'));
|
||||
map.put('ô', circumflex('o'));
|
||||
map.put('û', circumflex('u'));
|
||||
map.put('Ĉ', circumflex('C'));
|
||||
map.put('ĉ', circumflex('c'));
|
||||
map.put('Ĝ', circumflex('G'));
|
||||
map.put('ĝ', circumflex('g'));
|
||||
map.put('Ĥ', circumflex('H'));
|
||||
map.put('ĥ', circumflex('h'));
|
||||
map.put('Ĵ', circumflex('J'));
|
||||
map.put('ĵ', circumflex('j'));
|
||||
map.put('Ŝ', circumflex('S'));
|
||||
map.put('ŝ', circumflex('s'));
|
||||
map.put('Ŵ', circumflex('W'));
|
||||
map.put('ŵ', circumflex('w'));
|
||||
map.put('Ŷ', circumflex('Y'));
|
||||
map.put('ŷ', circumflex('y'));
|
||||
map.put('Ẑ', circumflex('Z'));
|
||||
map.put('ẑ', circumflex('z'));
|
||||
|
||||
map.put('Ã', tilde('A'));
|
||||
map.put('Ñ', tilde('N'));
|
||||
map.put('Õ', tilde('O'));
|
||||
map.put('ã', tilde('a'));
|
||||
map.put('ñ', tilde('n'));
|
||||
map.put('õ', tilde('o'));
|
||||
map.put('Ĩ', tilde('I'));
|
||||
map.put('ĩ', tilde('i'));
|
||||
map.put('Ũ', tilde('U'));
|
||||
map.put('ũ', tilde('u'));
|
||||
map.put('Ẽ', tilde('E'));
|
||||
map.put('ẽ', tilde('e'));
|
||||
map.put('Ỹ', tilde('Y'));
|
||||
map.put('ỹ', tilde('y'));
|
||||
|
||||
map.put('Ä', umlaut('A'));
|
||||
map.put('Ë', umlaut('E'));
|
||||
map.put('Ï', umlaut('I'));
|
||||
map.put('Ö', umlaut('O'));
|
||||
map.put('Ü', umlaut('U'));
|
||||
map.put('ä', umlaut('a'));
|
||||
map.put('ë', umlaut('e'));
|
||||
map.put('ï', umlaut('i'));
|
||||
map.put('ö', umlaut('o'));
|
||||
map.put('ü', umlaut('u'));
|
||||
map.put('ÿ', umlaut('y'));
|
||||
map.put('Ÿ', umlaut('Y'));
|
||||
map.put('Ḧ', umlaut('H'));
|
||||
map.put('ḧ', umlaut('h'));
|
||||
map.put('Ẅ', umlaut('W'));
|
||||
map.put('ẅ', umlaut('w'));
|
||||
map.put('Ẍ', umlaut('X'));
|
||||
map.put('ẍ', umlaut('x'));
|
||||
map.put('ẗ', umlaut('t'));
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal
|
||||
* directly).
|
||||
*/
|
||||
public final class Ln {
|
||||
|
||||
private static final String TAG = "scrcpy";
|
||||
private static final String PREFIX = "[server] ";
|
||||
|
||||
enum Level {
|
||||
VERBOSE, DEBUG, INFO, WARN, ERROR
|
||||
}
|
||||
|
||||
private static Level threshold = Level.INFO;
|
||||
|
||||
private Ln() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the log level.
|
||||
* <p>
|
||||
* Must be called before starting any new thread.
|
||||
*
|
||||
* @param level the log level
|
||||
*/
|
||||
public static void initLogLevel(Level level) {
|
||||
threshold = level;
|
||||
}
|
||||
|
||||
public static boolean isEnabled(Level level) {
|
||||
return level.ordinal() >= threshold.ordinal();
|
||||
}
|
||||
|
||||
public static void v(String message) {
|
||||
if (isEnabled(Level.VERBOSE)) {
|
||||
Log.v(TAG, message);
|
||||
System.out.println(PREFIX + "VERBOSE: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void d(String message) {
|
||||
if (isEnabled(Level.DEBUG)) {
|
||||
Log.d(TAG, message);
|
||||
System.out.println(PREFIX + "DEBUG: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void i(String message) {
|
||||
if (isEnabled(Level.INFO)) {
|
||||
Log.i(TAG, message);
|
||||
System.out.println(PREFIX + "INFO: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void w(String message, Throwable throwable) {
|
||||
if (isEnabled(Level.WARN)) {
|
||||
Log.w(TAG, message, throwable);
|
||||
System.out.println(PREFIX + "WARN: " + message);
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void w(String message) {
|
||||
w(message, null);
|
||||
}
|
||||
|
||||
public static void e(String message, Throwable throwable) {
|
||||
if (isEnabled(Level.ERROR)) {
|
||||
Log.e(TAG, message, throwable);
|
||||
System.out.println(PREFIX + "ERROR: " + message);
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void e(String message) {
|
||||
e(message, null);
|
||||
}
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Options {
|
||||
private Ln.Level logLevel = Ln.Level.DEBUG;
|
||||
private int maxSize;
|
||||
private int bitRate = 8000000;
|
||||
private int maxFps;
|
||||
private int lockVideoOrientation = -1;
|
||||
private boolean tunnelForward;
|
||||
private Rect crop;
|
||||
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
||||
private boolean control = true;
|
||||
private int displayId;
|
||||
private boolean showTouches;
|
||||
private boolean stayAwake;
|
||||
private List<CodecOption> codecOptions;
|
||||
private String encoderName;
|
||||
private boolean powerOffScreenOnClose;
|
||||
private boolean clipboardAutosync = true;
|
||||
|
||||
public Ln.Level getLogLevel() {
|
||||
return logLevel;
|
||||
}
|
||||
|
||||
public void setLogLevel(Ln.Level logLevel) {
|
||||
this.logLevel = logLevel;
|
||||
}
|
||||
|
||||
public int getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
public void setMaxSize(int maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public int getBitRate() {
|
||||
return bitRate;
|
||||
}
|
||||
|
||||
public void setBitRate(int bitRate) {
|
||||
this.bitRate = bitRate;
|
||||
}
|
||||
|
||||
public int getMaxFps() {
|
||||
return maxFps;
|
||||
}
|
||||
|
||||
public void setMaxFps(int maxFps) {
|
||||
this.maxFps = maxFps;
|
||||
}
|
||||
|
||||
public int getLockVideoOrientation() {
|
||||
return lockVideoOrientation;
|
||||
}
|
||||
|
||||
public void setLockVideoOrientation(int lockVideoOrientation) {
|
||||
this.lockVideoOrientation = lockVideoOrientation;
|
||||
}
|
||||
|
||||
public boolean isTunnelForward() {
|
||||
return tunnelForward;
|
||||
}
|
||||
|
||||
public void setTunnelForward(boolean tunnelForward) {
|
||||
this.tunnelForward = tunnelForward;
|
||||
}
|
||||
|
||||
public Rect getCrop() {
|
||||
return crop;
|
||||
}
|
||||
|
||||
public void setCrop(Rect crop) {
|
||||
this.crop = crop;
|
||||
}
|
||||
|
||||
public boolean getSendFrameMeta() {
|
||||
return sendFrameMeta;
|
||||
}
|
||||
|
||||
public void setSendFrameMeta(boolean sendFrameMeta) {
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
}
|
||||
|
||||
public boolean getControl() {
|
||||
return control;
|
||||
}
|
||||
|
||||
public void setControl(boolean control) {
|
||||
this.control = control;
|
||||
}
|
||||
|
||||
public int getDisplayId() {
|
||||
return displayId;
|
||||
}
|
||||
|
||||
public void setDisplayId(int displayId) {
|
||||
this.displayId = displayId;
|
||||
}
|
||||
|
||||
public boolean getShowTouches() {
|
||||
return showTouches;
|
||||
}
|
||||
|
||||
public void setShowTouches(boolean showTouches) {
|
||||
this.showTouches = showTouches;
|
||||
}
|
||||
|
||||
public boolean getStayAwake() {
|
||||
return stayAwake;
|
||||
}
|
||||
|
||||
public void setStayAwake(boolean stayAwake) {
|
||||
this.stayAwake = stayAwake;
|
||||
}
|
||||
|
||||
public List<CodecOption> getCodecOptions() {
|
||||
return codecOptions;
|
||||
}
|
||||
|
||||
public void setCodecOptions(List<CodecOption> codecOptions) {
|
||||
this.codecOptions = codecOptions;
|
||||
}
|
||||
|
||||
public String getEncoderName() {
|
||||
return encoderName;
|
||||
}
|
||||
|
||||
public void setEncoderName(String encoderName) {
|
||||
this.encoderName = encoderName;
|
||||
}
|
||||
|
||||
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
|
||||
this.powerOffScreenOnClose = powerOffScreenOnClose;
|
||||
}
|
||||
|
||||
public boolean getPowerOffScreenOnClose() {
|
||||
return this.powerOffScreenOnClose;
|
||||
}
|
||||
|
||||
public boolean getClipboardAutosync() {
|
||||
return clipboardAutosync;
|
||||
}
|
||||
|
||||
public void setClipboardAutosync(boolean clipboardAutosync) {
|
||||
this.clipboardAutosync = clipboardAutosync;
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Point {
|
||||
private final int x;
|
||||
private final int y;
|
||||
|
||||
public Point(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Point point = (Point) o;
|
||||
return x == point.x && y == point.y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Point{" + "x=" + x + ", y=" + y + '}';
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public class Pointer {
|
||||
|
||||
/**
|
||||
* Pointer id as received from the client.
|
||||
*/
|
||||
private final long id;
|
||||
|
||||
/**
|
||||
* Local pointer id, using the lowest possible values to fill the {@link android.view.MotionEvent.PointerProperties PointerProperties}.
|
||||
*/
|
||||
private final int localId;
|
||||
|
||||
private Point point;
|
||||
private float pressure;
|
||||
private boolean up;
|
||||
|
||||
public Pointer(long id, int localId) {
|
||||
this.id = id;
|
||||
this.localId = localId;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public int getLocalId() {
|
||||
return localId;
|
||||
}
|
||||
|
||||
public Point getPoint() {
|
||||
return point;
|
||||
}
|
||||
|
||||
public void setPoint(Point point) {
|
||||
this.point = point;
|
||||
}
|
||||
|
||||
public float getPressure() {
|
||||
return pressure;
|
||||
}
|
||||
|
||||
public void setPressure(float pressure) {
|
||||
this.pressure = pressure;
|
||||
}
|
||||
|
||||
public boolean isUp() {
|
||||
return up;
|
||||
}
|
||||
|
||||
public void setUp(boolean up) {
|
||||
this.up = up;
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PointersState {
|
||||
|
||||
public static final int MAX_POINTERS = 10;
|
||||
|
||||
private final List<Pointer> pointers = new ArrayList<>();
|
||||
|
||||
private int indexOf(long id) {
|
||||
for (int i = 0; i < pointers.size(); ++i) {
|
||||
Pointer pointer = pointers.get(i);
|
||||
if (pointer.getId() == id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean isLocalIdAvailable(int localId) {
|
||||
for (int i = 0; i < pointers.size(); ++i) {
|
||||
Pointer pointer = pointers.get(i);
|
||||
if (pointer.getLocalId() == localId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int nextUnusedLocalId() {
|
||||
for (int localId = 0; localId < MAX_POINTERS; ++localId) {
|
||||
if (isLocalIdAvailable(localId)) {
|
||||
return localId;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public Pointer get(int index) {
|
||||
return pointers.get(index);
|
||||
}
|
||||
|
||||
public int getPointerIndex(long id) {
|
||||
int index = indexOf(id);
|
||||
if (index != -1) {
|
||||
// already exists, return it
|
||||
return index;
|
||||
}
|
||||
if (pointers.size() >= MAX_POINTERS) {
|
||||
// it's full
|
||||
return -1;
|
||||
}
|
||||
// id 0 is reserved for mouse events
|
||||
int localId = nextUnusedLocalId();
|
||||
if (localId == -1) {
|
||||
throw new AssertionError("pointers.size() < maxFingers implies that a local id is available");
|
||||
}
|
||||
Pointer pointer = new Pointer(id, localId);
|
||||
pointers.add(pointer);
|
||||
// return the index of the pointer
|
||||
return pointers.size() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the motion event parameters.
|
||||
*
|
||||
* @param props the pointer properties
|
||||
* @param coords the pointer coordinates
|
||||
* @return The number of items initialized (the number of pointers).
|
||||
*/
|
||||
public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) {
|
||||
int count = pointers.size();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Pointer pointer = pointers.get(i);
|
||||
|
||||
// id 0 is reserved for mouse events
|
||||
props[i].id = pointer.getLocalId();
|
||||
|
||||
Point point = pointer.getPoint();
|
||||
coords[i].x = point.getX();
|
||||
coords[i].y = point.getY();
|
||||
coords[i].pressure = pointer.getPressure();
|
||||
}
|
||||
cleanUp();
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all pointers which are UP.
|
||||
*/
|
||||
private void cleanUp() {
|
||||
for (int i = pointers.size() - 1; i >= 0; --i) {
|
||||
Pointer pointer = pointers.get(i);
|
||||
if (pointer.isUp()) {
|
||||
pointers.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Position {
|
||||
private Point point;
|
||||
private Size screenSize;
|
||||
|
||||
public Position(Point point, Size screenSize) {
|
||||
this.point = point;
|
||||
this.screenSize = screenSize;
|
||||
}
|
||||
|
||||
public Position(int x, int y, int screenWidth, int screenHeight) {
|
||||
this(new Point(x, y), new Size(screenWidth, screenHeight));
|
||||
}
|
||||
|
||||
public Point getPoint() {
|
||||
return point;
|
||||
}
|
||||
|
||||
public Size getScreenSize() {
|
||||
return screenSize;
|
||||
}
|
||||
|
||||
public Position rotate(int rotation) {
|
||||
switch (rotation) {
|
||||
case 1:
|
||||
return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate());
|
||||
case 2:
|
||||
return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize);
|
||||
case 3:
|
||||
return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate());
|
||||
default:
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Position position = (Position) o;
|
||||
return Objects.equals(point, position.point) && Objects.equals(screenSize, position.screenSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(point, screenSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Position{" + "point=" + point + ", screenSize=" + screenSize + '}';
|
||||
}
|
||||
|
||||
}
|
|
@ -1,255 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
||||
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
|
||||
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
|
||||
|
||||
private static final int NO_PTS = -1;
|
||||
|
||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||
|
||||
private String encoderName;
|
||||
private List<CodecOption> codecOptions;
|
||||
private int bitRate;
|
||||
private int maxFps;
|
||||
private boolean sendFrameMeta;
|
||||
private long ptsOrigin;
|
||||
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName) {
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
this.bitRate = bitRate;
|
||||
this.maxFps = maxFps;
|
||||
this.codecOptions = codecOptions;
|
||||
this.encoderName = encoderName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
rotationChanged.set(true);
|
||||
}
|
||||
|
||||
public boolean consumeRotationChange() {
|
||||
return rotationChanged.getAndSet(false);
|
||||
}
|
||||
|
||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||
Workarounds.prepareMainLooper();
|
||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
||||
// <https://github.com/Genymobile/scrcpy/issues/240>
|
||||
// <https://github.com/Genymobile/scrcpy/issues/2656>
|
||||
Workarounds.fillAppInfo();
|
||||
}
|
||||
|
||||
internalStreamScreen(device, fd);
|
||||
}
|
||||
|
||||
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||
device.setRotationListener(this);
|
||||
boolean alive;
|
||||
try {
|
||||
do {
|
||||
MediaCodec codec = createCodec(encoderName);
|
||||
IBinder display = createDisplay();
|
||||
ScreenInfo screenInfo = device.getScreenInfo();
|
||||
Rect contentRect = screenInfo.getContentRect();
|
||||
// include the locked video orientation
|
||||
Rect videoRect = screenInfo.getVideoSize().toRect();
|
||||
// does not include the locked video orientation
|
||||
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||
int videoRotation = screenInfo.getVideoRotation();
|
||||
int layerStack = device.getLayerStack();
|
||||
|
||||
setSize(format, videoRect.width(), videoRect.height());
|
||||
configure(codec, format);
|
||||
Surface surface = codec.createInputSurface();
|
||||
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
||||
codec.start();
|
||||
try {
|
||||
alive = encode(codec, fd);
|
||||
// do not call stop() on exception, it would trigger an IllegalStateException
|
||||
codec.stop();
|
||||
} finally {
|
||||
destroyDisplay(display);
|
||||
codec.release();
|
||||
surface.release();
|
||||
}
|
||||
} while (alive);
|
||||
} finally {
|
||||
device.setRotationListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
|
||||
boolean eof = false;
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
while (!consumeRotationChange() && !eof) {
|
||||
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
|
||||
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
try {
|
||||
if (consumeRotationChange()) {
|
||||
// must restart encoding with new size
|
||||
break;
|
||||
}
|
||||
if (outputBufferId >= 0) {
|
||||
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
|
||||
|
||||
if (sendFrameMeta) {
|
||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||
}
|
||||
|
||||
IO.writeFully(fd, codecBuffer);
|
||||
}
|
||||
} finally {
|
||||
if (outputBufferId >= 0) {
|
||||
codec.releaseOutputBuffer(outputBufferId, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !eof;
|
||||
}
|
||||
|
||||
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
||||
headerBuffer.clear();
|
||||
|
||||
long pts;
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||
pts = NO_PTS; // non-media data packet
|
||||
} else {
|
||||
if (ptsOrigin == 0) {
|
||||
ptsOrigin = bufferInfo.presentationTimeUs;
|
||||
}
|
||||
pts = bufferInfo.presentationTimeUs - ptsOrigin;
|
||||
}
|
||||
|
||||
headerBuffer.putLong(pts);
|
||||
headerBuffer.putInt(packetSize);
|
||||
headerBuffer.flip();
|
||||
IO.writeFully(fd, headerBuffer);
|
||||
}
|
||||
|
||||
private static MediaCodecInfo[] listEncoders() {
|
||||
List<MediaCodecInfo> result = new ArrayList<>();
|
||||
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
|
||||
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
|
||||
result.add(codecInfo);
|
||||
}
|
||||
}
|
||||
return result.toArray(new MediaCodecInfo[result.size()]);
|
||||
}
|
||||
|
||||
private static MediaCodec createCodec(String encoderName) throws IOException {
|
||||
if (encoderName != null) {
|
||||
Ln.d("Creating encoder by name: '" + encoderName + "'");
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
MediaCodecInfo[] encoders = listEncoders();
|
||||
throw new InvalidEncoderException(encoderName, encoders);
|
||||
}
|
||||
}
|
||||
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
Ln.d("Using encoder: '" + codec.getName() + "'");
|
||||
return codec;
|
||||
}
|
||||
|
||||
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
|
||||
String key = codecOption.getKey();
|
||||
Object value = codecOption.getValue();
|
||||
|
||||
if (value instanceof Integer) {
|
||||
format.setInteger(key, (Integer) value);
|
||||
} else if (value instanceof Long) {
|
||||
format.setLong(key, (Long) value);
|
||||
} else if (value instanceof Float) {
|
||||
format.setFloat(key, (Float) value);
|
||||
} else if (value instanceof String) {
|
||||
format.setString(key, (String) value);
|
||||
}
|
||||
|
||||
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
||||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL);
|
||||
// display the very first frame, and recover from bad quality when no new frames
|
||||
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
|
||||
if (maxFps > 0) {
|
||||
// The key existed privately before Android 10:
|
||||
// <https://android.googlesource.com/platform/frameworks/base/+/625f0aad9f7a259b6881006ad8710adce57d1384%5E%21/>
|
||||
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
|
||||
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
|
||||
}
|
||||
|
||||
if (codecOptions != null) {
|
||||
for (CodecOption option : codecOptions) {
|
||||
setCodecOption(format, option);
|
||||
}
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
private static IBinder createDisplay() {
|
||||
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
|
||||
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
|
||||
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S"
|
||||
.equals(Build.VERSION.CODENAME));
|
||||
return SurfaceControl.createDisplay("scrcpy", secure);
|
||||
}
|
||||
|
||||
private static void configure(MediaCodec codec, MediaFormat format) {
|
||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
}
|
||||
|
||||
private static void setSize(MediaFormat format, int width, int height) {
|
||||
format.setInteger(MediaFormat.KEY_WIDTH, width);
|
||||
format.setInteger(MediaFormat.KEY_HEIGHT, height);
|
||||
}
|
||||
|
||||
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
|
||||
SurfaceControl.openTransaction();
|
||||
try {
|
||||
SurfaceControl.setDisplaySurface(display, surface);
|
||||
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
|
||||
SurfaceControl.setDisplayLayerStack(display, layerStack);
|
||||
} finally {
|
||||
SurfaceControl.closeTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private static void destroyDisplay(IBinder display) {
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
public final class ScreenInfo {
|
||||
/**
|
||||
* Device (physical) size, possibly cropped
|
||||
*/
|
||||
private final Rect contentRect; // device size, possibly cropped
|
||||
|
||||
/**
|
||||
* Video size, possibly smaller than the device size, already taking the device rotation and crop into account.
|
||||
* <p>
|
||||
* However, it does not include the locked video orientation.
|
||||
*/
|
||||
private final Size unlockedVideoSize;
|
||||
|
||||
/**
|
||||
* Device rotation, related to the natural device orientation (0, 1, 2 or 3)
|
||||
*/
|
||||
private final int deviceRotation;
|
||||
|
||||
/**
|
||||
* The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW)
|
||||
*/
|
||||
private final int lockedVideoOrientation;
|
||||
|
||||
public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) {
|
||||
this.contentRect = contentRect;
|
||||
this.unlockedVideoSize = unlockedVideoSize;
|
||||
this.deviceRotation = deviceRotation;
|
||||
this.lockedVideoOrientation = lockedVideoOrientation;
|
||||
}
|
||||
|
||||
public Rect getContentRect() {
|
||||
return contentRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the video size as if locked video orientation was not set.
|
||||
*
|
||||
* @return the unlocked video size
|
||||
*/
|
||||
public Size getUnlockedVideoSize() {
|
||||
return unlockedVideoSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual video size if locked video orientation is set.
|
||||
*
|
||||
* @return the actual video size
|
||||
*/
|
||||
public Size getVideoSize() {
|
||||
if (getVideoRotation() % 2 == 0) {
|
||||
return unlockedVideoSize;
|
||||
}
|
||||
|
||||
return unlockedVideoSize.rotate();
|
||||
}
|
||||
|
||||
public int getDeviceRotation() {
|
||||
return deviceRotation;
|
||||
}
|
||||
|
||||
public ScreenInfo withDeviceRotation(int newDeviceRotation) {
|
||||
if (newDeviceRotation == deviceRotation) {
|
||||
return this;
|
||||
}
|
||||
// true if changed between portrait and landscape
|
||||
boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0;
|
||||
Rect newContentRect;
|
||||
Size newUnlockedVideoSize;
|
||||
if (orientationChanged) {
|
||||
newContentRect = flipRect(contentRect);
|
||||
newUnlockedVideoSize = unlockedVideoSize.rotate();
|
||||
} else {
|
||||
newContentRect = contentRect;
|
||||
newUnlockedVideoSize = unlockedVideoSize;
|
||||
}
|
||||
return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation);
|
||||
}
|
||||
|
||||
public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
|
||||
int rotation = displayInfo.getRotation();
|
||||
|
||||
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) {
|
||||
// The user requested to lock the video orientation to the current orientation
|
||||
lockedVideoOrientation = rotation;
|
||||
}
|
||||
|
||||
Size deviceSize = displayInfo.getSize();
|
||||
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
|
||||
if (crop != null) {
|
||||
if (rotation % 2 != 0) { // 180s preserve dimensions
|
||||
// the crop (provided by the user) is expressed in the natural orientation
|
||||
crop = flipRect(crop);
|
||||
}
|
||||
if (!contentRect.intersect(crop)) {
|
||||
// intersect() changes contentRect so that it is intersected with crop
|
||||
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
|
||||
contentRect = new Rect(); // empty
|
||||
}
|
||||
}
|
||||
|
||||
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
|
||||
return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation);
|
||||
}
|
||||
|
||||
private static String formatCrop(Rect rect) {
|
||||
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
|
||||
}
|
||||
|
||||
private static Size computeVideoSize(int w, int h, int maxSize) {
|
||||
// Compute the video size and the padding of the content inside this video.
|
||||
// Principle:
|
||||
// - scale down the great side of the screen to maxSize (if necessary);
|
||||
// - scale down the other side so that the aspect ratio is preserved;
|
||||
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
|
||||
w &= ~7; // in case it's not a multiple of 8
|
||||
h &= ~7;
|
||||
if (maxSize > 0) {
|
||||
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
|
||||
throw new AssertionError("Max size must be a multiple of 8");
|
||||
}
|
||||
boolean portrait = h > w;
|
||||
int major = portrait ? h : w;
|
||||
int minor = portrait ? w : h;
|
||||
if (major > maxSize) {
|
||||
int minorExact = minor * maxSize / major;
|
||||
// +4 to round the value to the nearest multiple of 8
|
||||
minor = (minorExact + 4) & ~7;
|
||||
major = maxSize;
|
||||
}
|
||||
w = portrait ? minor : major;
|
||||
h = portrait ? major : minor;
|
||||
}
|
||||
return new Size(w, h);
|
||||
}
|
||||
|
||||
private static Rect flipRect(Rect crop) {
|
||||
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the rotation to apply to the device rotation to get the requested locked video orientation
|
||||
*
|
||||
* @return the rotation offset
|
||||
*/
|
||||
public int getVideoRotation() {
|
||||
if (lockedVideoOrientation == -1) {
|
||||
// no offset
|
||||
return 0;
|
||||
}
|
||||
return (deviceRotation + 4 - lockedVideoOrientation) % 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the rotation to apply to the requested locked video orientation to get the device rotation
|
||||
*
|
||||
* @return the (reverse) rotation offset
|
||||
*/
|
||||
public int getReverseVideoRotation() {
|
||||
if (lockedVideoOrientation == -1) {
|
||||
// no offset
|
||||
return 0;
|
||||
}
|
||||
return (lockedVideoOrientation + 4 - deviceRotation) % 4;
|
||||
}
|
||||
}
|
|
@ -1,311 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class Server {
|
||||
|
||||
|
||||
private Server() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
private static void initAndCleanUp(Options options) {
|
||||
boolean mustDisableShowTouchesOnCleanUp = false;
|
||||
int restoreStayOn = -1;
|
||||
if (options.getShowTouches() || options.getStayAwake()) {
|
||||
Settings settings = Device.getSettings();
|
||||
if (options.getShowTouches()) {
|
||||
try {
|
||||
String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
|
||||
// If "show touches" was disabled, it must be disabled back on clean up
|
||||
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"show_touches\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.getStayAwake()) {
|
||||
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
||||
try {
|
||||
String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
||||
try {
|
||||
restoreStayOn = Integer.parseInt(oldValue);
|
||||
if (restoreStayOn == stayOn) {
|
||||
// No need to restore
|
||||
restoreStayOn = -1;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
restoreStayOn = 0;
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
|
||||
} catch (IOException e) {
|
||||
Ln.e("Could not configure cleanup", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void scrcpy(Options options) throws IOException {
|
||||
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
||||
final Device device = new Device(options);
|
||||
List<CodecOption> codecOptions = options.getCodecOptions();
|
||||
|
||||
Thread initThread = startInitThread(options);
|
||||
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
|
||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||
options.getEncoderName());
|
||||
|
||||
Thread controllerThread = null;
|
||||
Thread deviceMessageSenderThread = null;
|
||||
if (options.getControl()) {
|
||||
final Controller controller = new Controller(device, connection, options.getClipboardAutosync());
|
||||
|
||||
// asynchronous
|
||||
controllerThread = startController(controller);
|
||||
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
|
||||
|
||||
device.setClipboardListener(new Device.ClipboardListener() {
|
||||
@Override
|
||||
public void onClipboardTextChanged(String text) {
|
||||
controller.getSender().pushClipboardText(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// synchronous
|
||||
screenEncoder.streamScreen(device, connection.getVideoFd());
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Screen streaming stopped");
|
||||
} finally {
|
||||
initThread.interrupt();
|
||||
if (controllerThread != null) {
|
||||
controllerThread.interrupt();
|
||||
}
|
||||
if (deviceMessageSenderThread != null) {
|
||||
deviceMessageSenderThread.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Thread startInitThread(final Options options) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initAndCleanUp(options);
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static Thread startController(final Controller controller) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
controller.control();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Controller stopped");
|
||||
}
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
sender.loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static Options createOptions(String... args) {
|
||||
if (args.length < 1) {
|
||||
throw new IllegalArgumentException("Missing client version");
|
||||
}
|
||||
|
||||
String clientVersion = args[0];
|
||||
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
|
||||
throw new IllegalArgumentException(
|
||||
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
||||
}
|
||||
|
||||
Options options = new Options();
|
||||
|
||||
for (int i = 1; i < args.length; ++i) {
|
||||
String arg = args[i];
|
||||
int equalIndex = arg.indexOf('=');
|
||||
if (equalIndex == -1) {
|
||||
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
|
||||
}
|
||||
String key = arg.substring(0, equalIndex);
|
||||
String value = arg.substring(equalIndex + 1);
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
||||
options.setLogLevel(level);
|
||||
break;
|
||||
case "max_size":
|
||||
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
||||
options.setMaxSize(maxSize);
|
||||
break;
|
||||
case "bit_rate":
|
||||
int bitRate = Integer.parseInt(value);
|
||||
options.setBitRate(bitRate);
|
||||
break;
|
||||
case "max_fps":
|
||||
int maxFps = Integer.parseInt(value);
|
||||
options.setMaxFps(maxFps);
|
||||
break;
|
||||
case "lock_video_orientation":
|
||||
int lockVideoOrientation = Integer.parseInt(value);
|
||||
options.setLockVideoOrientation(lockVideoOrientation);
|
||||
break;
|
||||
case "tunnel_forward":
|
||||
boolean tunnelForward = Boolean.parseBoolean(value);
|
||||
options.setTunnelForward(tunnelForward);
|
||||
break;
|
||||
case "crop":
|
||||
Rect crop = parseCrop(value);
|
||||
options.setCrop(crop);
|
||||
break;
|
||||
case "send_frame_meta":
|
||||
boolean sendFrameMeta = Boolean.parseBoolean(value);
|
||||
options.setSendFrameMeta(sendFrameMeta);
|
||||
break;
|
||||
case "control":
|
||||
boolean control = Boolean.parseBoolean(value);
|
||||
options.setControl(control);
|
||||
break;
|
||||
case "display_id":
|
||||
int displayId = Integer.parseInt(value);
|
||||
options.setDisplayId(displayId);
|
||||
break;
|
||||
case "show_touches":
|
||||
boolean showTouches = Boolean.parseBoolean(value);
|
||||
options.setShowTouches(showTouches);
|
||||
break;
|
||||
case "stay_awake":
|
||||
boolean stayAwake = Boolean.parseBoolean(value);
|
||||
options.setStayAwake(stayAwake);
|
||||
break;
|
||||
case "codec_options":
|
||||
List<CodecOption> codecOptions = CodecOption.parse(value);
|
||||
options.setCodecOptions(codecOptions);
|
||||
break;
|
||||
case "encoder_name":
|
||||
if (!value.isEmpty()) {
|
||||
options.setEncoderName(value);
|
||||
}
|
||||
break;
|
||||
case "power_off_on_close":
|
||||
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
|
||||
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
|
||||
break;
|
||||
case "clipboard_autosync":
|
||||
boolean clipboardAutosync = Boolean.parseBoolean(value);
|
||||
options.setClipboardAutosync(clipboardAutosync);
|
||||
break;
|
||||
default:
|
||||
Ln.w("Unknown server option: " + key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static Rect parseCrop(String crop) {
|
||||
if (crop.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// input format: "width:height:x:y"
|
||||
String[] tokens = crop.split(":");
|
||||
if (tokens.length != 4) {
|
||||
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
|
||||
}
|
||||
int width = Integer.parseInt(tokens[0]);
|
||||
int height = Integer.parseInt(tokens[1]);
|
||||
int x = Integer.parseInt(tokens[2]);
|
||||
int y = Integer.parseInt(tokens[3]);
|
||||
return new Rect(x, y, x + width, y + height);
|
||||
}
|
||||
|
||||
private static void suggestFix(Throwable e) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (e instanceof MediaCodec.CodecException) {
|
||||
MediaCodec.CodecException mce = (MediaCodec.CodecException) e;
|
||||
if (mce.getErrorCode() == 0xfffffc0e) {
|
||||
Ln.e("The hardware encoder is not able to encode at the given definition.");
|
||||
Ln.e("Try with a lower definition:");
|
||||
Ln.e(" scrcpy -m 1024");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e instanceof InvalidDisplayIdException) {
|
||||
InvalidDisplayIdException idie = (InvalidDisplayIdException) e;
|
||||
int[] displayIds = idie.getAvailableDisplayIds();
|
||||
if (displayIds != null && displayIds.length > 0) {
|
||||
Ln.e("Try to use one of the available display ids:");
|
||||
for (int id : displayIds) {
|
||||
Ln.e(" scrcpy --display " + id);
|
||||
}
|
||||
}
|
||||
} else if (e instanceof InvalidEncoderException) {
|
||||
InvalidEncoderException iee = (InvalidEncoderException) e;
|
||||
MediaCodecInfo[] encoders = iee.getAvailableEncoders();
|
||||
if (encoders != null && encoders.length > 0) {
|
||||
Ln.e("Try to use one of the available encoders:");
|
||||
for (MediaCodecInfo encoder : encoders) {
|
||||
Ln.e(" scrcpy --encoder '" + encoder.getName() + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String... args) throws Exception {
|
||||
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Ln.e("Exception on thread " + t, e);
|
||||
suggestFix(e);
|
||||
}
|
||||
});
|
||||
|
||||
Options options = createOptions(args);
|
||||
|
||||
Ln.initLogLevel(options.getLogLevel());
|
||||
|
||||
scrcpy(options);
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class Settings {
|
||||
|
||||
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
|
||||
public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE;
|
||||
public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL;
|
||||
|
||||
private final ServiceManager serviceManager;
|
||||
|
||||
public Settings(ServiceManager serviceManager) {
|
||||
this.serviceManager = serviceManager;
|
||||
}
|
||||
|
||||
private static void execSettingsPut(String table, String key, String value) throws SettingsException {
|
||||
try {
|
||||
Command.exec("settings", "put", table, key, value);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new SettingsException("put", table, key, value, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String execSettingsGet(String table, String key) throws SettingsException {
|
||||
try {
|
||||
return Command.execReadLine("settings", "get", table, key);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new SettingsException("get", table, key, null, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getValue(String table, String key) throws SettingsException {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||
return provider.getValue(table, key);
|
||||
} catch (SettingsException e) {
|
||||
Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e);
|
||||
}
|
||||
}
|
||||
|
||||
return execSettingsGet(table, key);
|
||||
}
|
||||
|
||||
public void putValue(String table, String key, String value) throws SettingsException {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||
provider.putValue(table, key, value);
|
||||
} catch (SettingsException e) {
|
||||
Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e);
|
||||
}
|
||||
}
|
||||
|
||||
execSettingsPut(table, key, value);
|
||||
}
|
||||
|
||||
public String getAndPutValue(String table, String key, String value) throws SettingsException {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||
String oldValue = provider.getValue(table, key);
|
||||
if (!value.equals(oldValue)) {
|
||||
provider.putValue(table, key, value);
|
||||
}
|
||||
return oldValue;
|
||||
} catch (SettingsException e) {
|
||||
Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e);
|
||||
}
|
||||
}
|
||||
|
||||
String oldValue = getValue(table, key);
|
||||
if (!value.equals(oldValue)) {
|
||||
putValue(table, key, value);
|
||||
}
|
||||
return oldValue;
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public class SettingsException extends Exception {
|
||||
private static String createMessage(String method, String table, String key, String value) {
|
||||
return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : "");
|
||||
}
|
||||
|
||||
public SettingsException(String method, String table, String key, String value, Throwable cause) {
|
||||
super(createMessage(method, table, key, value), cause);
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class Size {
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
public Size(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public Size rotate() {
|
||||
return new Size(height, width);
|
||||
}
|
||||
|
||||
public Rect toRect() {
|
||||
return new Rect(0, 0, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Size size = (Size) o;
|
||||
return width == size.width && height == size.height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Size{" + "width=" + width + ", height=" + height + '}';
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public final class StringUtils {
|
||||
private StringUtils() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) {
|
||||
int len = utf8.length;
|
||||
if (len <= maxLength) {
|
||||
return len;
|
||||
}
|
||||
len = maxLength;
|
||||
// see UTF-8 encoding <https://en.wikipedia.org/wiki/UTF-8#Description>
|
||||
while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) {
|
||||
// the next byte is not the start of a new UTF-8 codepoint
|
||||
// so if we would cut there, the character would be truncated
|
||||
len--;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class Workarounds {
|
||||
private Workarounds() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void prepareMainLooper() {
|
||||
// Some devices internally create a Handler when creating an input Surface, causing an exception:
|
||||
// "Can't create handler inside thread that has not called Looper.prepare()"
|
||||
// <https://github.com/Genymobile/scrcpy/issues/240>
|
||||
//
|
||||
// Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException:
|
||||
// "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue'
|
||||
// on a null object reference"
|
||||
// <https://github.com/Genymobile/scrcpy/issues/921>
|
||||
Looper.prepareMainLooper();
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public static void fillAppInfo() {
|
||||
try {
|
||||
// ActivityThread activityThread = new ActivityThread();
|
||||
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
|
||||
activityThreadConstructor.setAccessible(true);
|
||||
Object activityThread = activityThreadConstructor.newInstance();
|
||||
|
||||
// ActivityThread.sCurrentActivityThread = activityThread;
|
||||
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
|
||||
sCurrentActivityThreadField.setAccessible(true);
|
||||
sCurrentActivityThreadField.set(null, activityThread);
|
||||
|
||||
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
|
||||
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
|
||||
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor();
|
||||
appBindDataConstructor.setAccessible(true);
|
||||
Object appBindData = appBindDataConstructor.newInstance();
|
||||
|
||||
ApplicationInfo applicationInfo = new ApplicationInfo();
|
||||
applicationInfo.packageName = "com.genymobile.scrcpy";
|
||||
|
||||
// appBindData.appInfo = applicationInfo;
|
||||
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
|
||||
appInfoField.setAccessible(true);
|
||||
appInfoField.set(appBindData, applicationInfo);
|
||||
|
||||
// activityThread.mBoundApplication = appBindData;
|
||||
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
|
||||
mBoundApplicationField.setAccessible(true);
|
||||
mBoundApplicationField.set(activityThread, appBindData);
|
||||
|
||||
// Context ctx = activityThread.getSystemContext();
|
||||
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
|
||||
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
|
||||
|
||||
Application app = Instrumentation.newApplication(Application.class, ctx);
|
||||
|
||||
// activityThread.mInitialApplication = app;
|
||||
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
||||
mInitialApplicationField.setAccessible(true);
|
||||
mInitialApplicationField.set(activityThread, app);
|
||||
} catch (Throwable throwable) {
|
||||
// this is a workaround, so failing is not an error
|
||||
Ln.d("Could not fill app info: " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class ActivityManager {
|
||||
|
||||
private final IInterface manager;
|
||||
private Method getContentProviderExternalMethod;
|
||||
private boolean getContentProviderExternalMethodNewVersion = true;
|
||||
private Method removeContentProviderExternalMethod;
|
||||
|
||||
public ActivityManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private Method getGetContentProviderExternalMethod() throws NoSuchMethodException {
|
||||
if (getContentProviderExternalMethod == null) {
|
||||
try {
|
||||
getContentProviderExternalMethod = manager.getClass()
|
||||
.getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// old version
|
||||
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
|
||||
getContentProviderExternalMethodNewVersion = false;
|
||||
}
|
||||
}
|
||||
return getContentProviderExternalMethod;
|
||||
}
|
||||
|
||||
private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException {
|
||||
if (removeContentProviderExternalMethod == null) {
|
||||
removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class);
|
||||
}
|
||||
return removeContentProviderExternalMethod;
|
||||
}
|
||||
|
||||
private ContentProvider getContentProviderExternal(String name, IBinder token) {
|
||||
try {
|
||||
Method method = getGetContentProviderExternalMethod();
|
||||
Object[] args;
|
||||
if (getContentProviderExternalMethodNewVersion) {
|
||||
// new version
|
||||
args = new Object[]{name, ServiceManager.USER_ID, token, null};
|
||||
} else {
|
||||
// old version
|
||||
args = new Object[]{name, ServiceManager.USER_ID, token};
|
||||
}
|
||||
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
|
||||
Object providerHolder = method.invoke(manager, args);
|
||||
if (providerHolder == null) {
|
||||
return null;
|
||||
}
|
||||
// IContentProvider provider = providerHolder.provider;
|
||||
Field providerField = providerHolder.getClass().getDeclaredField("provider");
|
||||
providerField.setAccessible(true);
|
||||
Object provider = providerField.get(providerHolder);
|
||||
if (provider == null) {
|
||||
return null;
|
||||
}
|
||||
return new ContentProvider(this, provider, name, token);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void removeContentProviderExternal(String name, IBinder token) {
|
||||
try {
|
||||
Method method = getRemoveContentProviderExternalMethod();
|
||||
method.invoke(manager, name, token);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
public ContentProvider createSettingsProvider() {
|
||||
return getContentProviderExternal("settings", new Binder());
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class ClipboardManager {
|
||||
private final IInterface manager;
|
||||
private Method getPrimaryClipMethod;
|
||||
private Method setPrimaryClipMethod;
|
||||
private Method addPrimaryClipChangedListener;
|
||||
|
||||
public ClipboardManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
|
||||
if (getPrimaryClipMethod == null) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
|
||||
} else {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
|
||||
}
|
||||
}
|
||||
return getPrimaryClipMethod;
|
||||
}
|
||||
|
||||
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
|
||||
if (setPrimaryClipMethod == null) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
|
||||
} else {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
|
||||
}
|
||||
}
|
||||
return setPrimaryClipMethod;
|
||||
}
|
||||
|
||||
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
|
||||
}
|
||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||
}
|
||||
|
||||
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
|
||||
} else {
|
||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
try {
|
||||
Method method = getGetPrimaryClipMethod();
|
||||
ClipData clipData = getPrimaryClip(method, manager);
|
||||
if (clipData == null || clipData.getItemCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
return clipData.getItemAt(0).getText();
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setText(CharSequence text) {
|
||||
try {
|
||||
Method method = getSetPrimaryClipMethod();
|
||||
ClipData clipData = ClipData.newPlainText(null, text);
|
||||
setPrimaryClip(method, manager, clipData);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
|
||||
} else {
|
||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
|
||||
if (addPrimaryClipChangedListener == null) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
|
||||
} else {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
||||
}
|
||||
}
|
||||
return addPrimaryClipChangedListener;
|
||||
}
|
||||
|
||||
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
||||
try {
|
||||
Method method = getAddPrimaryClipChangedListener();
|
||||
addPrimaryClipChangedListener(method, manager, listener);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
import com.genymobile.scrcpy.SettingsException;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class ContentProvider implements Closeable {
|
||||
|
||||
public static final String TABLE_SYSTEM = "system";
|
||||
public static final String TABLE_SECURE = "secure";
|
||||
public static final String TABLE_GLOBAL = "global";
|
||||
|
||||
// See android/providerHolder/Settings.java
|
||||
private static final String CALL_METHOD_GET_SYSTEM = "GET_system";
|
||||
private static final String CALL_METHOD_GET_SECURE = "GET_secure";
|
||||
private static final String CALL_METHOD_GET_GLOBAL = "GET_global";
|
||||
|
||||
private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system";
|
||||
private static final String CALL_METHOD_PUT_SECURE = "PUT_secure";
|
||||
private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global";
|
||||
|
||||
private static final String CALL_METHOD_USER_KEY = "_user";
|
||||
|
||||
private static final String NAME_VALUE_TABLE_VALUE = "value";
|
||||
|
||||
private final ActivityManager manager;
|
||||
// android.content.IContentProvider
|
||||
private final Object provider;
|
||||
private final String name;
|
||||
private final IBinder token;
|
||||
|
||||
private Method callMethod;
|
||||
private int callMethodVersion;
|
||||
|
||||
private Object attributionSource;
|
||||
|
||||
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
|
||||
this.manager = manager;
|
||||
this.provider = provider;
|
||||
this.name = name;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private Method getCallMethod() throws NoSuchMethodException {
|
||||
if (callMethod == null) {
|
||||
try {
|
||||
Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource");
|
||||
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class);
|
||||
callMethodVersion = 0;
|
||||
} catch (NoSuchMethodException | ClassNotFoundException e0) {
|
||||
// old versions
|
||||
try {
|
||||
callMethod = provider.getClass()
|
||||
.getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class);
|
||||
callMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
try {
|
||||
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
|
||||
callMethodVersion = 2;
|
||||
} catch (NoSuchMethodException e2) {
|
||||
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
|
||||
callMethodVersion = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return callMethod;
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private Object getAttributionSource()
|
||||
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||
if (attributionSource == null) {
|
||||
Class<?> cl = Class.forName("android.content.AttributionSource$Builder");
|
||||
Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID);
|
||||
cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME);
|
||||
attributionSource = cl.getDeclaredMethod("build").invoke(builder);
|
||||
}
|
||||
|
||||
return attributionSource;
|
||||
}
|
||||
|
||||
private Bundle call(String callMethod, String arg, Bundle extras)
|
||||
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
|
||||
try {
|
||||
Method method = getCallMethod();
|
||||
Object[] args;
|
||||
switch (callMethodVersion) {
|
||||
case 0:
|
||||
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
|
||||
break;
|
||||
case 1:
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
|
||||
break;
|
||||
case 2:
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
||||
break;
|
||||
default:
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
|
||||
break;
|
||||
}
|
||||
return (Bundle) method.invoke(provider, args);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
manager.removeContentProviderExternal(name, token);
|
||||
}
|
||||
|
||||
private static String getGetMethod(String table) {
|
||||
switch (table) {
|
||||
case TABLE_SECURE:
|
||||
return CALL_METHOD_GET_SECURE;
|
||||
case TABLE_SYSTEM:
|
||||
return CALL_METHOD_GET_SYSTEM;
|
||||
case TABLE_GLOBAL:
|
||||
return CALL_METHOD_GET_GLOBAL;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid table: " + table);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getPutMethod(String table) {
|
||||
switch (table) {
|
||||
case TABLE_SECURE:
|
||||
return CALL_METHOD_PUT_SECURE;
|
||||
case TABLE_SYSTEM:
|
||||
return CALL_METHOD_PUT_SYSTEM;
|
||||
case TABLE_GLOBAL:
|
||||
return CALL_METHOD_PUT_GLOBAL;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid table: " + table);
|
||||
}
|
||||
}
|
||||
|
||||
public String getValue(String table, String key) throws SettingsException {
|
||||
String method = getGetMethod(table);
|
||||
Bundle arg = new Bundle();
|
||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
||||
try {
|
||||
Bundle bundle = call(method, key, arg);
|
||||
if (bundle == null) {
|
||||
return null;
|
||||
}
|
||||
return bundle.getString("value");
|
||||
} catch (Exception e) {
|
||||
throw new SettingsException(table, "get", key, null, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void putValue(String table, String key, String value) throws SettingsException {
|
||||
String method = getPutMethod(table);
|
||||
Bundle arg = new Bundle();
|
||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
||||
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
||||
try {
|
||||
call(method, key, arg);
|
||||
} catch (Exception e) {
|
||||
throw new SettingsException(table, "put", key, value, e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.DisplayInfo;
|
||||
import com.genymobile.scrcpy.Size;
|
||||
|
||||
import android.os.IInterface;
|
||||
|
||||
public final class DisplayManager {
|
||||
private final IInterface manager;
|
||||
|
||||
public DisplayManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public DisplayInfo getDisplayInfo(int displayId) {
|
||||
try {
|
||||
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
|
||||
if (displayInfo == null) {
|
||||
return null;
|
||||
}
|
||||
Class<?> cls = displayInfo.getClass();
|
||||
// width and height already take the rotation into account
|
||||
int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
|
||||
int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
|
||||
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
|
||||
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
|
||||
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int[] getDisplayIds() {
|
||||
try {
|
||||
return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.os.IInterface;
|
||||
import android.view.InputEvent;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class InputManager {
|
||||
|
||||
public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0;
|
||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
|
||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
|
||||
|
||||
private final IInterface manager;
|
||||
private Method injectInputEventMethod;
|
||||
|
||||
private static Method setDisplayIdMethod;
|
||||
|
||||
public InputManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private Method getInjectInputEventMethod() throws NoSuchMethodException {
|
||||
if (injectInputEventMethod == null) {
|
||||
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
|
||||
}
|
||||
return injectInputEventMethod;
|
||||
}
|
||||
|
||||
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
||||
try {
|
||||
Method method = getInjectInputEventMethod();
|
||||
return (boolean) method.invoke(manager, inputEvent, mode);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getSetDisplayIdMethod() throws NoSuchMethodException {
|
||||
if (setDisplayIdMethod == null) {
|
||||
setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class);
|
||||
}
|
||||
return setDisplayIdMethod;
|
||||
}
|
||||
|
||||
public static boolean setDisplayId(InputEvent inputEvent, int displayId) {
|
||||
try {
|
||||
Method method = getSetDisplayIdMethod();
|
||||
method.invoke(inputEvent, displayId);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Cannot associate a display id to the input event", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class PowerManager {
|
||||
private final IInterface manager;
|
||||
private Method isScreenOnMethod;
|
||||
|
||||
public PowerManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private Method getIsScreenOnMethod() throws NoSuchMethodException {
|
||||
if (isScreenOnMethod == null) {
|
||||
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
|
||||
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
|
||||
isScreenOnMethod = manager.getClass().getMethod(methodName);
|
||||
}
|
||||
return isScreenOnMethod;
|
||||
}
|
||||
|
||||
public boolean isScreenOn() {
|
||||
try {
|
||||
Method method = getIsScreenOnMethod();
|
||||
return (boolean) method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public final class ServiceManager {
|
||||
|
||||
public static final String PACKAGE_NAME = "com.android.shell";
|
||||
public static final int USER_ID = 0;
|
||||
|
||||
private final Method getServiceMethod;
|
||||
|
||||
private WindowManager windowManager;
|
||||
private DisplayManager displayManager;
|
||||
private InputManager inputManager;
|
||||
private PowerManager powerManager;
|
||||
private StatusBarManager statusBarManager;
|
||||
private ClipboardManager clipboardManager;
|
||||
private ActivityManager activityManager;
|
||||
|
||||
public ServiceManager() {
|
||||
try {
|
||||
getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private IInterface getService(String service, String type) {
|
||||
try {
|
||||
IBinder binder = (IBinder) getServiceMethod.invoke(null, service);
|
||||
Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);
|
||||
return (IInterface) asInterfaceMethod.invoke(null, binder);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WindowManager getWindowManager() {
|
||||
if (windowManager == null) {
|
||||
windowManager = new WindowManager(getService("window", "android.view.IWindowManager"));
|
||||
}
|
||||
return windowManager;
|
||||
}
|
||||
|
||||
public DisplayManager getDisplayManager() {
|
||||
if (displayManager == null) {
|
||||
displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager"));
|
||||
}
|
||||
return displayManager;
|
||||
}
|
||||
|
||||
public InputManager getInputManager() {
|
||||
if (inputManager == null) {
|
||||
inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager"));
|
||||
}
|
||||
return inputManager;
|
||||
}
|
||||
|
||||
public PowerManager getPowerManager() {
|
||||
if (powerManager == null) {
|
||||
powerManager = new PowerManager(getService("power", "android.os.IPowerManager"));
|
||||
}
|
||||
return powerManager;
|
||||
}
|
||||
|
||||
public StatusBarManager getStatusBarManager() {
|
||||
if (statusBarManager == null) {
|
||||
statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService"));
|
||||
}
|
||||
return statusBarManager;
|
||||
}
|
||||
|
||||
public ClipboardManager getClipboardManager() {
|
||||
if (clipboardManager == null) {
|
||||
IInterface clipboard = getService("clipboard", "android.content.IClipboard");
|
||||
if (clipboard == null) {
|
||||
// Some devices have no clipboard manager
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1440>
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1556>
|
||||
return null;
|
||||
}
|
||||
clipboardManager = new ClipboardManager(clipboard);
|
||||
}
|
||||
return clipboardManager;
|
||||
}
|
||||
|
||||
public ActivityManager getActivityManager() {
|
||||
if (activityManager == null) {
|
||||
try {
|
||||
// On old Android versions, the ActivityManager is not exposed via AIDL,
|
||||
// so use ActivityManagerNative.getDefault()
|
||||
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
|
||||
Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
|
||||
IInterface am = (IInterface) getDefaultMethod.invoke(null);
|
||||
activityManager = new ActivityManager(am);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return activityManager;
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class StatusBarManager {
|
||||
|
||||
private final IInterface manager;
|
||||
private Method expandNotificationsPanelMethod;
|
||||
private boolean expandNotificationPanelMethodCustomVersion;
|
||||
private Method expandSettingsPanelMethod;
|
||||
private boolean expandSettingsPanelMethodNewVersion = true;
|
||||
private Method collapsePanelsMethod;
|
||||
|
||||
public StatusBarManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException {
|
||||
if (expandNotificationsPanelMethod == null) {
|
||||
try {
|
||||
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Custom version for custom vendor ROM: <https://github.com/Genymobile/scrcpy/issues/2551>
|
||||
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel", int.class);
|
||||
expandNotificationPanelMethodCustomVersion = true;
|
||||
}
|
||||
}
|
||||
return expandNotificationsPanelMethod;
|
||||
}
|
||||
|
||||
private Method getExpandSettingsPanel() throws NoSuchMethodException {
|
||||
if (expandSettingsPanelMethod == null) {
|
||||
try {
|
||||
// Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/
|
||||
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// old version
|
||||
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel");
|
||||
expandSettingsPanelMethodNewVersion = false;
|
||||
}
|
||||
}
|
||||
return expandSettingsPanelMethod;
|
||||
}
|
||||
|
||||
private Method getCollapsePanelsMethod() throws NoSuchMethodException {
|
||||
if (collapsePanelsMethod == null) {
|
||||
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
|
||||
}
|
||||
return collapsePanelsMethod;
|
||||
}
|
||||
|
||||
public void expandNotificationsPanel() {
|
||||
try {
|
||||
Method method = getExpandNotificationsPanelMethod();
|
||||
if (expandNotificationPanelMethodCustomVersion) {
|
||||
method.invoke(manager, 0);
|
||||
} else {
|
||||
method.invoke(manager);
|
||||
}
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void expandSettingsPanel() {
|
||||
try {
|
||||
Method method = getExpandSettingsPanel();
|
||||
if (expandSettingsPanelMethodNewVersion) {
|
||||
// new version
|
||||
method.invoke(manager, (Object) null);
|
||||
} else {
|
||||
// old version
|
||||
method.invoke(manager);
|
||||
}
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void collapsePanels() {
|
||||
try {
|
||||
Method method = getCollapsePanelsMethod();
|
||||
method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
public final class SurfaceControl {
|
||||
|
||||
private static final Class<?> CLASS;
|
||||
|
||||
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
|
||||
public static final int POWER_MODE_OFF = 0;
|
||||
public static final int POWER_MODE_NORMAL = 2;
|
||||
|
||||
static {
|
||||
try {
|
||||
CLASS = Class.forName("android.view.SurfaceControl");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getBuiltInDisplayMethod;
|
||||
private static Method setDisplayPowerModeMethod;
|
||||
|
||||
private SurfaceControl() {
|
||||
// only static methods
|
||||
}
|
||||
|
||||
public static void openTransaction() {
|
||||
try {
|
||||
CLASS.getMethod("openTransaction").invoke(null);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeTransaction() {
|
||||
try {
|
||||
CLASS.getMethod("closeTransaction").invoke(null);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) {
|
||||
try {
|
||||
CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class)
|
||||
.invoke(null, displayToken, orientation, layerStackRect, displayRect);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setDisplayLayerStack(IBinder displayToken, int layerStack) {
|
||||
try {
|
||||
CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setDisplaySurface(IBinder displayToken, Surface surface) {
|
||||
try {
|
||||
CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static IBinder createDisplay(String name, boolean secure) {
|
||||
try {
|
||||
return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException {
|
||||
if (getBuiltInDisplayMethod == null) {
|
||||
// the method signature has changed in Android Q
|
||||
// <https://github.com/Genymobile/scrcpy/issues/586>
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
|
||||
} else {
|
||||
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
|
||||
}
|
||||
}
|
||||
return getBuiltInDisplayMethod;
|
||||
}
|
||||
|
||||
public static IBinder getBuiltInDisplay() {
|
||||
|
||||
try {
|
||||
Method method = getGetBuiltInDisplayMethod();
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// call getBuiltInDisplay(0)
|
||||
return (IBinder) method.invoke(null, 0);
|
||||
}
|
||||
|
||||
// call getInternalDisplayToken()
|
||||
return (IBinder) method.invoke(null);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
|
||||
if (setDisplayPowerModeMethod == null) {
|
||||
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
|
||||
}
|
||||
return setDisplayPowerModeMethod;
|
||||
}
|
||||
|
||||
public static boolean setDisplayPowerMode(IBinder displayToken, int mode) {
|
||||
try {
|
||||
Method method = getSetDisplayPowerModeMethod();
|
||||
method.invoke(null, displayToken, mode);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void destroyDisplay(IBinder displayToken) {
|
||||
try {
|
||||
CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.os.IInterface;
|
||||
import android.view.IRotationWatcher;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class WindowManager {
|
||||
private final IInterface manager;
|
||||
private Method getRotationMethod;
|
||||
private Method freezeRotationMethod;
|
||||
private Method isRotationFrozenMethod;
|
||||
private Method thawRotationMethod;
|
||||
|
||||
public WindowManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private Method getGetRotationMethod() throws NoSuchMethodException {
|
||||
if (getRotationMethod == null) {
|
||||
Class<?> cls = manager.getClass();
|
||||
try {
|
||||
// method changed since this commit:
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
|
||||
getRotationMethod = cls.getMethod("getDefaultDisplayRotation");
|
||||
} catch (NoSuchMethodException e) {
|
||||
// old version
|
||||
getRotationMethod = cls.getMethod("getRotation");
|
||||
}
|
||||
}
|
||||
return getRotationMethod;
|
||||
}
|
||||
|
||||
private Method getFreezeRotationMethod() throws NoSuchMethodException {
|
||||
if (freezeRotationMethod == null) {
|
||||
freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class);
|
||||
}
|
||||
return freezeRotationMethod;
|
||||
}
|
||||
|
||||
private Method getIsRotationFrozenMethod() throws NoSuchMethodException {
|
||||
if (isRotationFrozenMethod == null) {
|
||||
isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen");
|
||||
}
|
||||
return isRotationFrozenMethod;
|
||||
}
|
||||
|
||||
private Method getThawRotationMethod() throws NoSuchMethodException {
|
||||
if (thawRotationMethod == null) {
|
||||
thawRotationMethod = manager.getClass().getMethod("thawRotation");
|
||||
}
|
||||
return thawRotationMethod;
|
||||
}
|
||||
|
||||
public int getRotation() {
|
||||
try {
|
||||
Method method = getGetRotationMethod();
|
||||
return (int) method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void freezeRotation(int rotation) {
|
||||
try {
|
||||
Method method = getFreezeRotationMethod();
|
||||
method.invoke(manager, rotation);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRotationFrozen() {
|
||||
try {
|
||||
Method method = getIsRotationFrozenMethod();
|
||||
return (boolean) method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void thawRotation() {
|
||||
try {
|
||||
Method method = getThawRotationMethod();
|
||||
method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
|
||||
try {
|
||||
Class<?> cls = manager.getClass();
|
||||
try {
|
||||
// display parameter added since this commit:
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// old version
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CodecOptionsTest {
|
||||
|
||||
@Test
|
||||
public void testIntegerImplicit() {
|
||||
List<CodecOption> codecOptions = CodecOption.parse("some_key=5");
|
||||
|
||||
Assert.assertEquals(1, codecOptions.size());
|
||||
|
||||
CodecOption option = codecOptions.get(0);
|
||||
Assert.assertEquals("some_key", option.getKey());
|
||||
Assert.assertEquals(5, option.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInteger() {
|
||||
List<CodecOption> codecOptions = CodecOption.parse("some_key:int=5");
|
||||
|
||||
Assert.assertEquals(1, codecOptions.size());
|
||||
|
||||
CodecOption option = codecOptions.get(0);
|
||||
Assert.assertEquals("some_key", option.getKey());
|
||||
Assert.assertTrue(option.getValue() instanceof Integer);
|
||||
Assert.assertEquals(5, option.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLong() {
|
||||
List<CodecOption> codecOptions = CodecOption.parse("some_key:long=5");
|
||||
|
||||
Assert.assertEquals(1, codecOptions.size());
|
||||
|
||||
CodecOption option = codecOptions.get(0);
|
||||
Assert.assertEquals("some_key", option.getKey());
|
||||
Assert.assertTrue(option.getValue() instanceof Long);
|
||||
Assert.assertEquals(5L, option.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFloat() {
|
||||
List<CodecOption> codecOptions = CodecOption.parse("some_key:float=4.5");
|
||||
|
||||
Assert.assertEquals(1, codecOptions.size());
|
||||
|
||||
CodecOption option = codecOptions.get(0);
|
||||
Assert.assertEquals("some_key", option.getKey());
|
||||
Assert.assertTrue(option.getValue() instanceof Float);
|
||||
Assert.assertEquals(4.5f, option.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testString() {
|
||||
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=some_value");
|
||||
|
||||
Assert.assertEquals(1, codecOptions.size());
|
||||
|
||||
CodecOption option = codecOptions.get(0);
|
||||
Assert.assertEquals("some_key", option.getKey());
|
||||
Assert.assertTrue(option.getValue() instanceof String);
|
||||
Assert.assertEquals("some_value", option.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringEscaped() {
|
||||
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key");
|
||||
|
||||
Assert.assertEquals(1, codecOptions.size());
|
||||
|
||||
CodecOption option = codecOptions.get(0);
|
||||
Assert.assertEquals("some_key", option.getKey());
|
||||
Assert.assertTrue(option.getValue() instanceof String);
|
||||
Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testList() {
|
||||
List<CodecOption> codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c");
|
||||
|
||||
Assert.assertEquals(5, codecOptions.size());
|
||||
|
||||
CodecOption option;
|
||||
|
||||
option = codecOptions.get(0);
|
||||
Assert.assertEquals("a", option.getKey());
|
||||
Assert.assertTrue(option.getValue() instanceof Integer);
|
||||
Assert.assertEquals(1, option.getValue());
|
||||
|
||||
option = codecOptions.get(1);
|
||||
Assert.assertEquals("b", option.getKey());
|
||||
Assert.assertTrue(option.getValue() instanceof Integer);
|
||||
Assert.assertEquals(2, option.getValue());
|
||||
|
||||
option = codecOptions.get(2);
|
||||
Assert.assertEquals("c", option.getKey());
|
||||
Assert.assertTrue(option.getValue() instanceof Long);
|
||||
Assert.assertEquals(3L, option.getValue());
|
||||
|
||||
option = codecOptions.get(3);
|
||||
Assert.assertEquals("d", option.getKey());
|
||||
Assert.assertTrue(option.getValue() instanceof Float);
|
||||
Assert.assertEquals(4.5f, option.getValue());
|
||||
|
||||
option = codecOptions.get(4);
|
||||
Assert.assertEquals("e", option.getKey());
|
||||
Assert.assertTrue(option.getValue() instanceof String);
|
||||
Assert.assertEquals("a,b=c", option.getValue());
|
||||
}
|
||||
}
|
|
@ -1,403 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
public class ControlMessageReaderTest {
|
||||
|
||||
@Test
|
||||
public void testParseKeycodeEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||
dos.writeByte(KeyEvent.ACTION_UP);
|
||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||
dos.writeInt(5); // repeat
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
// The message type (1 byte) does not count
|
||||
Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1);
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||
Assert.assertEquals(5, event.getRepeat());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseTextEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
||||
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
||||
dos.writeInt(text.length);
|
||||
dos.write(text);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType());
|
||||
Assert.assertEquals("testé", event.getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseLongTextEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
||||
byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH];
|
||||
Arrays.fill(text, (byte) 'a');
|
||||
dos.writeInt(text.length);
|
||||
dos.write(text);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType());
|
||||
Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseTouchEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT);
|
||||
dos.writeByte(MotionEvent.ACTION_DOWN);
|
||||
dos.writeLong(-42); // pointerId
|
||||
dos.writeInt(100);
|
||||
dos.writeInt(200);
|
||||
dos.writeShort(1080);
|
||||
dos.writeShort(1920);
|
||||
dos.writeShort(0xffff); // pressure
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
// The message type (1 byte) does not count
|
||||
Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1);
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType());
|
||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||
Assert.assertEquals(-42, event.getPointerId());
|
||||
Assert.assertEquals(100, event.getPosition().getPoint().getX());
|
||||
Assert.assertEquals(200, event.getPosition().getPoint().getY());
|
||||
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
|
||||
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
|
||||
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseScrollEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT);
|
||||
dos.writeInt(260);
|
||||
dos.writeInt(1026);
|
||||
dos.writeShort(1080);
|
||||
dos.writeShort(1920);
|
||||
dos.writeInt(1);
|
||||
dos.writeInt(-1);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
// The message type (1 byte) does not count
|
||||
Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1);
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType());
|
||||
Assert.assertEquals(260, event.getPosition().getPoint().getX());
|
||||
Assert.assertEquals(1026, event.getPosition().getPoint().getY());
|
||||
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
|
||||
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
|
||||
Assert.assertEquals(1, event.getHScroll());
|
||||
Assert.assertEquals(-1, event.getVScroll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBackOrScreenOnEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON);
|
||||
dos.writeByte(KeyEvent.ACTION_UP);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseExpandNotificationPanelEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseExpandSettingsPanelEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCollapsePanelsEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseGetClipboardEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD);
|
||||
dos.writeByte(ControlMessage.COPY_KEY_COPY);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType());
|
||||
Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseSetClipboardEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
||||
dos.writeLong(0x0102030405060708L); // sequence
|
||||
dos.writeByte(1); // paste
|
||||
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
||||
dos.writeInt(text.length);
|
||||
dos.write(text);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
||||
Assert.assertEquals(0x0102030405060708L, event.getSequence());
|
||||
Assert.assertEquals("testé", event.getText());
|
||||
Assert.assertTrue(event.getPaste());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBigSetClipboardEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
||||
|
||||
byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH];
|
||||
dos.writeLong(0x0807060504030201L); // sequence
|
||||
dos.writeByte(1); // paste
|
||||
Arrays.fill(rawText, (byte) 'a');
|
||||
String text = new String(rawText, 0, rawText.length);
|
||||
|
||||
dos.writeInt(rawText.length);
|
||||
dos.write(rawText);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
||||
Assert.assertEquals(0x0807060504030201L, event.getSequence());
|
||||
Assert.assertEquals(text, event.getText());
|
||||
Assert.assertTrue(event.getPaste());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseSetScreenPowerMode() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE);
|
||||
dos.writeByte(Device.POWER_MODE_NORMAL);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
// The message type (1 byte) does not count
|
||||
Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1);
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType());
|
||||
Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseRotateDevice() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiEvents() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||
dos.writeByte(KeyEvent.ACTION_UP);
|
||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||
dos.writeInt(0); // repeat
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||
dos.writeByte(MotionEvent.ACTION_DOWN);
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
dos.writeInt(1); // repeat
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
|
||||
ControlMessage event = reader.next();
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||
Assert.assertEquals(0, event.getRepeat());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
|
||||
event = reader.next();
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||
Assert.assertEquals(1, event.getRepeat());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartialEvents() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||
dos.writeByte(KeyEvent.ACTION_UP);
|
||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||
dos.writeInt(4); // repeat
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||
dos.writeByte(MotionEvent.ACTION_DOWN);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
|
||||
ControlMessage event = reader.next();
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||
Assert.assertEquals(4, event.getRepeat());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
|
||||
event = reader.next();
|
||||
Assert.assertNull(event); // the event is not complete
|
||||
|
||||
bos.reset();
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
dos.writeInt(5); // repeat
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
packet = bos.toByteArray();
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
|
||||
// the event is now complete
|
||||
event = reader.next();
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||
Assert.assertEquals(5, event.getRepeat());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DeviceMessageWriterTest {
|
||||
|
||||
@Test
|
||||
public void testSerializeClipboard() throws IOException {
|
||||
DeviceMessageWriter writer = new DeviceMessageWriter();
|
||||
|
||||
String text = "aéûoç";
|
||||
byte[] data = text.getBytes(StandardCharsets.UTF_8);
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
|
||||
dos.writeInt(data.length);
|
||||
dos.write(data);
|
||||
|
||||
byte[] expected = bos.toByteArray();
|
||||
|
||||
DeviceMessage msg = DeviceMessage.createClipboard(text);
|
||||
bos = new ByteArrayOutputStream();
|
||||
writer.writeTo(msg, bos);
|
||||
|
||||
byte[] actual = bos.toByteArray();
|
||||
|
||||
Assert.assertArrayEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeAckSetClipboard() throws IOException {
|
||||
DeviceMessageWriter writer = new DeviceMessageWriter();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD);
|
||||
dos.writeLong(0x0102030405060708L);
|
||||
|
||||
byte[] expected = bos.toByteArray();
|
||||
|
||||
DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L);
|
||||
bos = new ByteArrayOutputStream();
|
||||
writer.writeTo(msg, bos);
|
||||
|
||||
byte[] actual = bos.toByteArray();
|
||||
|
||||
Assert.assertArrayEquals(expected, actual);
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class StringUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testUtf8Truncate() {
|
||||
String s = "aÉbÔc";
|
||||
byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
|
||||
Assert.assertEquals(7, utf8.length);
|
||||
|
||||
int count;
|
||||
|
||||
count = StringUtils.getUtf8TruncationIndex(utf8, 1);
|
||||
Assert.assertEquals(1, count);
|
||||
|
||||
count = StringUtils.getUtf8TruncationIndex(utf8, 2);
|
||||
Assert.assertEquals(1, count); // É is 2 bytes-wide
|
||||
|
||||
count = StringUtils.getUtf8TruncationIndex(utf8, 3);
|
||||
Assert.assertEquals(3, count);
|
||||
|
||||
count = StringUtils.getUtf8TruncationIndex(utf8, 4);
|
||||
Assert.assertEquals(4, count);
|
||||
|
||||
count = StringUtils.getUtf8TruncationIndex(utf8, 5);
|
||||
Assert.assertEquals(4, count); // Ô is 2 bytes-wide
|
||||
|
||||
count = StringUtils.getUtf8TruncationIndex(utf8, 6);
|
||||
Assert.assertEquals(6, count);
|
||||
|
||||
count = StringUtils.getUtf8TruncationIndex(utf8, 7);
|
||||
Assert.assertEquals(7, count);
|
||||
|
||||
count = StringUtils.getUtf8TruncationIndex(utf8, 8);
|
||||
Assert.assertEquals(7, count); // no more chars
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue