Merge pull request #545 from barry-ran/dev

Dev
This commit is contained in:
Barry 2022-01-09 17:11:41 +08:00 committed by GitHub
commit fc400fabd4
75 changed files with 1807 additions and 1145 deletions

View file

@ -13,7 +13,7 @@ on:
jobs:
build:
name: Build
runs-on: macos-latest
runs-on: macos-10.15
strategy:
matrix:
qt-ver: [5.15.1]
@ -44,7 +44,7 @@ jobs:
ENV_QT_PATH: ${{ env.qt-install-path }}
run: |
python ci/generate-version.py
ci/mac/build_for_mac.sh release
ci/mac/build_for_mac.sh RelWithDebInfo
# 获取ref最后一个/后的内容
- name: Get the version
shell: bash

View file

@ -6,6 +6,7 @@ on:
- 'QtScrcpy/**'
- '!QtScrcpy/res/**'
- '.github/workflows/ubuntu.yml'
- 'ci/linux/**'
pull_request:
paths:
- 'QtScrcpy/**'
@ -17,7 +18,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-16.04,ubuntu-18.04]
os: [ubuntu-18.04]
qt-ver: [5.15.1]
qt-arch-install: [gcc_64]
gcc-arch: [x64]
@ -47,4 +48,4 @@ jobs:
ENV_QT_PATH: ${{ env.qt-install-path }}
run: |
python ci/generate-version.py
ci/linux/build_for_ubuntu.sh release
ci/linux/build_for_ubuntu.sh RelWithDebInfo

View file

@ -76,7 +76,7 @@ jobs:
ENV_QT_PATH: ${{ env.qt-install-path }}
run: |
call python ci\generate-version.py
call "ci\win\build_for_win.bat" release ${{ matrix.msvc-arch }}
call "ci\win\build_for_win.bat" RelWithDebInfo ${{ matrix.msvc-arch }}
# 获取ref最后一个/后的内容
- name: Get the version
shell: bash

3
.gitignore vendored
View file

@ -12,4 +12,5 @@
/build/
build-*
*.DS_Store
userdata.ini
userdata.ini
Info_Mac.plist

View file

@ -1,21 +1,4 @@
cmake_minimum_required(VERSION 3.19 FATAL_ERROR)
# Read version numbers from file
file (STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/QtScrcpy/version STRING_VERSION)
message(STATUS "QtScrcpy Version ${STRING_VERSION}")
project(QtScrcpy
VERSION ${STRING_VERSION}
LANGUAGES C CXX
)
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
enable_language(OBJCXX)
endif()
# Split version numbers
string(REPLACE "." ";" VERSION_LIST ${STRING_VERSION})
list(GET VERSION_LIST 0 VERSION_MAJOR)
list(GET VERSION_LIST 1 VERSION_MINOR)
list(GET VERSION_LIST 2 VERSION_PATCH)
project(all)
add_subdirectory(QtScrcpy)

View file

@ -1,309 +1,434 @@
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
# For VS2019 and Xcode 12+ support.
cmake_minimum_required(VERSION 3.19 FATAL_ERROR)
#
# Global config
#
# QC is "Qt CMake"
# https://www.kdab.com/wp-content/uploads/stories/QTVTC20-Using-Modern-CMake-Kevin-Funk.pdf
# QC Custom config
set(QC_PROJECT_NAME "QtScrcpy")
# Read version numbers from file
file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/version QC_FILE_VERSION)
set(QC_PROJECT_VERSION ${QC_FILE_VERSION})
# Project declare
project(${QC_PROJECT_NAME} VERSION ${QC_PROJECT_VERSION} LANGUAGES CXX)
message(STATUS "[${PROJECT_NAME}] Project ${PROJECT_NAME} ${PROJECT_VERSION}")
# QC define
# check arch
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(QC_CPU_ARCH x64)
else()
set(QC_CPU_ARCH x86)
endif()
message(STATUS "[${PROJECT_NAME}] CPU_ARCH:${QC_CPU_ARCH}")
# CMake set
#set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Network LinguistTools REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Network LinguistTools REQUIRED)
# default RelWithDebInfo
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()
message(STATUS "[${PROJECT_NAME}] BUILD_TYPE:${CMAKE_BUILD_TYPE}")
if(MSVC)
# Compiler set
message(STATUS "[${PROJECT_NAME}] C++ compiler ID is: ${CMAKE_CXX_COMPILER_ID}")
if (MSVC)
# FFmpeg cannot be compiled natively by MSVC version < 12.0 (2013)
if(MSVC_VERSION LESS 1800)
message(FATAL_ERROR "[QtScrcpy] FATAL ERROR: MSVC version is older than 12.0 (2013).")
message(FATAL_ERROR "[${PROJECT_NAME}] ERROR: MSVC version is older than 12.0 (2013).")
endif()
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /utf-8")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /utf-8")
message(STATUS "[${PROJECT_NAME}] Set Warnings as error")
# warning level 3 and all warnings as errors
add_compile_options(/W3 /WX /wd4566)
# avoid warning C4819
add_compile_options(-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)
# disable SAFESEH - avoid "LNK2026: module unsafe"(Qt5.15&&vs2019)
add_link_options(/SAFESEH:NO)
endif()
if (NOT MSVC)
message(STATUS "[${PROJECT_NAME}] Set warnings as error")
# lots of warnings and all warnings as errors
add_compile_options(-Wall -Wextra -pedantic -Werror)
# ==================== macOS ====================
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
# QS_MAC_RESOURCES: esource file list stored in Contents/MacOS
file(GLOB QS_MAC_RESOURCES "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/lib/*.dylib")
list(APPEND QS_MAC_RESOURCES
"${PROJECT_SOURCE_DIR}/third_party/scrcpy-server"
"${PROJECT_SOURCE_DIR}/adb/mac/adb"
)
# QS_MAC_CONFIG: Config file stored in Contents/MacOS/config
set(QS_MAC_CONFIG "${PROJECT_SOURCE_DIR}/config/config.ini")
# disable some warning
add_compile_options(-Wno-nested-anon-types -Wno-c++17-extensions)
endif()
set(QS_TS_FILES
${CMAKE_CURRENT_SOURCE_DIR}/res/i18n/zh_CN.ts
${CMAKE_CURRENT_SOURCE_DIR}/res/i18n/en_US.ts
)
set_source_files_properties(${QS_TS_FILES} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/res/i18n")
#
# Qt
#
set(QS_SOURCES_MAIN
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Network LinguistTools REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Network LinguistTools 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)
endif()
message(STATUS "[${PROJECT_NAME}] Qt version is: ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}")
#
# Sources
#
# adb
set(QC_ADB_SOURCES
adb/adbprocess.h
adb/adbprocess.cpp
)
source_group(adb FILES ${QC_ADB_SOURCES})
# common
set(QC_COMMON_SOURCES
common/qscrcpyevent.h
)
source_group(common FILES ${QC_COMMON_SOURCES})
# device
set(QC_DEVICE_SOURCES
device/device.h
device/device.cpp
device/android/input.h
device/android/keycodes.h
device/controller/controller.h
device/controller/controller.cpp
device/controller/inputconvert/inputconvertbase.h
device/controller/inputconvert/inputconvertbase.cpp
device/controller/inputconvert/inputconvertnormal.h
device/controller/inputconvert/inputconvertnormal.cpp
device/controller/inputconvert/inputconvertgame.h
device/controller/inputconvert/inputconvertgame.cpp
device/controller/inputconvert/controlmsg.h
device/controller/inputconvert/controlmsg.cpp
device/controller/inputconvert/keymap/keymap.h
device/controller/inputconvert/keymap/keymap.cpp
device/controller/receiver/devicemsg.h
device/controller/receiver/devicemsg.cpp
device/controller/receiver/receiver.h
device/controller/receiver/receiver.cpp
device/decoder/avframeconvert.h
device/decoder/avframeconvert.cpp
device/decoder/decoder.h
device/decoder/decoder.cpp
device/decoder/fpscounter.h
device/decoder/fpscounter.cpp
device/decoder/videobuffer.h
device/decoder/videobuffer.cpp
device/filehandler/filehandler.h
device/filehandler/filehandler.cpp
device/recorder/recorder.h
device/recorder/recorder.cpp
device/render/qyuvopenglwidget.h
device/render/qyuvopenglwidget.cpp
device/server/server.h
device/server/server.cpp
device/server/tcpserver.h
device/server/tcpserver.cpp
device/server/videosocket.h
device/server/videosocket.cpp
device/stream/stream.h
device/stream/stream.cpp
device/ui/toolform.h
device/ui/toolform.cpp
device/ui/toolform.ui
device/ui/videoform.h
device/ui/videoform.cpp
device/ui/videoform.ui
)
source_group(device FILES ${QC_DEVICE_SOURCES})
# devicemanage
set(QC_DEVICEMANAGE_SOURCES
devicemanage/devicemanage.h
devicemanage/devicemanage.cpp
)
source_group(devicemanage FILES ${QC_DEVICEMANAGE_SOURCES})
# fontawesome
set(QC_FONTAWESOME_SOURCES
fontawesome/iconhelper.h
fontawesome/iconhelper.cpp
)
source_group(fontawesome FILES ${QC_FONTAWESOME_SOURCES})
# uibase
set(QC_UIBASE_SOURCES
uibase/keepratiowidget.h
uibase/keepratiowidget.cpp
uibase/magneticwidget.h
uibase/magneticwidget.cpp
)
source_group(uibase FILES ${QC_UIBASE_SOURCES})
# util
set(QC_UTIL_SOURCES
util/compat.h
util/config.h
util/config.cpp
util/bufferutil.h
util/bufferutil.cpp
util/mousetap/mousetap.h
util/mousetap/mousetap.cpp
)
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES}
util/mousetap/winmousetap.h
util/mousetap/winmousetap.cpp
)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES}
util/mousetap/xmousetap.h
util/mousetap/xmousetap.cpp
)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES}
util/mousetap/cocoamousetap.h
util/mousetap/cocoamousetap.mm
)
endif()
source_group(util FILES ${QC_UTIL_SOURCES})
# qrc
set(QC_QRC_SOURCES "res/res.qrc")
# main
set(QC_MAIN_SOURCES
main.cpp
dialog.cpp
dialog.h
dialog.ui
${QS_TS_FILES}
${QC_QRC_SOURCES}
)
set(QS_QRC_MAIN "${CMAKE_CURRENT_SOURCE_DIR}/res/res.qrc")
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) # Qt version 6
qt_create_translation(QS_QM_FILES ${CMAKE_CURRENT_SOURCE_DIR} ${QS_TS_FILES})
if(WIN32)
qt_add_executable(${CMAKE_PROJECT_NAME} WIN32 MANUAL_FINALIZATION
main.cpp
${QS_SOURCES_MAIN}
${QS_QRC_MAIN}
)
elseif(UNIX)
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
qt_add_executable(${CMAKE_PROJECT_NAME} MACOSX_BUNDLE MANUAL_FINALIZATION
main.cpp
${QS_SOURCES_MAIN}
${QS_MAC_RESOURCES}
${QS_MAC_CONFIG}
${QS_QRC_MAIN}
)
else()
qt_add_executable(${CMAKE_PROJECT_NAME} MANUAL_FINALIZATION
main.cpp
${QS_SOURCES_MAIN}
${QS_QRC_MAIN}
)
endif()
endif()
else() # Qt version 5
qt5_create_translation(QS_QM_FILES ${CMAKE_CURRENT_SOURCE_DIR} ${QS_TS_FILES})
if(WIN32)
add_executable(${CMAKE_PROJECT_NAME} WIN32
main.cpp
${QS_SOURCES_MAIN}
${QS_QRC_MAIN}
)
elseif(UNIX)
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
add_executable(${CMAKE_PROJECT_NAME} MACOSX_BUNDLE
main.cpp
${QS_SOURCES_MAIN}
${QS_MAC_RESOURCES}
${QS_MAC_CONFIG}
${QS_QRC_MAIN}
)
else()
add_executable(${CMAKE_PROJECT_NAME}
main.cpp
${QS_SOURCES_MAIN}
${QS_QRC_MAIN}
)
endif()
endif()
endif()
# ******************** Microsoft Windows ********************
if(WIN32)
message(STATUS "[QtScrcpy] Make for Microsoft Windows.")
# 通过rc的方式的话VERSION变量rc中获取不到,定义为宏方便rc中使用
# Define macros for .rc file
# plantform file
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
# Define VERSION macros for .rc file
add_compile_definitions(
VERSION_MAJOR=${VERSION_MAJOR}
VERSION_MINOR=${VERSION_MINOR}
VERSION_PATCH=${VERSION_PATCH}
VERSION_RC_STR=\\\"${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}\\\"
VERSION_MAJOR=${PROJECT_VERSION_MAJOR}
VERSION_MINOR=${PROJECT_VERSION_MINOR}
VERSION_PATCH=${PROJECT_VERSION_PATCH}
VERSION_RC_STR="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}"
)
set(QC_PLANTFORM_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/res/${PROJECT_NAME}.rc"
)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# Step 1. add icns to source file, for MACOSX_PACKAGE_LOCATION copy
set(QC_PLANTFORM_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/res/${PROJECT_NAME}.icns"
)
if(CMAKE_SIZEOF_VOID_P EQUAL 8) # Compiler is 64-bit
message(STATUS "[QtScrcpy] 64-bit compiler detected.")
set(QS_LIB_PATH "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/lib/x64")
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "[QtScrcpy] In debug mode.")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/win/x64/debug"
)
else()
message(STATUS "[QtScrcpy] In release mode.")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/win/x64/release")
endif()
set(QS_DLL_PATH "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/bin/x64")
elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) # Compiler is 32-bit
message(STATUS "[QtScrcpy] 32-bit compiler detected.")
set(QS_LIB_PATH "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/lib/x86")
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "[QtScrcpy] In debug mode.")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/win/x86/debug")
else()
message(STATUS "[QtScrcpy] In release mode.")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/win/x86/release")
endif()
set(QS_DLL_PATH "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/bin/x86")
endif()
# 构建完成后复制DLL依赖库
# Copy DLL dependencies after building
get_target_property(QS_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY)
file(GLOB QS_DLL_FILES "${QS_DLL_PATH}/*.dll")
foreach(QS_DLL_FILE ${QS_DLL_FILES})
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND
${CMAKE_COMMAND} -E copy_if_different
"${QS_DLL_FILE}" "${QS_RUNTIME_OUTPUT_DIRECTORY}"
)
endforeach()
if(MSVC)
message(STATUS "[QtScrcpy] Microsoft Visual C++ is used.")
target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${QS_LIB_PATH})
set(QS_EXTERNAL_LIBS_FFMPEG
avformat
avcodec
avutil
swscale
)
# If MinGW is used, it is not appropriate to link static MSVC libs.
# Instead, we link DLLs directly
elseif(MINGW)
message(STATUS "[QtScrcpy] MinGW GCC is used.")
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
"-static"
${QS_DLL_FILES}
"-Wl,--enable-stdcall-fixup"
)
endif()
set(RC_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/QtScrcpy.rc")
# ******************** Unix-like OSs ********************
elseif(UNIX)
set(QS_LIB_PATH "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/lib")
# ==================== macOS ====================
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
message(STATUS "[QtScrcpy] Make for macOS.")
target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${QS_LIB_PATH})
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/mac/debug")
else()
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/mac/release")
endif()
# Icon file stored in Contents/Resources
set(QS_MAC_ICON_NAME "QtScrcpy.icns")
set(QS_MAC_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/res/${QS_MAC_ICON_NAME}")
set_source_files_properties(${QS_MAC_RESOURCES} PROPERTIES
MACOSX_PACKAGE_LOCATION "MacOS"
)
set_source_files_properties(${QS_MAC_CONFIG} PROPERTIES
MACOSX_PACKAGE_LOCATION "MacOS/config"
)
set(QS_EXTERNAL_LIBS_FFMPEG
avformat.58
avcodec.58
avutil.56
swscale.5
)
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
# The base plist template file
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/res/Info_Mac.plist"
# The elements to be overwritten
MACOSX_BUNDLE_ICON_FILE "${QS_MAC_ICON_NAME}"
MACOSX_BUNDLE_BUNDLE_VERSION "${STRING_VERSION}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${STRING_VERSION}"
MACOSX_BUNDLE_LONG_VERSION_STRING "${STRING_VERSION}"
# Copy file(s) to Contents/Resources
RESOURCE "${QS_MAC_ICON_PATH}"
)
# =============== Non-Mac OSs (Linux, BSD, etc.) ===============
else()
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/linux/debug")
else()
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/linux/release")
endif()
find_package(Threads REQUIRED)
message(STATUS "[QtScrcpy] Make for non-Mac Unix-like OS.")
set(INSTALLED_FFMPEG_FOUND false)
find_package(PkgConfig)
if(PkgConfig_FOUND)
pkg_check_modules(FFmpeg libavformat>=58 libavcodec>=58 libavutil>=56 libswscale>=5)
if(FFmpeg_FOUND)
set(INSTALLED_FFMPEG_FOUND true)
message(STATUS "[QtScrcpy] Development files of FFmpeg were detected in your OS and will be used.")
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE ${FFmpeg_LDFLAGS})
target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE ${FFmpeg_CFLAGS})
set(QS_EXTERNAL_LIBS_FFMPEG ${FFmpeg_LIBRARIES})
endif()
endif()
if(NOT INSTALLED_FFMPEG_FOUND)
message(STATUS "[QtScrcpy] Development files of FFmpeg were not detected in your OS. Files within third_party/ffmpeg/ will be used.")
target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${QS_LIB_PATH})
set(QS_EXTERNAL_LIBS_FFMPEG
avformat
avcodec
avutil
swscale
Threads::Threads
)
endif()
endif()
endif()
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
set(QS_SUBDIRECTORIES_MAIN
adb
common
device
devicemanage
fontawesome
uibase
util
# 使用qt5_add_translation 根据已有ts文件生成qm文件不用qt5_create_translation
# 感兴趣可以了解下qt5_create_translation用法 https://www.cnblogs.com/apocelipes/p/14355460.html
set(QC_TS_FILES
${CMAKE_CURRENT_SOURCE_DIR}/res/i18n/zh_CN.ts
${CMAKE_CURRENT_SOURCE_DIR}/res/i18n/en_US.ts
)
foreach(QS_SUBDIRECTORY_MAIN ${QS_SUBDIRECTORIES_MAIN})
add_subdirectory(${QS_SUBDIRECTORY_MAIN})
endforeach()
set_source_files_properties(${QC_TS_FILES} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/res/i18n")
qt5_add_translation(QC_QM_FILES ${QC_TS_FILES})
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
adb
devicemanage
)
# all sources
set(QC_PROJECT_SOURCES
${QC_ADB_SOURCES}
${QC_COMMON_SOURCES}
${QC_DEVICE_SOURCES}
${QC_DEVICEMANAGE_SOURCES}
${QC_FONTAWESOME_SOURCES}
${QC_UIBASE_SOURCES}
${QC_UTIL_SOURCES}
${QC_MAIN_SOURCES}
${QC_PLANTFORM_SOURCES}
)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(QC_RUNTIME_TYPE MACOSX_BUNDLE)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(QC_RUNTIME_TYPE WIN32)
endif()
add_executable(${PROJECT_NAME} ${QC_RUNTIME_TYPE} ${QC_PROJECT_SOURCES})
#
# Internal include path (todo: remove this, use absolute path include)
#
target_include_directories(${PROJECT_NAME} PRIVATE adb)
target_include_directories(${PROJECT_NAME} PRIVATE common)
target_include_directories(${PROJECT_NAME} PRIVATE device)
target_include_directories(${PROJECT_NAME} PRIVATE device/filehandler)
target_include_directories(${PROJECT_NAME} PRIVATE device/android)
target_include_directories(${PROJECT_NAME} PRIVATE device/decoder)
target_include_directories(${PROJECT_NAME} PRIVATE device/controller)
target_include_directories(${PROJECT_NAME} PRIVATE device/controller/receiver)
target_include_directories(${PROJECT_NAME} PRIVATE device/controller/inputconvert)
target_include_directories(${PROJECT_NAME} PRIVATE device/controller/inputconvert/keymap)
target_include_directories(${PROJECT_NAME} PRIVATE device/server)
target_include_directories(${PROJECT_NAME} PRIVATE device/stream)
target_include_directories(${PROJECT_NAME} PRIVATE device/render)
target_include_directories(${PROJECT_NAME} PRIVATE device/ui)
target_include_directories(${PROJECT_NAME} PRIVATE device/recorder)
target_include_directories(${PROJECT_NAME} PRIVATE devicemanage)
target_include_directories(${PROJECT_NAME} PRIVATE fontawesome)
target_include_directories(${PROJECT_NAME} PRIVATE util)
target_include_directories(${PROJECT_NAME} PRIVATE uibase)
#
# common deps
#
# Qt
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Network
device
stream
ui
util
)
# output dir
# https://cmake.org/cmake/help/latest/prop_gbl/GENERATOR_IS_MULTI_CONFIG.html
get_property(QC_IS_MUTIL_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
message(STATUS "multi config:" QC_IS_MUTIL_CONFIG)
# $<0:> 使用生成器表达式为每个config设置RUNTIME_OUTPUT_DIRECTORY这样multi config就不会自动追加CMAKE_BUILD_TYPE子目录了
# 1. multi config介绍 https://cmake.org/cmake/help/latest/prop_gbl/GENERATOR_IS_MULTI_CONFIG.html
# 2. multi config在不用表达式生成器时自动追加子目录说明 https://cmake.org/cmake/help/latest/prop_tgt/RUNTIME_OUTPUT_DIRECTORY.html
# 3. 使用表达式生成器禁止multi config自动追加子目录解决方案 https://stackoverflow.com/questions/7747857/in-cmake-how-do-i-work-around-the-debug-and-release-directories-visual-studio-2
set_target_properties(${PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../output/${QC_CPU_ARCH}/${CMAKE_BUILD_TYPE}/$<0:>"
)
#
# plantform deps
#
# windows
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
# ffmpeg
# include
target_include_directories(${PROJECT_NAME} PRIVATE ../third_party/ffmpeg/include)
# link
set(FFMPEG_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/${QC_CPU_ARCH}")
target_link_directories(${PROJECT_NAME} PRIVATE ${FFMPEG_LIB_PATH})
target_link_libraries(${PROJECT_NAME} PRIVATE
avformat
avcodec
avutil
swscale
)
# copy
set(FFMPEG_BIN_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/bin/${QC_CPU_ARCH}")
get_target_property(FFMPEG_BIN_OUTPUT_PATH ${PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FFMPEG_BIN_PATH}/avcodec-58.dll" "${FFMPEG_BIN_OUTPUT_PATH}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FFMPEG_BIN_PATH}/avformat-58.dll" "${FFMPEG_BIN_OUTPUT_PATH}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FFMPEG_BIN_PATH}/avutil-56.dll" "${FFMPEG_BIN_OUTPUT_PATH}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FFMPEG_BIN_PATH}/swscale-5.dll" "${FFMPEG_BIN_OUTPUT_PATH}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FFMPEG_BIN_PATH}/swresample-3.dll" "${FFMPEG_BIN_OUTPUT_PATH}"
)
endif()
# MacOS
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# ffmpeg
# include
target_include_directories(${PROJECT_NAME} PRIVATE ../third_party/ffmpeg/include)
# link
set(FFMPEG_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib")
target_link_directories(${PROJECT_NAME} PRIVATE ${FFMPEG_LIB_PATH})
target_link_libraries(${PROJECT_NAME} PRIVATE
avformat.58
avcodec.58
avutil.56
swscale.5
)
if(QT_VERSION_MAJOR EQUAL 6)
qt_finalize_executable(${CMAKE_PROJECT_NAME})
# 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)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
# dylib,scrcpy-server,adb copy to Contents/MacOS
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/libavcodec.58.dylib" "${MACOS_BUNDLE_PATH}/MacOS"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/libavformat.58.dylib" "${MACOS_BUNDLE_PATH}/MacOS"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/libavutil.56.dylib" "${MACOS_BUNDLE_PATH}/MacOS"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/libswscale.5.dylib" "${MACOS_BUNDLE_PATH}/MacOS"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/libswresample.3.dylib" "${MACOS_BUNDLE_PATH}/MacOS"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/scrcpy-server" "${MACOS_BUNDLE_PATH}/MacOS"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/adb/mac/adb" "${MACOS_BUNDLE_PATH}/MacOS"
# config file copy to Contents/MacOS/config
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../config/config.ini" "${MACOS_BUNDLE_PATH}/MacOS/config"
)
# Step 2. ues MACOSX_PACKAGE_LOCATION copy icns to Resources
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/res/${PROJECT_NAME}.icns
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
)
# use MACOSX_BUNDLE_INFO_PLIST custom plist, not use MACOSX_BUNDLE_BUNDLE_NAME etc..
set(INFO_PLIST_TEMPLATE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/Info_Mac.plist.in")
set(INFO_PLIST_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/Info_Mac.plist")
file(READ "${INFO_PLIST_TEMPLATE_FILE}" plist_contents)
string(REPLACE "\${BUNDLE_VERSION}" "${PROJECT_VERSION}" plist_contents ${plist_contents})
file(WRITE ${INFO_PLIST_FILE} ${plist_contents})
set_target_properties(${PROJECT_NAME} PROPERTIES
MACOSX_BUNDLE_INFO_PLIST "${INFO_PLIST_FILE}"
# "" disable code sign
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY ""
)
# mac framework
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework AppKit")
endif()
# Linux
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
# include
target_include_directories(${PROJECT_NAME} PRIVATE ../third_party/ffmpeg/include)
# link
set(FFMPEG_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib")
target_link_directories(${PROJECT_NAME} PRIVATE ${FFMPEG_LIB_PATH})
target_link_libraries(${PROJECT_NAME} PRIVATE
# ffmpeg
avformat
avcodec
avutil
swscale
# qx11
Qt${QT_VERSION_MAJOR}::X11Extras
# xcb https://doc.qt.io/qt-5/linux-requirements.html
xcb
# pthread
Threads::Threads
)
# linux set app icon: https://blog.csdn.net/MrNoboday/article/details/82870853
endif()

View file

@ -97,9 +97,9 @@ win32 {
message("x64")
# 输出目录
CONFIG(debug, debug|release) {
DESTDIR = $$PWD/../output/win/x64/debug
DESTDIR = $$PWD/../output/x64/debug
} else {
DESTDIR = $$PWD/../output/win/x64/release
DESTDIR = $$PWD/../output/x64/release
}
# 依赖模块
@ -114,9 +114,9 @@ win32 {
message("x86")
# 输出目录
CONFIG(debug, debug|release) {
DESTDIR = $$PWD/../output/win/x86/debug
DESTDIR = $$PWD/../output/x86/debug
} else {
DESTDIR = $$PWD/../output/win/x86/release
DESTDIR = $$PWD/../output/x86/release
}
# 依赖模块
@ -147,9 +147,9 @@ win32 {
macos {
# 输出目录
CONFIG(debug, debug|release) {
DESTDIR = $$PWD/../output/mac/debug
DESTDIR = $$PWD/../output/debug
} else {
DESTDIR = $$PWD/../output/mac/release
DESTDIR = $$PWD/../output/release
}
# 依赖模块
@ -196,9 +196,9 @@ macos {
linux {
# 输出目录
CONFIG(debug, debug|release) {
DESTDIR = $$PWD/../output/linux/debug
DESTDIR = $$PWD/../output/debug
} else {
DESTDIR = $$PWD/../output/linux/release
DESTDIR = $$PWD/../output/release
}
# 依赖模块

View file

@ -1,11 +0,0 @@
set(QS_SOURCES_ADB
adbprocess.h
adbprocess.cpp
)
add_library(adb ${QS_SOURCES_ADB})
target_include_directories(adb PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(adb PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
util
)

View file

@ -116,9 +116,17 @@ QStringList AdbProcess::getDevicesSerialFromStdOut()
{
// get devices serial by adb devices
QStringList serials;
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
QStringList devicesInfoList = m_standardOutput.split(QRegExp("\r\n|\n"), Qt::SkipEmptyParts);
#else
QStringList devicesInfoList = m_standardOutput.split(QRegExp("\r\n|\n"), QString::SkipEmptyParts);
#endif
for (QString deviceInfo : devicesInfoList) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
QStringList deviceInfos = deviceInfo.split(QRegExp("\t"), Qt::SkipEmptyParts);
#else
QStringList deviceInfos = deviceInfo.split(QRegExp("\t"), QString::SkipEmptyParts);
#endif
if (2 == deviceInfos.count() && 0 == deviceInfos[1].compare("device")) {
serials << deviceInfos[0];
}
@ -131,7 +139,7 @@ QString AdbProcess::getDeviceIPFromStdOut()
QString ip = "";
#if 0
QString strIPExp = "inet [\\d.]*";
QRegExp ipRegExp(strIPExp,Qt::CaseInsensitive);
QRegExp ipRegExp(strIPExp, Qt::CaseInsensitive);
if (ipRegExp.indexIn(m_standardOutput) != -1) {
ip = ipRegExp.cap(0);
ip = ip.right(ip.size() - 5);

View file

@ -1,6 +0,0 @@
set(QS_SOURCES_COMMON
qscrcpyevent.h
)
add_library(common INTERFACE ${QS_SOURCES_COMMON})
target_include_directories(common INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

View file

@ -1,40 +0,0 @@
set(QS_SUBDIRECTORIES_DEVICE
android
controller
decoder
filehandler
recorder
render
server
stream
ui
)
set(QS_SOURCES_DEVICE
device.h
device.cpp
)
add_library(device ${QS_SOURCES_DEVICE})
target_include_directories(device PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
foreach(QS_SUBDIRECTORY_DEVICE ${QS_SUBDIRECTORIES_DEVICE})
add_subdirectory (${QS_SUBDIRECTORY_DEVICE})
endforeach()
target_link_libraries(device INTERFACE inputconvert)
target_link_libraries(device PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
# device (self)
controller
decoder
filehandler
recorder
server
stream
ui
util
mousetap
)

View file

@ -1,7 +0,0 @@
set(QS_SOURCES_DEVICE_ANDROID
input.h
keycodes.h
)
add_library(android INTERFACE ${QS_SOURCES_DEVICE_ANDROID})
target_include_directories(android INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

View file

@ -1,26 +0,0 @@
SET(QS_SUBDIRECTORIES_DEVICE_CONTROLLER
inputconvert
receiver
)
SET(QS_SOURCES_DEVICE_CONTROLLER
controller.h
controller.cpp
)
add_library(controller ${QS_SOURCES_DEVICE_CONTROLLER})
target_include_directories(controller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
foreach(QS_SUBDIRECTORY_DEVICE_CONTROLLER ${QS_SUBDIRECTORIES_DEVICE_CONTROLLER})
add_subdirectory (${QS_SUBDIRECTORY_DEVICE_CONTROLLER})
endforeach()
target_link_libraries(controller PUBLIC
inputconvert
)
target_link_libraries(controller PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
receiver
server
)

View file

@ -1,39 +0,0 @@
SET(QS_SUBDIRECTORIES_DEVICE_CONTROLLER_INPUTCONVERT
keymap
)
set(QS_SOURCES_DEVICE_CONTROLLER_INPUTCONVERT
controlmsg.h
controlmsg.cpp
inputconvertbase.h
inputconvertbase.cpp
inputconvertgame.h
inputconvertgame.cpp
inputconvertnormal.h
inputconvertnormal.cpp
)
add_library(inputconvert ${QS_SOURCES_DEVICE_CONTROLLER_INPUTCONVERT})
target_include_directories(inputconvert PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
foreach(QS_SUBDIRECTORY_DEVICE_CONTROLLER_INPUTCONVERT ${QS_SUBDIRECTORIES_DEVICE_CONTROLLER_INPUTCONVERT})
add_subdirectory (${QS_SUBDIRECTORY_DEVICE_CONTROLLER_INPUTCONVERT})
endforeach()
target_link_libraries(inputconvert PUBLIC
common
# controller
android
)
target_link_libraries(inputconvert INTERFACE
# controller
# inputconvert (self)
keymap
)
target_link_libraries(inputconvert PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
controller
util
)

View file

@ -47,9 +47,9 @@ void InputConvertNormal::mouseEvent(const QMouseEvent *from, const QSize &frameS
}
controlMsg->setInjectTouchMsgData(
static_cast<quint64>(POINTER_ID_MOUSE), action,
convertMouseButtons(from->buttons()),
QRect(pos.toPoint(), frameSize),
AMOTION_EVENT_ACTION_DOWN == action? 1.0f : 0.0f);
convertMouseButtons(from->buttons()),
QRect(pos.toPoint(), frameSize),
AMOTION_EVENT_ACTION_DOWN == action ? 1.0f : 0.0f);
sendControlMsg(controlMsg);
}
@ -64,7 +64,11 @@ void InputConvertNormal::wheelEvent(const QWheelEvent *from, const QSize &frameS
qint32 vScroll = from->angleDelta().y() == 0 ? 0 : from->angleDelta().y() / abs(from->angleDelta().y()) * 2;
// pos
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
QPointF pos = from->position();
#else
QPointF pos = from->posF();
#endif
// convert pos
pos.setX(pos.x() * frameSize.width() / showSize.width());
pos.setY(pos.y() * frameSize.height() / showSize.height());
@ -132,7 +136,11 @@ AndroidMotioneventButtons InputConvertNormal::convertMouseButtons(Qt::MouseButto
if (buttonState & Qt::RightButton) {
buttons |= AMOTION_EVENT_BUTTON_SECONDARY;
}
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
if (buttonState & Qt::MiddleButton) {
#else
if (buttonState & Qt::MidButton) {
#endif
buttons |= AMOTION_EVENT_BUTTON_TERTIARY;
}
if (buttonState & Qt::XButton1) {

View file

@ -1,8 +0,0 @@
set(QS_SOURCES_DEVICE_CONTROLLER_INPUTCONVERT_KEYMAP
keymap.h
keymap.cpp
)
add_library(keymap ${QS_SOURCES_DEVICE_CONTROLLER_INPUTCONVERT_KEYMAP})
target_include_directories(keymap PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(keymap PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

View file

@ -1,14 +0,0 @@
SET(QS_SOURCES_DEVICE_CONTROLLER_RECEIVER
devicemsg.h
devicemsg.cpp
receiver.h
receiver.cpp
)
add_library(receiver ${QS_SOURCES_DEVICE_CONTROLLER_RECEIVER})
target_include_directories(receiver PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(receiver PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Network
util
)

View file

@ -1,21 +0,0 @@
set(QS_SOURCES_DEVICE_DECODER
avframeconvert.h
avframeconvert.cpp
decoder.h
decoder.cpp
fpscounter.h
fpscounter.cpp
videobuffer.h
videobuffer.cpp
)
add_library(decoder ${QS_SOURCES_DEVICE_DECODER})
target_include_directories(decoder PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
"${PROJECT_SOURCE_DIR}/third_party/ffmpeg/include"
)
target_link_libraries(decoder PUBLIC ${QS_EXTERNAL_LIBS_FFMPEG})
target_link_libraries(decoder PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
util
)

View file

@ -300,14 +300,14 @@ void Device::startServer()
// support wireless connect, example:
//m_server->start("192.168.0.174:5555", 27183, m_maxSize, m_bitRate, "");
// only one devices, serial can be null
// mark: crop input format: "width:height:x:y" or - for no crop, for example: "100:200:0:0"
// mark: crop input format: "width:height:x:y" or "" for no crop, for example: "100:200:0:0"
Server::ServerParams params;
params.serial = m_params.serial;
params.localPort = m_params.localPort;
params.maxSize = m_params.maxSize;
params.bitRate = m_params.bitRate;
params.maxFps = m_params.maxFps;
params.crop = "-";
params.crop = "";
params.control = true;
params.useReverse = m_params.useReverse;
params.lockVideoOrientation = m_params.lockVideoOrientation;

View file

@ -1,9 +0,0 @@
set(QS_SOURCES_DEVICE_FILEHANDLER
filehandler.h
filehandler.cpp
)
add_library(filehandler ${QS_SOURCES_DEVICE_FILEHANDLER})
target_include_directories(filehandler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(filehandler PUBLIC adb)
target_link_libraries(filehandler PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

View file

@ -1,14 +0,0 @@
set(QS_SOURCES_DEVICE_RECORDER
recorder.h
recorder.cpp
)
add_library(recorder ${QS_SOURCES_DEVICE_RECORDER})
target_include_directories(recorder PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
"${PROJECT_SOURCE_DIR}/third_party/ffmpeg/include"
)
target_link_libraries(recorder PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
util
)

View file

@ -1,8 +0,0 @@
set(QS_SOURCES_DEVICE_RENDER
qyuvopenglwidget.h
qyuvopenglwidget.cpp
)
add_library(render ${QS_SOURCES_DEVICE_RENDER})
target_include_directories(render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(render PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

View file

@ -1,20 +0,0 @@
set(QS_SOURCES_DEVICE_SERVER
server.h
server.cpp
tcpserver.h
tcpserver.cpp
videosocket.h
videosocket.cpp
)
add_library(server ${QS_SOURCES_DEVICE_SERVER})
target_include_directories(server PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(server PUBLIC
Qt${QT_VERSION_MAJOR}::Network
adb
)
target_link_libraries(server PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
common
util
)

View file

@ -142,27 +142,31 @@ bool Server::execute()
args << "/"; // unused;
args << "com.genymobile.scrcpy.Server";
args << Config::getInstance().getServerVersion();
args << Config::getInstance().getLogLevel();
args << QString::number(m_params.maxSize);
args << QString::number(m_params.bitRate);
args << QString::number(m_params.maxFps);
args << QString::number(m_params.lockVideoOrientation);
args << (m_tunnelForward ? "true" : "false");
if (m_params.crop.isEmpty()) {
args << "-";
} else {
args << m_params.crop;
if (!Config::getInstance().getLogLevel().isEmpty()) {
args << QString("log_level=%1").arg(Config::getInstance().getLogLevel());
}
args << "true"; // always send frame meta (packet boundaries + timestamp)
args << (m_params.control ? "true" : "false");
args << "0"; // display id
args << "false"; // show touch
args << (m_params.stayAwake ? "true" : "false"); // stay awake
args << QString("max_size=%1").arg(QString::number(m_params.maxSize));
args << QString("bit_rate=%1").arg(QString::number(m_params.bitRate));
args << QString("max_fps=%1").arg(QString::number(m_params.maxFps));
args << QString("lock_video_orientation=%1").arg(QString::number(m_params.lockVideoOrientation));
args << QString("tunnel_forward=%1").arg((m_tunnelForward ? "true" : "false"));
if (!m_params.crop.isEmpty()) {
args << QString("crop=%1").arg(m_params.crop);
}
args << QString("control=%1").arg((m_params.control ? "true" : "false"));
args << "display_id=0"; // display id
args << "show_touches=false"; // show touch
args << QString("stay_awake=%1").arg((m_params.stayAwake ? "true" : "false")); // stay awake
// code option
// https://github.com/Genymobile/scrcpy/commit/080a4ee3654a9b7e96c8ffe37474b5c21c02852a
// <https://d.android.com/reference/android/media/MediaFormat>
args << Config::getInstance().getCodecOptions();
args << Config::getInstance().getCodecName();
if (Config::getInstance().getCodecOptions() != "") {
args << QString("codec_options=%1").arg(Config::getInstance().getCodecOptions());
}
if (Config::getInstance().getCodecName() != "") {
args << QString("encoder_name=%1").arg(Config::getInstance().getCodecName());
}
#ifdef SERVER_DEBUGGER
qInfo("Server debugger waiting for a client on device port " SERVER_DEBUGGER_PORT "...");
@ -176,7 +180,7 @@ bool Server::execute()
#endif
// adb -s P7C0218510000537 shell CLASSPATH=/data/local/tmp/scrcpy-server app_process / com.genymobile.scrcpy.Server 0 8000000 false
// mark: crop input format: "width:height:x:y" or - for no crop, for example: "100:200:0:0"
// mark: crop input format: "width:height:x:y" or "" for no crop, for example: "100:200:0:0"
// 这条adb命令是阻塞运行的m_serverProcess进程不会退出了
m_serverProcess.execute(m_params.serial, args);
return true;

View file

@ -31,7 +31,7 @@ public:
quint16 maxSize = 720; // 视频分辨率
quint32 bitRate = 8000000; // 视频比特率
quint32 maxFps = 60; // 视频最大帧率
QString crop = "-"; // 视频裁剪
QString crop = ""; // 视频裁剪
bool control = true; // 安卓端是否接收键鼠控制
bool useReverse = true; // true:先使用adb reverse失败后自动使用adb forwardfalse:直接使用adb forward
int lockVideoOrientation = -1; // 是否锁定视频方向

View file

@ -1,20 +0,0 @@
set(QS_SOURCES_DEVICE_STREAM
stream.h
stream.cpp
)
add_library(stream ${QS_SOURCES_DEVICE_STREAM})
target_include_directories(stream PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
"${PROJECT_SOURCE_DIR}/third_party/ffmpeg/include"
)
target_link_libraries(stream PUBLIC ${QS_EXTERNAL_LIBS_FFMPEG})
target_link_libraries(stream PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
#controller
decoder
recorder
server
util
)

View file

@ -1,23 +0,0 @@
set(QS_SOURCES_DEVICE_UI
toolform.h
toolform.cpp
toolform.ui
videoform.h
videoform.cpp
videoform.ui
)
add_library(ui ${QS_SOURCES_DEVICE_UI})
target_include_directories(ui PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(ui PRIVATE "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/include")
target_link_libraries(ui PUBLIC
device
uibase
)
target_link_libraries(ui PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
controller
render
fontawesome
util
)

View file

@ -627,6 +627,7 @@ void VideoForm::mouseDoubleClickEvent(QMouseEvent *event)
void VideoForm::wheelEvent(QWheelEvent *event)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
if (m_videoWidget->geometry().contains(event->position().toPoint())) {
if (!m_device) {
return;
@ -634,6 +635,17 @@ void VideoForm::wheelEvent(QWheelEvent *event)
QPointF pos = m_videoWidget->mapFrom(this, event->position().toPoint());
QWheelEvent wheelEvent(
pos, event->globalPosition(), event->pixelDelta(), event->angleDelta(), event->buttons(), event->modifiers(), event->phase(), event->inverted());
#else
if (m_videoWidget->geometry().contains(event->pos())) {
if (!m_device) {
return;
}
QPointF pos = m_videoWidget->mapFrom(this, event->pos());
QWheelEvent wheelEvent(
pos, event->globalPosF(), event->pixelDelta(), event->angleDelta(), event->delta(), event->orientation(),
event->buttons(), event->modifiers(), event->phase(), event->source(), event->inverted());
#endif
emit m_device->wheelEvent(&wheelEvent, m_videoWidget->frameSize(), m_videoWidget->size());
}
}

View file

@ -1,13 +0,0 @@
set(QS_SOURCES_DEVICEMANAGE
devicemanage.h
devicemanage.cpp
)
add_library(devicemanage ${QS_SOURCES_DEVICEMANAGE})
target_include_directories(devicemanage PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(devicemanage PUBLIC device)
target_link_libraries(devicemanage PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
server
ui
)

View file

@ -89,11 +89,11 @@ Dialog::Dialog(QWidget *parent) : QDialog(parent), ui(new Ui::Dialog)
m_hideIcon->setContextMenu(m_menu);
m_hideIcon->show();
connect(m_showWindow, &QAction::triggered, this, &Dialog::slotShow);
connect(m_quit, &QAction::triggered, this, [this](){
connect(m_quit, &QAction::triggered, this, [this]() {
m_hideIcon->hide();
qApp->quit();
});
connect(m_hideIcon, &QSystemTrayIcon::activated,this,&Dialog::slotActivated);
connect(m_hideIcon, &QSystemTrayIcon::activated, this, &Dialog::slotActivated);
}
Dialog::~Dialog()
@ -152,9 +152,9 @@ void Dialog::updateBootConfig(bool toView)
if (toView) {
UserBootConfig config = Config::getInstance().getUserBootConfig();
if(config.bitRate == 0) {
if (config.bitRate == 0) {
ui->bitRateBox->setCurrentText("Mbps");
} else if(config.bitRate % 1000000 == 0) {
} else if (config.bitRate % 1000000 == 0) {
ui->bitRateEdit->setText(QString::number(config.bitRate / 1000000));
ui->bitRateBox->setCurrentText("Mbps");
} else {
@ -203,7 +203,11 @@ void Dialog::execAdbCmd()
}
QString cmd = ui->adbCommandEdt->text().trimmed();
outLog("adb " + cmd, false);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
m_adb.execute(ui->serialBox->currentText().trimmed(), cmd.split(" ", Qt::SkipEmptyParts));
#else
m_adb.execute(ui->serialBox->currentText().trimmed(), cmd.split(" ", QString::SkipEmptyParts));
#endif
}
void Dialog::delayMs(int ms)
@ -249,9 +253,9 @@ void Dialog::closeEvent(QCloseEvent *event)
{
this->hide();
m_hideIcon->showMessage(tr("Notice"),
tr("Hidden here!"),
QSystemTrayIcon::Information,
3000);
tr("Hidden here!"),
QSystemTrayIcon::Information,
3000);
event->ignore();
}
@ -521,14 +525,13 @@ void Dialog::on_usbConnectBtn_clicked()
on_startServerBtn_clicked();
}
int Dialog::findDeviceFromeSerialBox(bool wifi) {
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");
for (int i = 0; i < ui->serialBox->count(); ++i)
{
for (int i = 0; i < ui->serialBox->count(); ++i) {
bool isWifi = regIP.exactMatch(ui->serialBox->itemText(i));
bool found = wifi ? isWifi : !isWifi;
if(found)
{
if (found) {
return i;
}
}
@ -582,8 +585,8 @@ void Dialog::on_connectedPhoneList_itemDoubleClicked(QListWidgetItem *item)
void Dialog::on_updateNameBtn_clicked()
{
if(ui->serialBox->count()!=0) {
if(ui->userNameEdt->text().isEmpty()) {
if (ui->serialBox->count() != 0) {
if (ui->userNameEdt->text().isEmpty()) {
Config::getInstance().setNickName(ui->serialBox->currentText(), "Phone");
} else {
Config::getInstance().setNickName(ui->serialBox->currentText(), ui->userNameEdt->text());
@ -591,30 +594,27 @@ void Dialog::on_updateNameBtn_clicked()
on_updateDevice_clicked();
qDebug()<<"Update OK!";
qDebug() << "Update OK!";
} else {
qWarning()<<"No device is connected!";
qWarning() << "No device is connected!";
}
}
void Dialog::on_useSingleModeCheck_clicked()
{
if(ui->useSingleModeCheck->isChecked())
{
if (ui->useSingleModeCheck->isChecked()) {
ui->configGroupBox->hide();
ui->adbGroupBox->hide();
ui->wirelessGroupBox->hide();
ui->usbGroupBox->hide();
}
else
{
} else {
ui->configGroupBox->show();
ui->adbGroupBox->show();
ui->wirelessGroupBox->show();
ui->usbGroupBox->show();
}
QTimer::singleShot(0, this, [this](){
QTimer::singleShot(0, this, [this]() {
resize(width(), layout()->sizeHint().height());
});
}
@ -627,5 +627,5 @@ void Dialog::on_serialBox_currentIndexChanged(const QString &arg1)
quint32 Dialog::getBitRate()
{
return ui->bitRateEdit->text().trimmed().toUInt() *
(ui->bitRateBox->currentText() == QString("Mbps") ? 1000000 : 1000);
(ui->bitRateBox->currentText() == QString("Mbps") ? 1000000 : 1000);
}

View file

@ -207,9 +207,6 @@
<property name="currentText">
<string notr="true">Mbps</string>
</property>
<property name="placeholderText">
<string notr="true"/>
</property>
<item>
<property name="text">
<string notr="true">Mbps</string>

View file

@ -1,8 +0,0 @@
set(QS_SOURCES_FONTAWESOME
iconhelper.h
iconhelper.cpp
)
add_library(fontawesome ${QS_SOURCES_FONTAWESOME})
target_include_directories(fontawesome PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(fontawesome PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

View file

@ -23,21 +23,21 @@ int main(int argc, char *argv[])
{
// set env
#ifdef Q_OS_WIN32
qputenv("QTSCRCPY_ADB_PATH", "../../../../third_party/adb/win/adb.exe");
qputenv("QTSCRCPY_SERVER_PATH", "../../../../third_party/scrcpy-server");
qputenv("QTSCRCPY_KEYMAP_PATH", "../../../../keymap");
qputenv("QTSCRCPY_CONFIG_PATH", "../../../../config");
qputenv("QTSCRCPY_ADB_PATH", "../../../third_party/adb/win/adb.exe");
qputenv("QTSCRCPY_SERVER_PATH", "../../../third_party/scrcpy-server");
qputenv("QTSCRCPY_KEYMAP_PATH", "../../../keymap");
qputenv("QTSCRCPY_CONFIG_PATH", "../../../config");
#endif
#ifdef Q_OS_OSX
qputenv("QTSCRCPY_KEYMAP_PATH", "../../../../../../keymap");
qputenv("QTSCRCPY_KEYMAP_PATH", "../../../../../keymap");
#endif
#ifdef Q_OS_LINUX
qputenv("QTSCRCPY_ADB_PATH", "../../../third_party/adb/linux/adb");
qputenv("QTSCRCPY_SERVER_PATH", "../../../third_party/scrcpy-server");
qputenv("QTSCRCPY_CONFIG_PATH", "../../../config");
qputenv("QTSCRCPY_KEYMAP_PATH", "../../../keymap");
qputenv("QTSCRCPY_ADB_PATH", "../../third_party/adb/linux/adb");
qputenv("QTSCRCPY_SERVER_PATH", "../../third_party/scrcpy-server");
qputenv("QTSCRCPY_CONFIG_PATH", "../../config");
qputenv("QTSCRCPY_KEYMAP_PATH", "../../keymap");
#endif
g_msgType = covertLogLevel(Config::getInstance().getLogLevel());

View file

@ -5,11 +5,11 @@
<key>CFBundleDevelopmentRegion</key>
<string>zh-Hans</string>
<key>CFBundleExecutable</key>
<string>@EXECUTABLE@</string>
<string>QtScrcpy</string>
<key>CFBundleGetInfoString</key>
<string>Created by rankun</string>
<key>CFBundleIconFile</key>
<string>@ICON@</string>
<string>QtScrcpy</string>
<key>CFBundleIdentifier</key>
<string>rankun.QtScrcpy</string>
<key>CFBundleInfoDictionaryVersion</key>
@ -19,13 +19,13 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>${BUNDLE_VERSION}</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<string>${BUNDLE_VERSION}</string>
<key>LSMinimumSystemVersion</key>
<string>10.10</string>
<key>NSAppleEventsUsageDescription</key>

View file

@ -1,10 +0,0 @@
set(QS_SOURCES_UIBASE
keepratiowidget.h
keepratiowidget.cpp
magneticwidget.h
magneticwidget.cpp
)
add_library(uibase ${QS_SOURCES_UIBASE})
target_include_directories(uibase PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(uibase PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

View file

@ -1,21 +0,0 @@
set(QS_SUBDIRECTORIES_UTIL
mousetap
)
set(QS_SOURCES_UTIL
bufferutil.h
bufferutil.cpp
compat.h
config.h
config.cpp
)
add_library(util ${QS_SOURCES_UTIL})
target_include_directories(util PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
foreach(QS_SUBDIRECTORY_UTIL ${QS_SUBDIRECTORIES_UTIL})
add_subdirectory (${QS_SUBDIRECTORY_UTIL})
endforeach()
target_link_libraries(util PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

View file

@ -15,7 +15,7 @@
#define COMMON_PUSHFILE_DEF "/sdcard/"
#define COMMON_SERVER_VERSION_KEY "ServerVersion"
#define COMMON_SERVER_VERSION_DEF "1.17"
#define COMMON_SERVER_VERSION_DEF "1.21"
#define COMMON_SERVER_PATH_KEY "ServerPath"
#define COMMON_SERVER_PATH_DEF "/data/local/tmp/scrcpy-server.jar"

View file

@ -1,50 +0,0 @@
set(QS_SOURCES_UTIL_MOUSETAP
mousetap.h
mousetap.cpp
)
# Microsoft Windows
if(WIN32)
list(APPEND QS_SOURCES_UTIL_MOUSETAP
winmousetap.h
winmousetap.cpp
)
set(QS_EXTERNAL_LIBS_UTIL_MOUSETAP User32)
elseif(UNIX)
# macOS
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
find_library(APPKIT AppKit REQUIRED)
set(QS_EXTERNAL_LIBS_UTIL_MOUSETAP ${APPKIT})
list(APPEND QS_SOURCES_UTIL_MOUSETAP
cocoamousetap.h
cocoamousetap.mm
)
target_compile_options(mousetap "-mmacosx-version-min=10.6")
# Linux, BSD, etc.
else()
find_package(QT NAMES Qt6 Qt5 COMPONENTS X11Extras REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS X11Extras REQUIRED)
set(QS_EXTERNAL_LIBS_UTIL_MOUSETAP
Qt${QT_VERSION_MAJOR}::X11Extras
xcb
)
list(APPEND QS_SOURCES_UTIL_MOUSETAP
xmousetap.h
xmousetap.cpp
)
endif()
endif()
add_library(mousetap ${QS_SOURCES_UTIL_MOUSETAP})
target_link_libraries(mousetap PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
${QS_EXTERNAL_LIBS_UTIL_MOUSETAP})

View file

@ -1 +1 @@
1.6.0
0.0.0

View file

@ -8,7 +8,7 @@ echo ---------------------------------------------------------------
# 从环境变量获取必要参数
# 例如 /home/barry/Qt5.9.6/5.9.6
echo ENV_QT_PATH $ENV_QT_PATH
qt_gcc_path=$ENV_QT_PATH/gcc_64
qt_cmake_path=$ENV_QT_PATH/gcc_64/lib/cmake/Qt5
# 获取绝对路径,保证其他目录执行此脚本依然正确
{
@ -21,17 +21,17 @@ old_cd=$(pwd)
cd $(dirname "$0")
# 启动参数声明
build_mode=debug
build_mode=RelWithDebInfo
echo
echo
echo ---------------------------------------------------------------
echo check build param[debug/release]
echo check build param[Debug/Release/MinSizeRel/RelWithDebInfo]
echo ---------------------------------------------------------------
# 编译参数检查
build_mode=$(echo $1 | tr '[:upper:]' '[:lower:]')
if [[ $build_mode != "release" && $build_mode != "debug" ]]; then
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
@ -40,44 +40,37 @@ fi
echo current build mode: $build_mode
# 环境变量设置
export PATH=$qt_gcc_path/bin:$PATH
#export PATH=$qt_gcc_path/bin:$PATH
echo
echo
echo ---------------------------------------------------------------
echo begin qmake build
echo begin cmake build
echo ---------------------------------------------------------------
# 删除输出目录
output_path=$script_path../../output/linux/$build_mode
output_path=$script_path../../output
if [ -d "$output_path" ]; then
rm -rf $output_path
fi
# 删除临时目录
temp_path=$script_path/../temp
if [ -d "$temp_path" ]; then
rm -rf $temp_path
build_path=$script_path/../build_temp
if [ -d "$build_path" ]; then
rm -rf $build_path
fi
mkdir $temp_path
cd $temp_path
mkdir $build_path
cd $build_path
qmake_params="-spec linux-g++"
if [ $build_mode == "debug" ]; then
qmake_params="$qmake_params CONFIG+=debug CONFIG+=x86_64 CONFIG+=qml_debug"
else
qmake_params="$qmake_params CONFIG+=x86_64 CONFIG+=qtquickcompiler"
fi
# qmake ../../all.pro -spec linux-g++ CONFIG+=debug CONFIG+=x86_64 CONFIG+=qml_debug
qmake ../../all.pro $qmake_params
cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode"
cmake $cmake_params ../..
if [ $? -ne 0 ] ;then
echo "qmake failed"
echo "cmake failed"
exit 1
fi
make -j8
cmake --build . --config $build_mode -j8
if [ $? -ne 0 ] ;then
echo "make failed"
echo "cmake build failed"
exit 1
fi

View file

@ -8,7 +8,7 @@ echo ---------------------------------------------------------------
# 从环境变量获取必要参数
# 例如 /Users/barry/Qt5.12.5/5.12.5
echo ENV_QT_PATH $ENV_QT_PATH
qt_clang_path=$ENV_QT_PATH/clang_64
qt_cmake_path=$ENV_QT_PATH/clang_64/lib/cmake/Qt5
# 获取绝对路径,保证其他目录执行此脚本依然正确
{
@ -21,17 +21,17 @@ old_cd=$(pwd)
cd $(dirname "$0")
# 启动参数声明
build_mode=debug
build_mode=RelWithDebInfo
echo
echo
echo ---------------------------------------------------------------
echo check build param[debug/release]
echo check build param[Debug/Release/MinSizeRel/RelWithDebInfo]
echo ---------------------------------------------------------------
# 编译参数检查
build_mode=$(echo $1 | tr '[:upper:]' '[:lower:]')
if [[ $build_mode != "release" && $build_mode != "debug" ]]; then
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
@ -39,45 +39,35 @@ fi
# 提示
echo current build mode: $build_mode
# 环境变量设置
export PATH=$qt_clang_path/bin:$PATH
echo
echo
echo ---------------------------------------------------------------
echo begin qmake build
echo begin cmake build
echo ---------------------------------------------------------------
# 删除输出目录
output_path=$script_path../../output/mac/$build_mode
output_path=$script_path../../output
if [ -d "$output_path" ]; then
rm -rf $output_path
fi
# 删除临时目录
temp_path=$script_path/../temp
if [ -d "$temp_path" ]; then
rm -rf $temp_path
# 删除编译目录
build_path=$script_path/../build_temp
if [ -d "$build_path" ]; then
rm -rf $build_path
fi
mkdir $temp_path
cd $temp_path
mkdir $build_path
cd $build_path
qmake_params="-spec macx-clang"
if [ $build_mode == "debug" ]; then
qmake_params="$qmake_params CONFIG+=debug CONFIG+=x86_64 CONFIG+=qml_debug"
else
qmake_params="$qmake_params CONFIG+=x86_64 CONFIG+=qtquickcompiler"
fi
# qmake ../../all.pro -spec macx-clang CONFIG+=debug CONFIG+=x86_64 CONFIG+=qml_debug
qmake ../../all.pro $qmake_params
cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode -G Xcode"
cmake $cmake_params ../..
if [ $? -ne 0 ] ;then
echo "qmake failed"
echo "cmake failed"
exit 1
fi
make -j8
cmake --build . --config $build_mode -j8
if [ $? -ne 0 ] ;then
echo "make failed"
echo "cmake build failed"
exit 1
fi

View file

@ -30,7 +30,7 @@ keymap_path=$script_path/../../keymap
# config_path=$script_path/../../config
publish_path=$script_path/$publish_dir
release_path=$script_path/../../output/mac/release
release_path=$script_path/../../output/x64/RelWithDebInfo
export PATH=$qt_clang_path/bin:$PATH

View file

@ -7,12 +7,11 @@ echo check ENV
echo ---------------------------------------------------------------
:: 从环境变量获取必要参数
:: example: D:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\VC\Auxiliary\Build\vcvarsall.bat
set vcvarsall="%ENV_VCVARSALL%"
:: example: D:\Qt\Qt5.12.5\5.12.5
set qt_msvc_path="%ENV_QT_PATH%"
:: example: D:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvarsall.bat
:: set vcvarsall="%ENV_VCVARSALL%"
echo ENV_VCVARSALL %ENV_VCVARSALL%
:: example: D:\Qt\Qt5.12.5\5.12.5
:: echo ENV_VCVARSALL %ENV_VCVARSALL%
echo ENV_QT_PATH %ENV_QT_PATH%
:: 获取脚本绝对路径
@ -23,96 +22,82 @@ cd /d %~dp0
:: 启动参数声明
set cpu_mode=x86
set build_mode=debug
set build_mode=RelWithDebInfo
set errno=1
echo=
echo=
echo ---------------------------------------------------------------
echo check build param[debug/release x86/x64]
echo check build param[Debug/Release/MinSizeRel/RelWithDebInfo]
echo ---------------------------------------------------------------
:: 编译参数检查 /i忽略大小写
if /i "%1"=="debug" (
set build_mode=debug
:: 编译参数检查
if "%1"=="Debug" (
goto build_mode_ok
)
if /i "%1"=="release" (
set build_mode=release
if "%1"=="Release" (
goto build_mode_ok
)
if "%1"=="MinSizeRel" (
goto build_mode_ok
)
if "%1"=="RelWithDebInfo" (
goto build_mode_ok
)
echo error: unkonow build mode -- %1
goto return
:build_mode_ok
set build_mode=%1
set cmake_vs_build_mode=Win32
set qt_cmake_path=%ENV_QT_PATH%\msvc2019\lib\cmake\Qt5
if /i "%2"=="x86" (
set cpu_mode=x86
set cmake_vs_build_mode=Win32
set qt_cmake_path=%ENV_QT_PATH%\msvc2019\lib\cmake\Qt5
)
if /i "%2"=="x64" (
set cpu_mode=x64
set cmake_vs_build_mode=x64
set qt_cmake_path=%ENV_QT_PATH%\msvc2019_64\lib\cmake\Qt5
)
:: 提示
echo current build mode: %build_mode% %cpu_mode%
:: 环境变量设置
if /i %cpu_mode% == x86 (
set qt_msvc_path=%qt_msvc_path%\msvc2017\bin
) else (
set qt_msvc_path=%qt_msvc_path%\msvc2017_64\bin
)
set PATH=%qt_msvc_path%;%PATH%
:: 注册vc环境
if /i %cpu_mode% == x86 (
call %vcvarsall% %cpu_mode%
) else (
call %vcvarsall% %cpu_mode%
)
if not %errorlevel%==0 (
echo "vcvarsall not find"
goto return
)
echo qt cmake path: %qt_cmake_path%
echo=
echo=
echo ---------------------------------------------------------------
echo begin qmake build
echo begin cmake build
echo ---------------------------------------------------------------
:: 删除输出目录
set output_path=%script_path%..\..\output\win\%cpu_mode%\%build_mode%
set output_path=%script_path%..\..\output
if exist %output_path% (
rmdir /q /s %output_path%
)
:: 删除临时目录
set temp_path=%script_path%..\temp
set temp_path=%script_path%..\build_temp
if exist %temp_path% (
rmdir /q /s %temp_path%
)
md %temp_path%
cd %temp_path%
set qmake_params=-spec win32-msvc
if /i %build_mode% == debug (
set qmake_params=%qmake_params% "CONFIG+=debug" "CONFIG+=qml_debug"
) else (
set qmake_params=%qmake_params% "CONFIG+=qtquickcompiler"
)
set cmake_params=-DCMAKE_PREFIX_PATH=%qt_cmake_path% -DCMAKE_BUILD_TYPE=%build_mode% -G "Visual Studio 16 2019" -A %cmake_vs_build_mode%
echo cmake params: %cmake_params%
:: qmake ../../all.pro -spec win32-msvc "CONFIG+=debug" "CONFIG+=qml_debug"
qmake ../../all.pro %qmake_params%
cmake %cmake_params% ../..
if not %errorlevel%==0 (
echo "qmake failed"
echo "cmake failed"
goto return
)
:: nmake
:: jom是qt的多进程nmake工具
..\win\jom -j8
cmake --build . --config %build_mode% -j8
if not %errorlevel%==0 (
echo "nmake failed"
echo "cmake build failed"
goto return
)

View file

@ -7,7 +7,7 @@ echo check ENV
echo ---------------------------------------------------------------
:: 从环境变量获取必要参数
:: example: D:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\VC\Auxiliary\Build\vcvarsall.bat
:: example: D:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvarsall.bat
set vcvarsall="%ENV_VCVARSALL%"
:: 例如 d:\a\QtScrcpy\Qt\5.12.7
set qt_msvc_path="%ENV_QT_PATH%"
@ -47,12 +47,12 @@ set config_path=%script_path%..\..\config
if /i %cpu_mode% == x86 (
set publish_path=%script_path%%publish_dir%\
set release_path=%script_path%..\..\output\win\x86\release
set qt_msvc_path=%qt_msvc_path%\msvc2017\bin
set release_path=%script_path%..\..\output\x86\RelWithDebInfo
set qt_msvc_path=%qt_msvc_path%\msvc2019\bin
) else (
set publish_path=%script_path%%publish_dir%\
set release_path=%script_path%..\..\output\win\x64\release
set qt_msvc_path=%qt_msvc_path%\msvc2017_64\bin
set release_path=%script_path%..\..\output\x64\RelWithDebInfo
set qt_msvc_path=%qt_msvc_path%\msvc2019_64\bin
)
set PATH=%qt_msvc_path%;%PATH%
@ -100,10 +100,17 @@ if /i %cpu_mode% == x86 (
)
:: copy vcruntime dll
:: 只有在64位下需要这个
if /i %cpu_mode% == x64 (
cp "C:\Windows\System32\vcruntime140_1.dll" %publish_path%\vcruntime140_1.dll
cp "C:\Windows\System32\msvcp140_1.dll" %publish_path%\msvcp140_1.dll
cp "C:\Windows\System32\msvcp140.dll" %publish_path%\msvcp140.dll
cp "C:\Windows\System32\vcruntime140.dll" %publish_path%\vcruntime140.dll
:: 只有x64需要
cp "C:\Windows\System32\vcruntime140_1.dll" %publish_path%\vcruntime140_1.dll
) else (
cp "C:\Windows\SysWOW64\msvcp140_1.dll" %publish_path%\msvcp140_1.dll
cp "C:\Windows\SysWOW64\msvcp140.dll" %publish_path%\msvcp140.dll
cp "C:\Windows\SysWOW64\vcruntime140.dll" %publish_path%\vcruntime140.dll
)
::cp "C:\Program Files (x86)\Microsoft Visual Studio\Installer\VCRUNTIME140.dll" %publish_path%\VCRUNTIME140.dll

View file

@ -10,18 +10,18 @@ RenderExpiredFrames=0
# 视频解码方式:-1 自动0 软解1 dx硬解2 opengl硬解
UseDesktopOpenGL=-1
# scrcpy-server的版本号不要修改
ServerVersion=1.17
ServerVersion=1.21
# scrcpy-server推送到安卓设备的路径
ServerPath=/data/local/tmp/scrcpy-server.jar
# 自定义adb路径例如D:/android/tools/adb.exe
AdbPath=
# 编码选项 "-"表示默认
# 编码选项 ""表示默认
# 例如 CodecOptions="profile=1,level=2"
# 更多编码选项参考 https://d.android.com/reference/android/media/MediaFormat
CodecOptions="-"
# 指定编码器名称必须是H.264编码器
CodecOptions=""
# 指定编码器名称(必须是H.264编码器)""表示默认
# 例如 CodecName="OMX.qcom.video.encoder.avc"
CodecName="-"
CodecName=""
# Set the log level (debug, info, warn, error)
LogLevel=info

8
server/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
/.idea/
.DS_Store
/build
/captures
.externalNativeBuild

View file

@ -1,13 +1,13 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
compileSdkVersion 31
defaultConfig {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 29
versionCode 16
versionName "1.14"
targetSdkVersion 31
versionCode 12100
versionName "1.21"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@ -20,7 +20,7 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.1'
}
apply from: "$project.rootDir/config/android-checkstyle.gradle"

View file

@ -0,0 +1,88 @@
#!/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"

25
server/meson.build Normal file
View file

@ -0,0 +1,25 @@
# 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

View file

@ -1,8 +1,11 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
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;
@ -15,22 +18,123 @@ 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(boolean disableShowTouches, int restoreStayOn) throws IOException {
boolean needProcess = disableShowTouches || restoreStayOn != -1;
if (needProcess) {
startProcess(disableShowTouches, restoreStayOn);
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(boolean disableShowTouches, int restoreStayOn) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)};
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);
@ -57,21 +161,37 @@ public final class CleanUp {
Ln.i("Cleaning up");
boolean disableShowTouches = Boolean.parseBoolean(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
Config config = Config.fromBase64(args[0]);
if (disableShowTouches || restoreStayOn != -1) {
if (config.disableShowTouches || config.restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
if (disableShowTouches) {
Ln.i("Disabling \"show touches\"");
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
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 (restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
}
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);
}
}
}
}

View file

@ -21,7 +21,7 @@ public class CodecOption {
}
public static List<CodecOption> parse(String codecOptions) {
if ("-".equals(codecOptions)) {
if (codecOptions.isEmpty()) {
return null;
}

View file

@ -0,0 +1,33 @@
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;
}
}

View file

@ -11,13 +11,18 @@ public final class ControlMessage {
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_COLLAPSE_NOTIFICATION_PANEL = 6;
public static final int TYPE_GET_CLIPBOARD = 7;
public static final int TYPE_SET_CLIPBOARD = 8;
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
public static final int TYPE_ROTATE_DEVICE = 10;
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 int FLAGS_PASTE = 1;
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;
@ -30,16 +35,20 @@ public final class ControlMessage {
private Position position;
private int hScroll;
private int vScroll;
private int flags;
private int copyKey;
private boolean paste;
private int repeat;
private long sequence;
private ControlMessage() {
}
public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) {
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;
}
@ -71,13 +80,26 @@ public final class ControlMessage {
return msg;
}
public static ControlMessage createSetClipboard(String text, boolean paste) {
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;
if (paste) {
msg.flags = FLAGS_PASTE;
}
msg.paste = paste;
return msg;
}
@ -141,7 +163,19 @@ public final class ControlMessage {
return vScroll;
}
public int getFlags() {
return flags;
public int getCopyKey() {
return copyKey;
}
public boolean getPaste() {
return paste;
}
public int getRepeat() {
return repeat;
}
public long getSequence() {
return sequence;
}
}

View file

@ -8,20 +8,21 @@ import java.nio.charset.StandardCharsets;
public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
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 SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
static final int GET_CLIPBOARD_LENGTH = 1;
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length)
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 static final int RAW_BUFFER_SIZE = 4096;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH];
public ControlMessageReader() {
// invariant: the buffer is always in "get" mode
@ -67,16 +68,21 @@ public class ControlMessageReader {
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_BACK_OR_SCREEN_ON:
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_GET_CLIPBOARD:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type);
break;
@ -99,20 +105,23 @@ public class ControlMessageReader {
}
int action = toUnsigned(buffer.get());
int keycode = buffer.getInt();
int repeat = buffer.getInt();
int metaState = buffer.getInt();
return ControlMessage.createInjectKeycode(action, keycode, metaState);
return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
}
private String parseString() {
if (buffer.remaining() < 2) {
if (buffer.remaining() < 4) {
return null;
}
int len = toUnsigned(buffer.getShort());
int len = buffer.getInt();
if (buffer.remaining() < len) {
return null;
}
buffer.get(textBuffer, 0, len);
return new String(textBuffer, 0, len, StandardCharsets.UTF_8);
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() {
@ -148,16 +157,33 @@ public class ControlMessageReader {
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;
}
boolean parse = buffer.get() != 0;
long sequence = buffer.getLong();
boolean paste = buffer.get() != 0;
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createSetClipboard(text, parse);
return ControlMessage.createSetClipboard(sequence, text, paste);
}
private ControlMessage parseSetScreenPowerMode() {

View file

@ -8,14 +8,20 @@ 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 DEVICE_ID_VIRTUAL = -1;
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);
@ -24,9 +30,12 @@ public class Controller {
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
public Controller(Device device, DesktopConnection connection) {
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);
}
@ -38,7 +47,7 @@ public class Controller {
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
coords.orientation = 0;
coords.size = 1;
coords.size = 0;
pointerProperties[i] = props;
pointerCoords[i] = coords;
@ -47,8 +56,8 @@ public class Controller {
public void control() throws IOException {
// on start, power on the device
if (!device.isScreenOn()) {
device.injectKeycode(KeyEvent.KEYCODE_POWER);
if (!Device.isScreenOn()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
// dirty hack
// After POWER is injected, the device is powered on asynchronously.
@ -74,7 +83,7 @@ public class Controller {
switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE:
if (device.supportsInputEvents()) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
}
break;
case ControlMessage.TYPE_INJECT_TEXT:
@ -94,44 +103,47 @@ public class Controller {
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
if (device.supportsInputEvents()) {
pressBackOrTurnScreenOn();
pressBackOrTurnScreenOn(msg.getAction());
}
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel();
Device.expandNotificationPanel();
break;
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
device.collapsePanels();
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
Device.expandSettingsPanel();
break;
case ControlMessage.TYPE_COLLAPSE_PANELS:
Device.collapsePanels();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
String clipboardText = device.getClipboardText();
if (clipboardText != null) {
sender.pushClipboardText(clipboardText);
}
getClipboard(msg.getCopyKey());
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
setClipboard(msg.getText(), paste);
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);
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();
Device.rotateDevice();
break;
default:
// do nothing
}
}
private boolean injectKeycode(int action, int keycode, int metaState) {
return device.injectKeyEvent(action, keycode, 0, metaState);
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) {
@ -142,7 +154,7 @@ public class Controller {
return false;
}
for (KeyEvent event : events) {
if (!device.injectEvent(event)) {
if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
@ -166,7 +178,7 @@ public class Controller {
Point point = device.getPhysicalPoint(position);
if (point == null) {
// ignore event
Ln.w("Ignore touch event, it was generated for a different device size");
return false;
}
@ -195,10 +207,18 @@ public class Controller {
}
}
// 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, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
return device.injectEvent(event);
.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) {
@ -219,17 +239,62 @@ public class Controller {
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
return device.injectEvent(event);
.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);
}
private boolean pressBackOrTurnScreenOn() {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
return device.injectKeycode(keycode);
/**
* 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 setClipboard(String text, boolean paste) {
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");
@ -237,7 +302,12 @@ public class Controller {
// On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
device.injectKeycode(KeyEvent.KEYCODE_PASTE);
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC);
}
if (sequence != ControlMessage.SEQUENCE_INVALID) {
// Acknowledgement requested
sender.pushAckClipboard(sequence);
}
return ok;

View file

@ -1,6 +1,6 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
@ -24,6 +24,16 @@ 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);
}
@ -32,8 +42,6 @@ public final class Device {
void onClipboardTextChanged(String text);
}
private final ServiceManager serviceManager = new ServiceManager();
private ScreenInfo screenInfo;
private RotationListener rotationListener;
private ClipboardListener clipboardListener;
@ -53,18 +61,18 @@ public final class Device {
public Device(Options options) {
displayId = options.getDisplayId();
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId);
DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
int[] displayIds = serviceManager.getDisplayManager().getDisplayIds();
int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds();
throw new InvalidDisplayIdException(displayId, displayIds);
}
int displayInfoFlags = displayInfo.getFlags();
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockVideoOrientation());
layerStack = displayInfo.getLayerStack();
serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
synchronized (Device.this) {
@ -78,25 +86,30 @@ public final class Device {
}
}, displayId);
if (options.getControl()) {
// If control is enabled, synchronize Android clipboard to the computer automatically
serviceManager.getClipboardManager().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);
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) {
@ -147,12 +160,16 @@ public final class Device {
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 boolean injectEvent(InputEvent inputEvent, int mode) {
if (!supportsInputEvents()) {
public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) {
if (!supportsInputEvents(displayId)) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
}
@ -160,26 +177,35 @@ public final class Device {
return false;
}
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode);
}
public boolean injectEvent(InputEvent event) {
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
public boolean injectEvent(InputEvent event, int injectMode) {
return injectEvent(event, displayId, injectMode);
}
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
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);
return injectEvent(event, displayId, injectMode);
}
public boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) {
return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode);
}
public boolean isScreenOn() {
return serviceManager.getPowerManager().isScreenOn();
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) {
@ -190,16 +216,24 @@ public final class Device {
this.clipboardListener = clipboardListener;
}
public void expandNotificationPanel() {
serviceManager.getStatusBarManager().expandNotificationsPanel();
public static void expandNotificationPanel() {
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel();
}
public void collapsePanels() {
serviceManager.getStatusBarManager().collapsePanels();
public static void expandSettingsPanel() {
SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel();
}
public String getClipboardText() {
CharSequence s = serviceManager.getClipboardManager().getText();
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;
}
@ -207,16 +241,30 @@ public final class Device {
}
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 = serviceManager.getClipboardManager().setText(text);
boolean ok = clipboardManager.setText(text);
isSettingClipboard.set(false);
return ok;
}
/**
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
* @param mode one of the {@code POWER_MODE_*} constants
*/
public boolean setScreenPowerMode(int mode) {
public static boolean setScreenPowerMode(int mode) {
IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) {
Ln.e("Could not get built-in display");
@ -225,11 +273,18 @@ public final class Device {
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 void rotateDevice() {
WindowManager wm = serviceManager.getWindowManager();
public static void rotateDevice() {
WindowManager wm = SERVICE_MANAGER.getWindowManager();
boolean accelerometerRotation = !wm.isRotationFrozen();
@ -246,7 +301,7 @@ public final class Device {
}
}
public ContentProvider createSettingsProvider() {
return serviceManager.getActivityManager().createSettingsProvider();
public static Settings getSettings() {
return SETTINGS;
}
}

View file

@ -3,9 +3,13 @@ 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() {
}
@ -17,6 +21,13 @@ public final class DeviceMessage {
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;
}
@ -24,4 +35,8 @@ public final class DeviceMessage {
public String getText() {
return text;
}
public long getSequence() {
return sequence;
}
}

View file

@ -8,6 +8,8 @@ public final class DeviceMessageSender {
private String clipboardText;
private long ack;
public DeviceMessageSender(DesktopConnection connection) {
this.connection = connection;
}
@ -17,18 +19,34 @@ public final class DeviceMessageSender {
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 (clipboardText == null) {
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);
}
DeviceMessage event = DeviceMessage.createClipboard(text);
connection.sendDeviceMessage(event);
}
}
}

View file

@ -7,24 +7,28 @@ import java.nio.charset.StandardCharsets;
public class DeviceMessageWriter {
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3;
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[MAX_EVENT_SIZE];
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) DeviceMessage.TYPE_CLIPBOARD);
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.putShort((short) len);
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;

View file

@ -0,0 +1,23 @@
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;
}
}

View file

@ -12,7 +12,7 @@ public final class Ln {
private static final String PREFIX = "[server] ";
enum Level {
DEBUG, INFO, WARN, ERROR
VERBOSE, DEBUG, INFO, WARN, ERROR
}
private static Level threshold = Level.INFO;
@ -36,6 +36,13 @@ public final class Ln {
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);
@ -50,13 +57,20 @@ public final class Ln {
}
}
public static void w(String message) {
public static void w(String message, Throwable throwable) {
if (isEnabled(Level.WARN)) {
Log.w(TAG, message);
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);

View file

@ -2,20 +2,25 @@ package com.genymobile.scrcpy;
import android.graphics.Rect;
import java.util.List;
public class Options {
private Ln.Level logLevel;
private Ln.Level logLevel = Ln.Level.DEBUG;
private int maxSize;
private int bitRate;
private int bitRate = 8000000;
private int maxFps;
private int lockedVideoOrientation;
private int lockVideoOrientation = -1;
private boolean tunnelForward;
private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly
private boolean control;
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 String codecOptions;
private List<CodecOption> codecOptions;
private String encoderName;
private boolean powerOffScreenOnClose;
private boolean clipboardAutosync = true;
public Ln.Level getLogLevel() {
return logLevel;
@ -49,12 +54,12 @@ public class Options {
this.maxFps = maxFps;
}
public int getLockedVideoOrientation() {
return lockedVideoOrientation;
public int getLockVideoOrientation() {
return lockVideoOrientation;
}
public void setLockedVideoOrientation(int lockedVideoOrientation) {
this.lockedVideoOrientation = lockedVideoOrientation;
public void setLockVideoOrientation(int lockVideoOrientation) {
this.lockVideoOrientation = lockVideoOrientation;
}
public boolean isTunnelForward() {
@ -113,11 +118,35 @@ public class Options {
this.stayAwake = stayAwake;
}
public String getCodecOptions() {
public List<CodecOption> getCodecOptions() {
return codecOptions;
}
public void setCodecOptions(String 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;
}
}

View file

@ -5,13 +5,17 @@ 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;
@ -26,17 +30,19 @@ public class ScreenEncoder implements Device.RotationListener {
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) {
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
@ -50,17 +56,13 @@ public class ScreenEncoder implements Device.RotationListener {
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
Workarounds.prepareMainLooper();
try {
internalStreamScreen(device, fd);
} catch (NullPointerException e) {
// Retry with workarounds enabled:
// <https://github.com/Genymobile/scrcpy/issues/365>
// <https://github.com/Genymobile/scrcpy/issues/940>
Ln.d("Applying workarounds to avoid NullPointerException");
if (Build.BRAND.equalsIgnoreCase("meizu")) {
// <https://github.com/Genymobile/scrcpy/issues/240>
// <https://github.com/Genymobile/scrcpy/issues/2656>
Workarounds.fillAppInfo();
internalStreamScreen(device, fd);
}
internalStreamScreen(device, fd);
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
@ -69,7 +71,7 @@ public class ScreenEncoder implements Device.RotationListener {
boolean alive;
try {
do {
MediaCodec codec = createCodec();
MediaCodec codec = createCodec(encoderName);
IBinder display = createDisplay();
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
@ -150,8 +152,30 @@ public class ScreenEncoder implements Device.RotationListener {
IO.writeFully(fd, headerBuffer);
}
private static MediaCodec createCodec() throws IOException {
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
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) {
@ -198,7 +222,11 @@ public class ScreenEncoder implements Device.RotationListener {
}
private static IBinder createDisplay() {
return SurfaceControl.createDisplay("scrcpy", true);
// 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) {

View file

@ -82,6 +82,12 @@ public final class ScreenInfo {
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) {

View file

@ -1,9 +1,8 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.os.BatteryManager;
import android.os.Build;
@ -18,24 +17,25 @@ public final class Server {
// not instantiable
}
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 = CodecOption.parse(options.getCodecOptions());
private static void initAndCleanUp(Options options) {
boolean mustDisableShowTouchesOnCleanUp = false;
int restoreStayOn = -1;
if (options.getShowTouches() || options.getStayAwake()) {
try (ContentProvider settings = device.createSettingsProvider()) {
if (options.getShowTouches()) {
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1");
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;
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
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) {
@ -45,23 +45,40 @@ public final class Server {
} catch (NumberFormatException e) {
restoreStayOn = 0;
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
}
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
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);
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);
final Controller controller = new Controller(device, connection, options.getClipboardAutosync());
// asynchronous
startController(controller);
startDeviceMessageSender(controller.getSender());
controllerThread = startController(controller);
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
device.setClipboardListener(new Device.ClipboardListener() {
@Override
@ -77,12 +94,31 @@ public final class Server {
} 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 void startController(final Controller controller) {
new Thread(new Runnable() {
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 {
@ -92,11 +128,13 @@ public final class Server {
Ln.d("Controller stopped");
}
}
}).start();
});
thread.start();
return thread;
}
private static void startDeviceMessageSender(final DeviceMessageSender sender) {
new Thread(new Runnable() {
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
@ -106,7 +144,9 @@ public final class Server {
Ln.d("Device message sender stopped");
}
}
}).start();
});
thread.start();
return thread;
}
private static Options createOptions(String... args) {
@ -120,58 +160,93 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
final int expectedParameters = 14;
if (args.length != expectedParameters) {
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
}
Options options = new Options();
Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH));
options.setLogLevel(level);
int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8
options.setMaxSize(maxSize);
int bitRate = Integer.parseInt(args[3]);
options.setBitRate(bitRate);
int maxFps = Integer.parseInt(args[4]);
options.setMaxFps(maxFps);
int lockedVideoOrientation = Integer.parseInt(args[5]);
options.setLockedVideoOrientation(lockedVideoOrientation);
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[6]);
options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[7]);
options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[8]);
options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[9]);
options.setControl(control);
int displayId = Integer.parseInt(args[10]);
options.setDisplayId(displayId);
boolean showTouches = Boolean.parseBoolean(args[11]);
options.setShowTouches(showTouches);
boolean stayAwake = Boolean.parseBoolean(args[12]);
options.setStayAwake(stayAwake);
String codecOptions = args[13];
options.setCodecOptions(codecOptions);
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 ("-".equals(crop)) {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"
@ -206,6 +281,15 @@ public final class Server {
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() + "'");
}
}
}
}

View file

@ -0,0 +1,84 @@
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;
}
}

View file

@ -0,0 +1,11 @@
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);
}
}

View file

@ -16,6 +16,7 @@ public final class 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()"

View file

@ -14,7 +14,7 @@ public class ActivityManager {
private final IInterface manager;
private Method getContentProviderExternalMethod;
private boolean getContentProviderExternalMethodLegacy;
private boolean getContentProviderExternalMethodNewVersion = true;
private Method removeContentProviderExternalMethod;
public ActivityManager(IInterface manager) {
@ -29,7 +29,7 @@ public class ActivityManager {
} catch (NoSuchMethodException e) {
// old version
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
getContentProviderExternalMethodLegacy = true;
getContentProviderExternalMethodNewVersion = false;
}
}
return getContentProviderExternalMethod;
@ -46,7 +46,7 @@ public class ActivityManager {
try {
Method method = getGetContentProviderExternalMethod();
Object[] args;
if (!getContentProviderExternalMethodLegacy) {
if (getContentProviderExternalMethodNewVersion) {
// new version
args = new Object[]{name, ServiceManager.USER_ID, token, null};
} else {

View file

@ -1,7 +1,9 @@
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;
@ -35,7 +37,9 @@ public class ContentProvider implements Closeable {
private final IBinder token;
private Method callMethod;
private boolean callMethodLegacy;
private int callMethodVersion;
private Object attributionSource;
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
this.manager = manager;
@ -44,32 +48,69 @@ public class ContentProvider implements Closeable {
this.token = token;
}
@SuppressLint("PrivateApi")
private Method getCallMethod() throws NoSuchMethodException {
if (callMethod == null) {
try {
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
} catch (NoSuchMethodException e) {
// old version
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
callMethodLegacy = true;
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;
}
private Bundle call(String callMethod, String arg, Bundle extras) {
@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;
if (!callMethodLegacy) {
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
} else {
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
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 e) {
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
Ln.e("Could not invoke method", e);
return null;
throw e;
}
}
@ -103,30 +144,31 @@ public class ContentProvider implements Closeable {
}
}
public String getValue(String table, String key) {
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);
Bundle bundle = call(method, key, arg);
if (bundle == null) {
return null;
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);
}
return bundle.getString("value");
}
public void putValue(String table, String key, String value) {
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);
call(method, key, arg);
}
public String getAndPutValue(String table, String key, String value) {
String oldValue = getValue(table, key);
if (!value.equals(oldValue)) {
putValue(table, key, value);
try {
call(method, key, arg);
} catch (Exception e) {
throw new SettingsException(table, "put", key, value, e);
}
return oldValue;
}
}

View file

@ -77,7 +77,14 @@ public final class ServiceManager {
public ClipboardManager getClipboardManager() {
if (clipboardManager == null) {
clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard"));
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;
}

View file

@ -11,6 +11,9 @@ 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) {
@ -19,11 +22,31 @@ public class StatusBarManager {
private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException {
if (expandNotificationsPanelMethod == null) {
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
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");
@ -34,7 +57,26 @@ public class StatusBarManager {
public void expandNotificationsPanel() {
try {
Method method = getExpandNotificationsPanelMethod();
method.invoke(manager);
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);
}

View file

@ -2,7 +2,6 @@ package com.genymobile.scrcpy;
import android.view.KeyEvent;
import android.view.MotionEvent;
import org.junit.Assert;
import org.junit.Test;
@ -25,6 +24,7 @@ public class ControlMessageReaderTest {
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();
@ -37,6 +37,7 @@ public class ControlMessageReaderTest {
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());
}
@ -48,7 +49,7 @@ public class ControlMessageReaderTest {
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length);
dos.writeInt(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
@ -68,7 +69,7 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH];
Arrays.fill(text, (byte) 'a');
dos.writeShort(text.length);
dos.writeInt(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
@ -152,6 +153,7 @@ public class ControlMessageReaderTest {
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();
@ -159,6 +161,7 @@ public class ControlMessageReaderTest {
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
}
@Test
@ -178,19 +181,35 @@ public class ControlMessageReaderTest {
}
@Test
public void testParseCollapseNotificationPanelEvent() throws IOException {
public void testParseExpandSettingsPanelEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL);
dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType());
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
@ -200,6 +219,7 @@ public class ControlMessageReaderTest {
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();
@ -207,6 +227,7 @@ public class ControlMessageReaderTest {
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType());
Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey());
}
@Test
@ -216,9 +237,10 @@ public class ControlMessageReaderTest {
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.writeShort(text.length);
dos.writeInt(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
@ -227,10 +249,9 @@ public class ControlMessageReaderTest {
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(0x0102030405060708L, event.getSequence());
Assert.assertEquals("testé", event.getText());
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
Assert.assertTrue(parse);
Assert.assertTrue(event.getPaste());
}
@Test
@ -242,11 +263,12 @@ public class ControlMessageReaderTest {
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.writeShort(rawText.length);
dos.writeInt(rawText.length);
dos.write(rawText);
byte[] packet = bos.toByteArray();
@ -255,10 +277,9 @@ public class ControlMessageReaderTest {
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(0x0807060504030201L, event.getSequence());
Assert.assertEquals(text, event.getText());
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
Assert.assertTrue(parse);
Assert.assertTrue(event.getPaste());
}
@Test
@ -308,11 +329,13 @@ public class ControlMessageReaderTest {
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();
@ -322,12 +345,14 @@ public class ControlMessageReaderTest {
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());
}
@ -341,6 +366,7 @@ public class ControlMessageReaderTest {
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);
@ -353,6 +379,7 @@ public class ControlMessageReaderTest {
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();
@ -360,6 +387,7 @@ public class ControlMessageReaderTest {
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));
@ -369,6 +397,7 @@ public class ControlMessageReaderTest {
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());
}
}

View file

@ -19,7 +19,7 @@ public class DeviceMessageWriterTest {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
dos.writeShort(data.length);
dos.writeInt(data.length);
dos.write(data);
byte[] expected = bos.toByteArray();
@ -32,4 +32,24 @@ public class DeviceMessageWriterTest {
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);
}
}

Binary file not shown.