Merge pull request #220 from barry-ran/dev

Dev
This commit is contained in:
Barry 2020-06-14 16:51:37 +08:00 committed by GitHub
commit 2ca4bef199
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1726 additions and 355 deletions

View file

@ -32,6 +32,11 @@ msvc{
*g++*: QMAKE_CXXFLAGS += -Werror
*msvc*: QMAKE_CXXFLAGS += /WX /wd4566
# run a server debugger and wait for a client to be attached
# DEFINES += SERVER_DEBUGGER
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
# DEFINES += SERVER_DEBUGGER_METHOD_NEW
# 源码
SOURCES += \
main.cpp \

View file

@ -135,7 +135,7 @@ void Controller::onSetDeviceClipboard()
if (!controlMsg) {
return;
}
controlMsg->setSetClipboardMsgData(text);
controlMsg->setSetClipboardMsgData(text, true);
postControlMsg(controlMsg);
}

View file

@ -29,9 +29,9 @@ void ControlMsg::setInjectKeycodeMsgData(AndroidKeyeventAction action, AndroidKe
void ControlMsg::setInjectTextMsgData(QString &text)
{
// write length (2 byte) + string (non nul-terminated)
if (CONTROL_MSG_TEXT_MAX_LENGTH < text.length()) {
if (CONTROL_MSG_INJECT_TEXT_MAX_LENGTH < text.length()) {
// injecting a text takes time, so limit the text length
text = text.left(CONTROL_MSG_TEXT_MAX_LENGTH);
text = text.left(CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
}
QByteArray tmp = text.toUtf8();
m_data.injectText.text = new char[tmp.length() + 1];
@ -55,7 +55,7 @@ void ControlMsg::setInjectScrollMsgData(QRect position, qint32 hScroll, qint32 v
m_data.injectScroll.vScroll = vScroll;
}
void ControlMsg::setSetClipboardMsgData(QString &text)
void ControlMsg::setSetClipboardMsgData(QString &text, bool paste)
{
if (text.isEmpty()) {
return;
@ -68,6 +68,7 @@ void ControlMsg::setSetClipboardMsgData(QString &text)
m_data.setClipboard.text = new char[tmp.length() + 1];
memcpy(m_data.setClipboard.text, tmp.data(), tmp.length());
m_data.setClipboard.text[tmp.length()] = '\0';
m_data.setClipboard.paste = paste;
}
void ControlMsg::setSetScreenPowerModeData(ControlMsg::ScreenPowerMode mode)
@ -124,6 +125,7 @@ QByteArray ControlMsg::serializeData()
BufferUtil::write32(buffer, m_data.injectScroll.vScroll);
break;
case CMT_SET_CLIPBOARD:
buffer.putChar(!!m_data.setClipboard.paste);
BufferUtil::write16(buffer, static_cast<quint32>(strlen(m_data.setClipboard.text)));
buffer.write(m_data.setClipboard.text, strlen(m_data.setClipboard.text));
break;

View file

@ -9,8 +9,8 @@
#include "keycodes.h"
#include "qscrcpyevent.h"
#define CONTROL_MSG_TEXT_MAX_LENGTH 300
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093
#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4092
#define POINTER_ID_MOUSE static_cast<quint64>(-1)
// ControlMsg
class ControlMsg : public QScrcpyEvent
@ -48,7 +48,7 @@ public:
// position action动作对应的位置
void setInjectTouchMsgData(quint64 id, AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position, float pressure);
void setInjectScrollMsgData(QRect position, qint32 hScroll, qint32 vScroll);
void setSetClipboardMsgData(QString &text);
void setSetClipboardMsgData(QString &text, bool paste);
void setSetScreenPowerModeData(ControlMsg::ScreenPowerMode mode);
QByteArray serializeData();
@ -90,6 +90,7 @@ private:
struct
{
char *text = Q_NULLPTR;
bool paste = true;
} setClipboard;
struct
{

View file

@ -19,12 +19,12 @@ void InputConvertGame::mouseEvent(const QMouseEvent *from, const QSize &frameSiz
return;
}
if (!switchGameMap()) {
m_needSwitchGameAgain = false;
m_needBackMouseMove = false;
}
return;
}
if (m_gameMap) {
if (!m_needBackMouseMove && m_gameMap) {
updateSize(frameSize, showSize);
// mouse move
if (m_keyMap.isValidMouseMoveMap()) {
@ -57,14 +57,14 @@ void InputConvertGame::keyEvent(const QKeyEvent *from, const QSize &frameSize, c
return;
}
if (!switchGameMap()) {
m_needSwitchGameAgain = false;
m_needBackMouseMove = false;
}
return;
}
const KeyMap::KeyMapNode &node = m_keyMap.getKeyMapNodeKey(from->key());
// 处理特殊按键:可以在按键映射和普通映射间切换的按键
if (m_needSwitchGameAgain && KeyMap::KMT_CLICK == node.type && node.data.click.switchMap) {
// 处理特殊按键:可以释放出鼠标的按键
if (m_needBackMouseMove && KeyMap::KMT_CLICK == node.type && node.data.click.switchMap) {
updateSize(frameSize, showSize);
// Qt::Key_Tab Qt::Key_M for PUBG mobile
processKeyClick(node.data.click.keyNode.pos, false, node.data.click.switchMap, from);
@ -281,8 +281,8 @@ void InputConvertGame::processSteerWheel(const KeyMap::KeyMapNode &node, const Q
void InputConvertGame::processKeyClick(const QPointF &clickPos, bool clickTwice, bool switchMap, const QKeyEvent *from)
{
if (switchMap && QEvent::KeyRelease == from->type()) {
m_needSwitchGameAgain = !m_needSwitchGameAgain;
switchGameMap();
m_needBackMouseMove = !m_needBackMouseMove;
hideMouseCursor(!m_needBackMouseMove);
}
if (QEvent::KeyPress == from->type()) {
@ -463,18 +463,27 @@ bool InputConvertGame::switchGameMap()
// grab cursor and set cursor only mouse move map
emit grabCursor(m_gameMap);
if (m_gameMap) {
hideMouseCursor(m_gameMap);
if (!m_gameMap) {
stopMouseMoveTimer();
mouseMoveStopTouch();
}
return m_gameMap;
}
void InputConvertGame::hideMouseCursor(bool hide)
{
if (hide) {
#ifdef QT_NO_DEBUG
QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
#else
QGuiApplication::setOverrideCursor(QCursor(Qt::CrossCursor));
#endif
} else {
stopMouseMoveTimer();
mouseMoveStopTouch();
QGuiApplication::restoreOverrideCursor();
}
return m_gameMap;
}
void InputConvertGame::timerEvent(QTimerEvent *event)

View file

@ -54,6 +54,7 @@ protected:
bool switchGameMap();
bool checkCursorPos(const QMouseEvent *from);
void hideMouseCursor(bool hide);
protected:
void timerEvent(QTimerEvent *event);
@ -62,7 +63,7 @@ private:
QSize m_frameSize;
QSize m_showSize;
bool m_gameMap = false;
bool m_needSwitchGameAgain = false;
bool m_needBackMouseMove = false;
int m_multiTouchID[MULTI_TOUCH_MAX_NUM] = { 0 };
KeyMap m_keyMap;

View file

@ -173,8 +173,10 @@ void Device::initSignals()
connect(this, &Device::switchFullScreen, m_videoForm, &VideoForm::onSwitchFullScreen);
}
if (m_fileHandler) {
connect(this, &Device::pushFileRequest, m_fileHandler, &FileHandler::onPushFileRequest);
connect(this, &Device::installApkRequest, m_fileHandler, &FileHandler::onInstallApkRequest);
connect(this, &Device::pushFileRequest, this, [this](const QString &file, const QString &devicePath) {
m_fileHandler->onPushFileRequest(getSerial(), file, devicePath);
});
connect(this, &Device::installApkRequest, this, [this](const QString &apkFile) { m_fileHandler->onInstallApkRequest(getSerial(), apkFile); });
connect(m_fileHandler, &FileHandler::fileHandlerResult, this, [this](FileHandler::FILE_HANDLER_RESULT processResult, bool isApk) {
QString tipsType = "";
if (isApk) {
@ -303,6 +305,8 @@ void Device::startServer()
params.crop = "-";
params.control = true;
params.useReverse = m_params.useReverse;
params.lockVideoOrientation = m_params.lockVideoOrientation;
params.stayAwake = m_params.stayAwake;
m_server->start(params);
});
}

View file

@ -35,6 +35,8 @@ public:
bool display = true; // 是否显示画面(或者仅仅后台录制)
QString gameScript = ""; // 游戏映射脚本
bool renderExpiredFrames = false; // 是否渲染延迟视频帧
int lockVideoOrientation = -1; // 是否锁定视频方向
int stayAwake = false; // 是否保持唤醒
};
enum GroupControlState
{
@ -73,8 +75,8 @@ signals:
void requestDeviceClipboard();
void setDeviceClipboard();
void clipboardPaste();
void pushFileRequest(const QString &serial, const QString &file, const QString &devicePath = "");
void installApkRequest(const QString &serial, const QString &apkFile);
void pushFileRequest(const QString &file, const QString &devicePath = "");
void installApkRequest(const QString &apkFile);
// key map
void mouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize);

View file

@ -124,12 +124,29 @@ bool Server::execute()
args << "shell";
args << QString("CLASSPATH=%1").arg(Config::getInstance().getServerPath());
args << "app_process";
args << "/"; // unused;
#ifdef SERVER_DEBUGGER
#define SERVER_DEBUGGER_PORT "5005"
args <<
#ifdef SERVER_DEBUGGER_METHOD_NEW
/* Android 9 and above */
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address="
#else
/* Android 8 and below */
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
#endif
SERVER_DEBUGGER_PORT,
#endif
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 << "-";
@ -138,6 +155,24 @@ bool Server::execute()
}
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
// code option
// https://github.com/Genymobile/scrcpy/commit/080a4ee3654a9b7e96c8ffe37474b5c21c02852a
// <https://d.android.com/reference/android/media/MediaFormat>
args << "-";
#ifdef SERVER_DEBUGGER
qInfo("Server debugger waiting for a client on device port " SERVER_DEBUGGER_PORT "...");
// From the computer, run
// adb forward tcp:5005 tcp:5005
// Then, from Android Studio: Run > Debug > Edit configurations...
// On the left, click on '+', "Remote", with:
// Host: localhost
// Port: 5005
// Then click on "Debug"
#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"
@ -245,27 +280,6 @@ bool Server::startServerByStep()
stepSuccess = enableTunnelForward();
break;
case SSS_EXECUTE_SERVER:
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// "adb forward", so the app socket is the client
if (!m_tunnelForward) {
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no need to
// try to connect until the server socket is listening on the device.
m_serverSocket.setMaxPendingConnections(2);
if (!m_serverSocket.listen(QHostAddress::LocalHost, m_params.localPort)) {
qCritical() << QString("Could not listen on port %1").arg(m_params.localPort).toStdString().c_str();
m_serverStartStep = SSS_NULL;
if (m_tunnelForward) {
disableTunnelForward();
} else {
disableTunnelReverse();
}
emit serverStartResult(false);
return false;
}
}
// server will connect to our server socket
stepSuccess = execute();
break;
@ -432,6 +446,20 @@ void Server::onWorkProcessResult(AdbProcess::ADB_EXEC_RESULT processResult)
break;
case SSS_ENABLE_TUNNEL_REVERSE:
if (AdbProcess::AER_SUCCESS_EXEC == processResult) {
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no need to
// try to connect until the server socket is listening on the device.
m_serverSocket.setMaxPendingConnections(2);
if (!m_serverSocket.listen(QHostAddress::LocalHost, m_params.localPort)) {
qCritical() << QString("Could not listen on port %1").arg(m_params.localPort).toStdString().c_str();
m_serverStartStep = SSS_NULL;
disableTunnelReverse();
emit serverStartResult(false);
break;
}
m_serverStartStep = SSS_EXECUTE_SERVER;
startServerByStep();
} else if (AdbProcess::AER_SUCCESS_START != processResult) {

View file

@ -26,14 +26,16 @@ class Server : public QObject
public:
struct ServerParams
{
QString serial = ""; // 设备序列号
quint16 localPort = 27183; // reverse时本地监听端口
quint16 maxSize = 720; // 视频分辨率
quint32 bitRate = 8000000; // 视频比特率
quint32 maxFps = 60; // 视频最大帧率
QString crop = "-"; // 视频裁剪
bool control = true; // 安卓端是否接收键鼠控制
bool useReverse = true; // true:先使用adb reverse失败后自动使用adb forwardfalse:直接使用adb forward
QString serial = ""; // 设备序列号
quint16 localPort = 27183; // reverse时本地监听端口
quint16 maxSize = 720; // 视频分辨率
quint32 bitRate = 8000000; // 视频比特率
quint32 maxFps = 60; // 视频最大帧率
QString crop = "-"; // 视频裁剪
bool control = true; // 安卓端是否接收键鼠控制
bool useReverse = true; // true:先使用adb reverse失败后自动使用adb forwardfalse:直接使用adb forward
int lockVideoOrientation = -1; // 是否锁定视频方向
int stayAwake = false; // 是否保持唤醒
};
explicit Server(QObject *parent = nullptr);

View file

@ -92,6 +92,7 @@ QRect VideoForm::getGrabCursorRect()
// high dpi support
rc.setTopLeft(rc.topLeft() * m_videoWidget->devicePixelRatio());
rc.setBottomRight(rc.bottomRight() * m_videoWidget->devicePixelRatio());
rc.setX(rc.x() + 10);
rc.setY(rc.y() + 10);
rc.setWidth(rc.width() - 20);
@ -100,12 +101,21 @@ QRect VideoForm::getGrabCursorRect()
rc = m_videoWidget->geometry();
rc.setTopLeft(ui->keepRadioWidget->mapToGlobal(rc.topLeft()));
rc.setBottomRight(ui->keepRadioWidget->mapToGlobal(rc.bottomRight()));
rc.setX(rc.x() + 10);
rc.setY(rc.y() + 10);
rc.setWidth(rc.width() - 20);
rc.setHeight(rc.height() - 20);
#else
#elif defined(Q_OS_LINUX)
rc = QRect(ui->keepRadioWidget->mapToGlobal(m_videoWidget->pos()), m_videoWidget->size());
// high dpi support -- taken from the WIN32 section and untested
rc.setTopLeft(rc.topLeft() * m_videoWidget->devicePixelRatio());
rc.setBottomRight(rc.bottomRight() * m_videoWidget->devicePixelRatio());
rc.setX(rc.x() + 10);
rc.setY(rc.y() + 10);
rc.setWidth(rc.width() - 20);
rc.setHeight(rc.height() - 20);
#endif
return rc;
}
@ -319,14 +329,16 @@ QRect VideoForm::getScreenRect()
if (!win) {
return screenRect;
}
QWindow *winHandle = win->windowHandle();
if (!winHandle) {
return screenRect;
QScreen *screen = QGuiApplication::primaryScreen();
if (winHandle) {
screen = winHandle->screen();
}
QScreen *screen = winHandle->screen();
if (!screen) {
return screenRect;
}
screenRect = screen->availableGeometry();
return screenRect;
}
@ -388,6 +400,11 @@ void VideoForm::updateShowSize(const QSize &newSize)
if (isFullScreen() && m_device) {
emit m_device->switchFullScreen();
}
if (isMaximized()) {
showNormal();
}
if (m_skin) {
QMargins m = getMargins(vertical);
showSize.setWidth(showSize.width() + m.left() + m.right());
@ -558,7 +575,9 @@ void VideoForm::mouseMoveEvent(QMouseEvent *event)
void VideoForm::mouseDoubleClickEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && !m_videoWidget->geometry().contains(event->pos())) {
removeBlackRect();
if (!isMaximized()) {
removeBlackRect();
}
}
if (event->button() == Qt::RightButton && m_device) {
@ -693,8 +712,8 @@ void VideoForm::dropEvent(QDropEvent *event)
}
if (fileInfo.isFile() && fileInfo.suffix() == "apk") {
emit m_device->installApkRequest(m_device->getSerial(), file);
emit m_device->installApkRequest(file);
return;
}
emit m_device->pushFileRequest(m_device->getSerial(), file, Config::getInstance().getPushFilePath() + fileInfo.fileName());
emit m_device->pushFileRequest(file, Config::getInstance().getPushFilePath() + fileInfo.fileName());
}

View file

@ -109,6 +109,13 @@ void Dialog::initUI()
ui->formatBox->addItem("mkv");
ui->formatBox->setCurrentIndex(Config::getInstance().getRecordFormatIndex());
ui->lockOrientationBox->addItem(tr("no lock"));
ui->lockOrientationBox->addItem("0");
ui->lockOrientationBox->addItem("90");
ui->lockOrientationBox->addItem("180");
ui->lockOrientationBox->addItem("270");
ui->lockOrientationBox->setCurrentIndex(0);
ui->recordPathEdt->setText(Config::getInstance().getRecordPath());
ui->framelessCheck->setChecked(Config::getInstance().getFramelessWindow());
@ -186,6 +193,8 @@ void Dialog::on_startServerBtn_clicked()
params.useReverse = ui->useReverseCheck->isChecked();
params.display = !ui->notDisplayCheck->isChecked();
params.renderExpiredFrames = Config::getInstance().getRenderExpiredFrames();
params.lockVideoOrientation = ui->lockOrientationBox->currentIndex() - 1;
params.stayAwake = ui->stayAwakeCheck->isChecked();
m_deviceManage.connectDevice(params);

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>420</width>
<height>492</height>
<height>517</height>
</rect>
</property>
<property name="minimumSize">
@ -106,6 +106,47 @@
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="configWidget5" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>lock orientation:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="lockOrientationBox"/>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="configWidget2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_6">
@ -290,6 +331,13 @@
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QCheckBox" name="stayAwakeCheck">
<property name="text">
<string>stay awake</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View file

@ -17,6 +17,9 @@ static QtMessageHandler g_oldMessageHandler = Q_NULLPTR;
void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg);
void installTranslator();
static QtMsgType g_msgType = QtInfoMsg;
QtMsgType covertLogLevel(const QString &logLevel);
int main(int argc, char *argv[])
{
// set env
@ -38,6 +41,8 @@ int main(int argc, char *argv[])
qputenv("QTSCRCPY_KEYMAP_PATH", "../../../keymap");
#endif
g_msgType = covertLogLevel(Config::getInstance().getLogLevel());
// set on QApplication before
int opengl = Config::getInstance().getDesktopOpenGL();
if (0 == opengl) {
@ -136,17 +141,53 @@ void installTranslator()
qApp->installTranslator(&translator);
}
QtMsgType covertLogLevel(const QString &logLevel)
{
if ("debug" == logLevel) {
return QtDebugMsg;
}
if ("info" == logLevel) {
return QtInfoMsg;
}
if ("warn" == logLevel) {
return QtWarningMsg;
}
if ("error" == logLevel) {
return QtCriticalMsg;
}
#ifdef QT_NO_DEBUG
return QtInfoMsg;
#else
return QtDebugMsg;
#endif
}
void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
if (g_oldMessageHandler) {
g_oldMessageHandler(type, context, msg);
}
if (QtDebugMsg < type) {
// qt log info big than warning?
float fLogLevel = 1.0f * g_msgType;
if (QtInfoMsg == g_msgType) {
fLogLevel = QtDebugMsg + 0.5f;
}
float fLogLevel2 = 1.0f * type;
if (QtInfoMsg == type) {
fLogLevel2 = QtDebugMsg + 0.5f;
}
if (fLogLevel <= fLogLevel2) {
if (g_mainDlg && g_mainDlg->isVisible() && !g_mainDlg->filterLog(msg)) {
g_mainDlg->outLog(msg);
}
}
if (QtFatalMsg == type) {
//abort();
}

Binary file not shown.

View file

@ -49,17 +49,17 @@
<context>
<name>Dialog</name>
<message>
<location filename="../../dialog.ui" line="429"/>
<location filename="../../dialog.ui" line="477"/>
<source>Wireless</source>
<translation>Wireless</translation>
</message>
<message>
<location filename="../../dialog.ui" line="513"/>
<location filename="../../dialog.ui" line="561"/>
<source>wireless connect</source>
<translation>wireless connect</translation>
</message>
<message>
<location filename="../../dialog.ui" line="529"/>
<location filename="../../dialog.ui" line="577"/>
<source>wireless disconnect</source>
<translation>wireless disconnect</translation>
</message>
@ -69,13 +69,13 @@
<translation>Start Config</translation>
</message>
<message>
<location filename="../../dialog.ui" line="127"/>
<location filename="../../dialog.ui" line="168"/>
<source>record save path:</source>
<translation>record save path:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="144"/>
<location filename="../../dialog.cpp" line="325"/>
<location filename="../../dialog.ui" line="185"/>
<location filename="../../dialog.cpp" line="338"/>
<source>select path</source>
<translation>select path</translation>
</message>
@ -85,47 +85,57 @@
<translation>record format:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="282"/>
<location filename="../../dialog.ui" line="323"/>
<source>record screen</source>
<translation>record screen</translation>
</message>
<message>
<location filename="../../dialog.ui" line="227"/>
<location filename="../../dialog.ui" line="268"/>
<source>frameless</source>
<translation>frameless</translation>
</message>
<message>
<location filename="../../dialog.ui" line="289"/>
<location filename="../../dialog.ui" line="127"/>
<source>lock orientation:</source>
<translation>lock orientation:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="330"/>
<source>show fps</source>
<translation>show fps</translation>
</message>
<message>
<location filename="../../dialog.ui" line="386"/>
<location filename="../../dialog.ui" line="337"/>
<source>stay awake</source>
<translation>stay awake</translation>
</message>
<message>
<location filename="../../dialog.ui" line="434"/>
<source>stop all server</source>
<translation>stop all server</translation>
</message>
<message>
<location filename="../../dialog.ui" line="563"/>
<location filename="../../dialog.ui" line="611"/>
<source>adb command:</source>
<translation>adb command:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="599"/>
<location filename="../../dialog.ui" line="647"/>
<source>terminate</source>
<translation>terminate</translation>
</message>
<message>
<location filename="../../dialog.ui" line="586"/>
<location filename="../../dialog.ui" line="634"/>
<source>execute</source>
<translation>execute</translation>
</message>
<message>
<location filename="../../dialog.ui" line="612"/>
<location filename="../../dialog.ui" line="660"/>
<source>clear</source>
<translation>clear</translation>
</message>
<message>
<location filename="../../dialog.ui" line="272"/>
<location filename="../../dialog.ui" line="313"/>
<source>reverse connection</source>
<translation>reverse connection</translation>
</message>
@ -134,17 +144,17 @@
<translation type="vanished">auto enable</translation>
</message>
<message>
<location filename="../../dialog.ui" line="256"/>
<location filename="../../dialog.ui" line="297"/>
<source>background record</source>
<translation>background record</translation>
</message>
<message>
<location filename="../../dialog.ui" line="220"/>
<location filename="../../dialog.ui" line="261"/>
<source>screen-off</source>
<translation>screen-off</translation>
</message>
<message>
<location filename="../../dialog.ui" line="189"/>
<location filename="../../dialog.ui" line="230"/>
<source>apply</source>
<translation>apply</translation>
</message>
@ -154,37 +164,37 @@
<translation>max size:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="240"/>
<location filename="../../dialog.ui" line="281"/>
<source>always on top</source>
<translation>always on top</translation>
</message>
<message>
<location filename="../../dialog.ui" line="182"/>
<location filename="../../dialog.ui" line="223"/>
<source>refresh script</source>
<translation>refresh script</translation>
</message>
<message>
<location filename="../../dialog.ui" line="403"/>
<location filename="../../dialog.ui" line="451"/>
<source>get device IP</source>
<translation>get device IP</translation>
</message>
<message>
<location filename="../../dialog.ui" line="302"/>
<location filename="../../dialog.ui" line="350"/>
<source>USB line</source>
<translation>USB line</translation>
</message>
<message>
<location filename="../../dialog.ui" line="358"/>
<location filename="../../dialog.ui" line="406"/>
<source>stop server</source>
<translation>stop server</translation>
</message>
<message>
<location filename="../../dialog.ui" line="348"/>
<location filename="../../dialog.ui" line="396"/>
<source>start server</source>
<translation>start server</translation>
</message>
<message>
<location filename="../../dialog.ui" line="338"/>
<location filename="../../dialog.ui" line="386"/>
<source>device serial:</source>
<translation>device serial:</translation>
</message>
@ -198,20 +208,25 @@
<translation>bit rate:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="413"/>
<location filename="../../dialog.ui" line="461"/>
<source>start adbd</source>
<translation>start adbd</translation>
</message>
<message>
<location filename="../../dialog.ui" line="393"/>
<location filename="../../dialog.ui" line="441"/>
<source>refresh devices</source>
<translation>refresh devices</translation>
</message>
<message>
<location filename="../../dialog.cpp" line="101"/>
<location filename="../../dialog.cpp" line="105"/>
<source>original</source>
<translation>original</translation>
</message>
<message>
<location filename="../../dialog.cpp" line="112"/>
<source>no lock</source>
<translation>no lock</translation>
</message>
</context>
<context>
<name>QObject</name>
@ -226,7 +241,7 @@ You can download it at the following address:</source>
<translation type="vanished">This software is completely open source and free.\nStrictly used for illegal purposes, or at your own risk.\nYou can download it at the following address:</translation>
</message>
<message>
<location filename="../../main.cpp" line="103"/>
<location filename="../../main.cpp" line="108"/>
<source>This software is completely open source and free. Strictly used for illegal purposes, or at your own risk. You can download it at the following address:</source>
<translation>This software is completely open source and free. Strictly used for illegal purposes, or at your own risk. You can download it at the following address:</translation>
</message>
@ -322,7 +337,7 @@ You can download it at the following address:</source>
<translation type="vanished">file transfer failed</translation>
</message>
<message>
<location filename="../../device/ui/videoform.cpp" line="671"/>
<location filename="../../device/ui/videoform.cpp" line="710"/>
<source>file does not exist</source>
<translation>file does not exist</translation>
</message>

Binary file not shown.

View file

@ -49,17 +49,17 @@
<context>
<name>Dialog</name>
<message>
<location filename="../../dialog.ui" line="429"/>
<location filename="../../dialog.ui" line="477"/>
<source>Wireless</source>
<translation>线</translation>
</message>
<message>
<location filename="../../dialog.ui" line="513"/>
<location filename="../../dialog.ui" line="561"/>
<source>wireless connect</source>
<translation>线</translation>
</message>
<message>
<location filename="../../dialog.ui" line="529"/>
<location filename="../../dialog.ui" line="577"/>
<source>wireless disconnect</source>
<translation>线</translation>
</message>
@ -69,13 +69,13 @@
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="127"/>
<location filename="../../dialog.ui" line="168"/>
<source>record save path:</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="144"/>
<location filename="../../dialog.cpp" line="325"/>
<location filename="../../dialog.ui" line="185"/>
<location filename="../../dialog.cpp" line="338"/>
<source>select path</source>
<translation></translation>
</message>
@ -85,47 +85,57 @@
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="282"/>
<location filename="../../dialog.ui" line="323"/>
<source>record screen</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="227"/>
<location filename="../../dialog.ui" line="268"/>
<source>frameless</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="289"/>
<location filename="../../dialog.ui" line="127"/>
<source>lock orientation:</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="330"/>
<source>show fps</source>
<translation>fps</translation>
</message>
<message>
<location filename="../../dialog.ui" line="386"/>
<location filename="../../dialog.ui" line="337"/>
<source>stay awake</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="434"/>
<source>stop all server</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="563"/>
<location filename="../../dialog.ui" line="611"/>
<source>adb command:</source>
<translation>adb命令</translation>
</message>
<message>
<location filename="../../dialog.ui" line="599"/>
<location filename="../../dialog.ui" line="647"/>
<source>terminate</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="586"/>
<location filename="../../dialog.ui" line="634"/>
<source>execute</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="612"/>
<location filename="../../dialog.ui" line="660"/>
<source>clear</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="272"/>
<location filename="../../dialog.ui" line="313"/>
<source>reverse connection</source>
<translation></translation>
</message>
@ -134,17 +144,17 @@
<translation type="vanished"></translation>
</message>
<message>
<location filename="../../dialog.ui" line="256"/>
<location filename="../../dialog.ui" line="297"/>
<source>background record</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="220"/>
<location filename="../../dialog.ui" line="261"/>
<source>screen-off</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="189"/>
<location filename="../../dialog.ui" line="230"/>
<source>apply</source>
<translation></translation>
</message>
@ -154,37 +164,37 @@
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="240"/>
<location filename="../../dialog.ui" line="281"/>
<source>always on top</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="182"/>
<location filename="../../dialog.ui" line="223"/>
<source>refresh script</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="403"/>
<location filename="../../dialog.ui" line="451"/>
<source>get device IP</source>
<translation>IP</translation>
</message>
<message>
<location filename="../../dialog.ui" line="302"/>
<location filename="../../dialog.ui" line="350"/>
<source>USB line</source>
<translation>USB线</translation>
</message>
<message>
<location filename="../../dialog.ui" line="358"/>
<location filename="../../dialog.ui" line="406"/>
<source>stop server</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="348"/>
<location filename="../../dialog.ui" line="396"/>
<source>start server</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="338"/>
<location filename="../../dialog.ui" line="386"/>
<source>device serial:</source>
<translation></translation>
</message>
@ -198,20 +208,25 @@
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="413"/>
<location filename="../../dialog.ui" line="461"/>
<source>start adbd</source>
<translation>adbd</translation>
</message>
<message>
<location filename="../../dialog.ui" line="393"/>
<location filename="../../dialog.ui" line="441"/>
<source>refresh devices</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.cpp" line="101"/>
<location filename="../../dialog.cpp" line="105"/>
<source>original</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.cpp" line="112"/>
<source>no lock</source>
<translation></translation>
</message>
</context>
<context>
<name>QObject</name>
@ -226,7 +241,7 @@ You can download it at the following address:</source>
<translation type="vanished">.\n严禁用于非法用途.\n你可以在下面地址下载:</translation>
</message>
<message>
<location filename="../../main.cpp" line="103"/>
<location filename="../../main.cpp" line="108"/>
<source>This software is completely open source and free. Strictly used for illegal purposes, or at your own risk. You can download it at the following address:</source>
<translation>:</translation>
</message>
@ -322,7 +337,7 @@ You can download it at the following address:</source>
<translation type="vanished"></translation>
</message>
<message>
<location filename="../../device/ui/videoform.cpp" line="671"/>
<location filename="../../device/ui/videoform.cpp" line="710"/>
<source>file does not exist</source>
<translation></translation>
</message>

View file

@ -34,6 +34,9 @@
#define COMMON_ADB_PATH_KEY "AdbPath"
#define COMMON_ADB_PATH_DEF ""
#define COMMON_LOG_LEVEL_KEY "LogLevel"
#define COMMON_LOG_LEVEL_DEF "info"
// user data
#define COMMON_RECORD_KEY "RecordPath"
#define COMMON_RECORD_DEF ""
@ -74,7 +77,7 @@ Config &Config::getInstance()
static Config config;
return config;
}
#include <QDebug>
const QString &Config::getConfigPath()
{
if (s_configPath.isEmpty()) {
@ -265,6 +268,15 @@ QString Config::getAdbPath()
return adbPath;
}
QString Config::getLogLevel()
{
QString logLevel;
m_settings->beginGroup(GROUP_COMMON);
logLevel = m_settings->value(COMMON_LOG_LEVEL_KEY, COMMON_LOG_LEVEL_DEF).toString();
m_settings->endGroup();
return logLevel;
}
QString Config::getTitle()
{
QString title;

View file

@ -21,6 +21,7 @@ public:
QString getPushFilePath();
QString getServerPath();
QString getAdbPath();
QString getLogLevel();
// user data
QString getRecordPath();

View file

@ -7,6 +7,9 @@
#ifdef Q_OS_OSX
#include "cocoamousetap.h"
#endif
#ifdef Q_OS_LINUX
#include "xmousetap.h"
#endif
MouseTap *MouseTap::s_instance = Q_NULLPTR;
MouseTap *MouseTap::getInstance()
@ -19,7 +22,7 @@ MouseTap *MouseTap::getInstance()
s_instance = new CocoaMouseTap();
#endif
#ifdef Q_OS_LINUX
Q_ASSERT(false);
s_instance = new XMouseTap();
#endif
}
return s_instance;

View file

@ -16,3 +16,10 @@ mac {
LIBS += -framework Appkit
QMAKE_CFLAGS += -mmacosx-version-min=10.6
}
linux {
HEADERS += $$PWD/xmousetap.h
SOURCES += $$PWD/xmousetap.cpp
LIBS += -lxcb
QT += x11extras
}

View file

@ -0,0 +1,84 @@
#include <QX11Info>
#include <xcb/xproto.h>
#include <stdlib.h>
#include <stdint.h>
#include "xmousetap.h"
XMouseTap::XMouseTap() {}
XMouseTap::~XMouseTap() {}
void XMouseTap::initMouseEventTap() {}
void XMouseTap::quitMouseEventTap() {}
static void find_grab_window_recursive(xcb_connection_t *dpy, xcb_window_t window,
QRect rc, int16_t offset_x, int16_t offset_y,
xcb_window_t *grab_window, uint32_t *grab_window_size) {
xcb_query_tree_cookie_t tree_cookie;
xcb_query_tree_reply_t *tree;
tree_cookie = xcb_query_tree(dpy, window);
tree = xcb_query_tree_reply(dpy, tree_cookie, NULL);
xcb_window_t *children = xcb_query_tree_children(tree);
for (int i = 0; i < xcb_query_tree_children_length(tree); i++) {
xcb_get_geometry_cookie_t gg_cookie;
xcb_get_geometry_reply_t *gg;
gg_cookie = xcb_get_geometry(dpy, children[i]);
gg = xcb_get_geometry_reply(dpy, gg_cookie, NULL);
if (gg->x + offset_x <= rc.left() && gg->x + offset_x + gg->width >= rc.right() &&
gg->y + offset_y <= rc.top() && gg->y + offset_y + gg->height >= rc.bottom()) {
if (!*grab_window || gg->width * gg->height <= *grab_window_size) {
*grab_window = children[i];
*grab_window_size = gg->width * gg->height;
}
}
find_grab_window_recursive(dpy, children[i], rc,
gg->x + offset_x, gg->y + offset_y,
grab_window, grab_window_size);
free(gg);
}
free(tree);
}
void XMouseTap::enableMouseEventTap(QRect rc, bool enabled) {
if (enabled && rc.isEmpty()) {
return;
}
xcb_connection_t *dpy = QX11Info::connection();
if (enabled) {
// We grab the top-most smallest window
xcb_window_t grab_window = 0;
uint32_t grab_window_size = 0;
find_grab_window_recursive(dpy, QX11Info::appRootWindow(QX11Info::appScreen()),
rc, 0, 0, &grab_window, &grab_window_size);
if (grab_window) {
xcb_grab_pointer_cookie_t grab_cookie;
xcb_grab_pointer_reply_t *grab;
grab_cookie = xcb_grab_pointer(dpy, /* owner_events = */ 1,
grab_window, /* event_mask = */ 0,
XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC,
grab_window, XCB_NONE, XCB_CURRENT_TIME);
grab = xcb_grab_pointer_reply(dpy, grab_cookie, NULL);
free(grab);
}
} else {
xcb_void_cookie_t ungrab_cookie;
xcb_generic_error_t *error;
ungrab_cookie = xcb_ungrab_pointer_checked(dpy, XCB_CURRENT_TIME);
error = xcb_request_check(dpy, ungrab_cookie);
free(error);
}
}

View file

@ -0,0 +1,17 @@
#ifndef XMOUSETAP_H
#define XMOUSETAP_H
#include "mousetap.h"
class XMouseTap : public MouseTap
{
public:
XMouseTap();
virtual ~XMouseTap();
void initMouseEventTap() override;
void quitMouseEventTap() override;
void enableMouseEventTap(QRect rc, bool enabled) override;
};
#endif // XMOUSETAP_H

View file

@ -257,7 +257,7 @@ All the dependencies are provided and it is easy to compile.
## Licence
Since it is based on scrcpy, respect its Licence
Copyright (C) 2018 Genymobile
Copyright (C) 2020 Barry
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -255,7 +255,7 @@ Mac OS平台你可以直接使用我编译好的可执行程序:
## Licence
由于是复刻的scrcpy尊重它的Licence
Copyright (C) 2018 Genymobile
Copyright (C) 2020 Barry
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -10,8 +10,11 @@ RenderExpiredFrames=0
# 视频解码方式:-1 自动0 软解1 dx硬解2 opengl硬解
UseDesktopOpenGL=-1
# scrcpy-server的版本号不要修改
ServerVersion=1.12.1
ServerVersion=1.14
# scrcpy-server推送到安卓设备的路径
ServerPath=/data/local/tmp/scrcpy-server.jar
# 自定义adb路径例如D:/android/tools/adb.exe
AdbPath=
# Set the log level (debug, info, warn, error)
LogLevel=info

View file

@ -189,7 +189,7 @@ The client uses 4 threads:
recording,
- the **controller** thread, sending _control messages_ to the server,
- the **receiver** thread (managed by the controller), receiving _device
messages_ from the client.
messages_ from the server.
In addition, another thread can be started if necessary to handle APK
installation or file push requests (via drag&drop on the main window) or to
@ -214,7 +214,7 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
rendering frame (with proper synchronization). Thus, it immediatly starts
to decode a new frame while the main thread renders the last one.
If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw
H.264 packet to the output video file.
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
@ -282,6 +282,15 @@ meson x -Dserver_debugger=true
meson configure x -Dserver_debugger=true
```
If your device runs Android 8 or below, set the `server_debugger_method` to
`old` in addition:
```bash
meson x -Dserver_debugger=true -Dserver_debugger_method=old
# or, if x is already configured
meson configure x -Dserver_debugger=true -Dserver_debugger_method=old
```
Then recompile.
When you start scrcpy, it will start a debugger on port 5005 on the device.

View file

@ -3,6 +3,26 @@
如果在此文档没有解决你的问题描述你的问题截图软件控制台中打印的日志一起发到QQ群里提问。
# adb问题
## ADB版本之间的冲突
```
adb server version (41) doesn't match this client (39); killing...
```
当你的电脑中运行不同版本的adb时会发生此错误。你必须保证所有程序使用相同版本的adb。
现在你有两个办法解决这个问题:
1. 任务管理器找到adb进程并杀死
2. 配置QtScrcpy的config.ini中的AdbPath路径指向当前使用的adb
## 手机通过数据线连接电脑,刷新设备列表以后,没有任何设备出现
随便下载一个手机助手尝试连接成功以后再用QtScrcpy刷新设备列表连接
# 控制问题
## 可以看到画面,但无法控制
有些手机(小米等手机)需要额外打开控制权限检查是否USB调试里打开了允许模拟点击
![image](image/USB调试(安全设置).jpg)
# 其它
## 支持声音(软件不做支持)
[关于转发安卓声音到PC的讨论](https://github.com/Genymobile/scrcpy/issues/14#issuecomment-543204526)
@ -21,19 +41,11 @@ QtScrcpy.exe>属性>兼容性>更改高DPI设置>覆盖高DPI缩放行为>由以
## 无法输入中文
手机端安装搜狗输入法/QQ输入法就可以支持输入中文了
## 可以看到画面,但无法控制
有些手机(小米等手机)需要额外打开控制权限检查是否USB调试里打开了允许模拟点击
![image](image/USB调试(安全设置).jpg)
## 可以控制,但无法看到画面
控制台错误信息可能会包含 QOpenGLShaderProgram::attributeLocation(vertexIn): shader program is not linked
一般是由于显卡不支持当前的视频渲染方式config.ini里修改下解码方式改成1或者2试试
## 手机通过数据线连接电脑,刷新设备列表以后,没有任何设备出现
随便下载一个手机助手尝试连接成功以后再用QtScrcpy刷新设备列表连接
## 错误信息AdbProcess::error:adb server version (40) doesnt match this client (41)
任务管理找到adb进程并杀死重新操作即可

View file

@ -1,21 +1,22 @@
最后同步scrcpy 31bd95022bc525be42ca273d59a3211d964d278b
最后同步scrcpy 3c0fc8f54f42bf6e7eca35b352a7d343749b65c4
# TODO
## 低优先级
- [单独线程统计帧率](https://github.com/Genymobile/scrcpy/commit/e2a272bf99ecf48fcb050177113f903b3fb323c4)
- text转换 https://github.com/Genymobile/scrcpy/commit/c916af0984f72a60301d13fa8ef9a85112f54202?tdsourcetag=s_pctim_aiomsg
- 关闭number lock时的数字小键盘处理 https://github.com/Genymobile/scrcpy/commit/cd69eb4a4fecf8167208399def4ef536b59c9d22
- mipmapping https://github.com/Genymobile/scrcpy/commit/bea7658807d276aeab7d18d856a366c83ee05827
## 中优先级
- 脚本
- 某些机器软解不行
- opengles 3.0 兼容性参考[这里](https://github.com/libretro/glsl-shaders/blob/master/nnedi3/shaders/yuv-to-rgb-2x.glsl)
- 通过host:track-devices实现自动连接 https://www.jianshu.com/p/2cb86c6de76c
- 旋转 https://github.com/Genymobile/scrcpy/commit/d48b375a1dbc8bed92e3424b5967e59c2d8f6ca1
## 高优先级
- linux打包以及版本号
- 关于
- 旋转
- ubuntu自动打包
- 版本号抽离优化
- 音频转发 https://github.com/rom1v/sndcpy
# mark
## ffmpeg

View file

@ -43,7 +43,7 @@
- KMT_CLICK
- key 要映射的按键码
- pos 模拟触摸的位置
- switchMap 是否切换按键模式,点击此按键后,除了默认的模拟触摸映射,是否附带切换按键映射模式可以参考和平精英映射中M地图映射的效果
- switchMap 是否释放出鼠标,点击此按键后,除了默认的模拟触摸映射,是否释放出鼠标操作可以参考和平精英映射中M地图映射的效果
- KMT_CLICK_TWICE
- key 要映射的按键码

View file

@ -52,6 +52,16 @@
"y": 0.35
}
},
{
"comment": "自动跑",
"type": "KMT_CLICK",
"key": "Key_Equal",
"pos": {
"x": 0.84,
"y": 0.26
},
"switchMap": false
},
{
"comment": "跳",
"type": "KMT_CLICK",
@ -82,6 +92,16 @@
},
"switchMap": true
},
{
"comment": "视角",
"type": "KMT_CLICK",
"key": "Key_V",
"pos": {
"x": 0.23,
"y": 0.95
},
"switchMap": false
},
{
"comment": "趴",
"type": "KMT_CLICK",
@ -202,6 +222,16 @@
},
"switchMap": false
},
{
"comment": "手枪",
"type": "KMT_CLICK",
"key": "Key_7",
"pos": {
"x": 0.61,
"y": 0.82
},
"switchMap": false
},
{
"comment": "车加速",
"type": "KMT_CLICK",
@ -212,6 +242,46 @@
},
"switchMap": false
},
{
"comment": "投掷物菜单",
"type": "KMT_CLICK",
"key": "Key_F1",
"pos": {
"x": 0.69,
"y": 0.88
},
"switchMap": true
},
{
"comment": "药物菜单",
"type": "KMT_CLICK",
"key": "Key_F2",
"pos": {
"x": 0.31,
"y": 0.88
},
"switchMap": true
},
{
"comment": "消息菜单",
"type": "KMT_CLICK",
"key": "Key_F3",
"pos": {
"x": 0.98,
"y": 0.34
},
"switchMap": true
},
{
"comment": "表情菜单",
"type": "KMT_CLICK",
"key": "Key_F4",
"pos": {
"x": 0.81,
"y": 0.03
},
"switchMap": true
},
{
"comment": "开关门",
"type": "KMT_CLICK",
@ -253,4 +323,4 @@
"switchMap": false
}
]
}
}

View file

@ -0,0 +1,24 @@
/**
* Copyright (c) 2008, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content;
/**
* {@hide}
*/
oneway interface IOnPrimaryClipChangedListener {
void dispatchPrimaryClipChanged();
}

View file

@ -0,0 +1,77 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import java.io.File;
import java.io.IOException;
/**
* Handle the cleanup of scrcpy, even if the main process is killed.
* <p>
* This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process).
*/
public final class CleanUp {
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
private CleanUp() {
// not instantiable
}
public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException {
boolean needProcess = disableShowTouches || restoreStayOn != -1;
if (needProcess) {
startProcess(disableShowTouches, restoreStayOn);
} 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)};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH);
builder.start();
}
private static void unlinkSelf() {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Could not unlink server", e);
}
}
public static void main(String... args) {
unlinkSelf();
try {
// Wait for the server to die
System.in.read();
} catch (IOException e) {
// Expected when the server is dead
}
Ln.i("Cleaning up");
boolean disableShowTouches = Boolean.parseBoolean(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
if (disableShowTouches || 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");
}
if (restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
}
}
}
}
}

View file

@ -0,0 +1,112 @@
package com.genymobile.scrcpy;
import java.util.ArrayList;
import java.util.List;
public class CodecOption {
private String key;
private Object value;
public CodecOption(String key, Object value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public Object getValue() {
return value;
}
public static List<CodecOption> parse(String codecOptions) {
if ("-".equals(codecOptions)) {
return null;
}
List<CodecOption> result = new ArrayList<>();
boolean escape = false;
StringBuilder buf = new StringBuilder();
for (char c : codecOptions.toCharArray()) {
switch (c) {
case '\\':
if (escape) {
buf.append('\\');
escape = false;
} else {
escape = true;
}
break;
case ',':
if (escape) {
buf.append(',');
escape = false;
} else {
// This comma is a separator between codec options
String codecOption = buf.toString();
result.add(parseOption(codecOption));
// Clear buf
buf.setLength(0);
}
break;
default:
buf.append(c);
break;
}
}
if (buf.length() > 0) {
String codecOption = buf.toString();
result.add(parseOption(codecOption));
}
return result;
}
private static CodecOption parseOption(String option) {
int equalSignIndex = option.indexOf('=');
if (equalSignIndex == -1) {
throw new IllegalArgumentException("'=' expected");
}
String keyAndType = option.substring(0, equalSignIndex);
if (keyAndType.length() == 0) {
throw new IllegalArgumentException("Key may not be null");
}
String key;
String type;
int colonIndex = keyAndType.indexOf(':');
if (colonIndex != -1) {
key = keyAndType.substring(0, colonIndex);
type = keyAndType.substring(colonIndex + 1);
} else {
key = keyAndType;
type = "int"; // assume int by default
}
Object value;
String valueString = option.substring(equalSignIndex + 1);
switch (type) {
case "int":
value = Integer.parseInt(valueString);
break;
case "long":
value = Long.parseLong(valueString);
break;
case "float":
value = Float.parseFloat(valueString);
break;
case "string":
value = valueString;
break;
default:
throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type);
}
return new CodecOption(key, value);
}
}

View file

@ -17,6 +17,8 @@ public final class ControlMessage {
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
public static final int TYPE_ROTATE_DEVICE = 10;
public static final int FLAGS_PASTE = 1;
private int type;
private String text;
private int metaState; // KeyEvent.META_*
@ -28,6 +30,7 @@ public final class ControlMessage {
private Position position;
private int hScroll;
private int vScroll;
private int flags;
private ControlMessage() {
}
@ -68,10 +71,13 @@ public final class ControlMessage {
return msg;
}
public static ControlMessage createSetClipboard(String text) {
public static ControlMessage createSetClipboard(String text, boolean paste) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD;
msg.text = text;
if (paste) {
msg.flags = FLAGS_PASTE;
}
return msg;
}
@ -134,4 +140,8 @@ public final class ControlMessage {
public int getVScroll() {
return vScroll;
}
public int getFlags() {
return flags;
}
}

View file

@ -8,15 +8,16 @@ import java.nio.charset.StandardCharsets;
public class ControlMessageReader {
private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17;
private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21;
private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
public static final int TEXT_MAX_LENGTH = 300;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
private static final int RAW_BUFFER_SIZE = 1024;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length)
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 ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
@ -122,7 +123,6 @@ public class ControlMessageReader {
return ControlMessage.createInjectText(text);
}
@SuppressWarnings("checkstyle:MagicNumber")
private ControlMessage parseInjectTouchEvent() {
if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) {
return null;
@ -149,11 +149,15 @@ public class ControlMessageReader {
}
private ControlMessage parseSetClipboard() {
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
return null;
}
boolean parse = buffer.get() != 0;
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createSetClipboard(text);
return ControlMessage.createSetClipboard(text, parse);
}
private ControlMessage parseSetScreenPowerMode() {
@ -172,12 +176,10 @@ public class ControlMessageReader {
return new Position(x, y, screenWidth, screenHeight);
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int toUnsigned(short value) {
return value & 0xffff;
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int toUnsigned(byte value) {
return value & 0xff;
}

View file

@ -1,10 +1,8 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import android.os.Build;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
@ -47,11 +45,10 @@ public class Controller {
}
}
@SuppressWarnings("checkstyle:MagicNumber")
public void control() throws IOException {
// on start, power on the device
if (!device.isScreenOn()) {
injectKeycode(KeyEvent.KEYCODE_POWER);
device.injectKeycode(KeyEvent.KEYCODE_POWER);
// dirty hack
// After POWER is injected, the device is powered on asynchronously.
@ -76,19 +73,29 @@ public class Controller {
ControlMessage msg = connection.receiveControlMessage();
switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE:
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
if (device.supportsInputEvents()) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
}
break;
case ControlMessage.TYPE_INJECT_TEXT:
injectText(msg.getText());
if (device.supportsInputEvents()) {
injectText(msg.getText());
}
break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
if (device.supportsInputEvents()) {
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
}
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
if (device.supportsInputEvents()) {
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
}
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
pressBackOrTurnScreenOn();
if (device.supportsInputEvents()) {
pressBackOrTurnScreenOn();
}
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel();
@ -98,13 +105,22 @@ public class Controller {
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
String clipboardText = device.getClipboardText();
sender.pushClipboardText(clipboardText);
if (clipboardText != null) {
sender.pushClipboardText(clipboardText);
}
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
device.setClipboardText(msg.getText());
boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
setClipboard(msg.getText(), paste);
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
device.setScreenPowerMode(msg.getAction());
if (device.supportsInputEvents()) {
int mode = msg.getAction();
boolean setPowerModeOk = device.setScreenPowerMode(mode);
if (setPowerModeOk) {
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
}
}
break;
case ControlMessage.TYPE_ROTATE_DEVICE:
device.rotateDevice();
@ -115,7 +131,7 @@ public class Controller {
}
private boolean injectKeycode(int action, int keycode, int metaState) {
return injectKeyEvent(action, keycode, 0, metaState);
return device.injectKeyEvent(action, keycode, 0, metaState);
}
private boolean injectChar(char c) {
@ -126,7 +142,7 @@ public class Controller {
return false;
}
for (KeyEvent event : events) {
if (!injectEvent(event)) {
if (!device.injectEvent(event)) {
return false;
}
}
@ -182,7 +198,7 @@ public class Controller {
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
return injectEvent(event);
return device.injectEvent(event);
}
private boolean injectScroll(Position position, int hScroll, int vScroll) {
@ -204,27 +220,26 @@ public class Controller {
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_MOUSE, 0);
return injectEvent(event);
}
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
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);
}
private boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
}
private boolean injectEvent(InputEvent event) {
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
InputDevice.SOURCE_TOUCHSCREEN, 0);
return device.injectEvent(event);
}
private boolean pressBackOrTurnScreenOn() {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
return injectKeycode(keycode);
return device.injectKeycode(keycode);
}
private boolean setClipboard(String text, boolean paste) {
boolean ok = device.setClipboardText(text);
if (ok) {
Ln.i("Device clipboard set");
}
// On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
device.injectKeycode(KeyEvent.KEYCODE_PASTE);
}
return ok;
}
}

View file

@ -84,7 +84,6 @@ public final class DesktopConnection implements Closeable {
controlSocket.close();
}
@SuppressWarnings("checkstyle:MagicNumber")
private void send(String deviceName, int width, int height) throws IOException {
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];

View file

@ -1,15 +1,23 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager;
import android.content.IOnPrimaryClipChangedListener;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.view.IRotationWatcher;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import java.util.concurrent.atomic.AtomicBoolean;
public final class Device {
@ -20,18 +28,47 @@ public final class Device {
void onRotationChanged(int rotation);
}
public interface ClipboardListener {
void onClipboardTextChanged(String text);
}
private final ServiceManager serviceManager = new ServiceManager();
private ScreenInfo screenInfo;
private RotationListener rotationListener;
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
/**
* Logical display identifier
*/
private final int displayId;
/**
* The surface flinger layer stack associated with this logical display
*/
private final int layerStack;
private final boolean supportsInputEvents;
public Device(Options options) {
screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize());
registerRotationWatcher(new IRotationWatcher.Stub() {
displayId = options.getDisplayId();
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
int[] displayIds = serviceManager.getDisplayManager().getDisplayIds();
throw new InvalidDisplayIdException(displayId, displayIds);
}
int displayInfoFlags = displayInfo.getFlags();
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
layerStack = displayInfo.getLayerStack();
serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) throws RemoteException {
public void onRotationChanged(int rotation) {
synchronized (Device.this) {
screenInfo = screenInfo.withRotation(rotation);
screenInfo = screenInfo.withDeviceRotation(rotation);
// notify
if (rotationListener != null) {
@ -39,104 +76,120 @@ 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 ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
}
// main display or any display on Android >= Q
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
if (!supportsInputEvents) {
Ln.w("Input events are not supported for secondary displays before Android 10");
}
}
public synchronized ScreenInfo getScreenInfo() {
return screenInfo;
}
private ScreenInfo computeScreenInfo(Rect crop, int maxSize) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize();
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
if (crop != null) {
if (rotated) {
// the crop (provided by the user) is expressed in the natural orientation
crop = flipRect(crop);
}
if (!contentRect.intersect(crop)) {
// intersect() changes contentRect so that it is intersected with crop
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
contentRect = new Rect(); // empty
}
}
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
return new ScreenInfo(contentRect, videoSize, rotated);
}
private static String formatCrop(Rect rect) {
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
}
@SuppressWarnings("checkstyle:MagicNumber")
private static Size computeVideoSize(int w, int h, int maxSize) {
// Compute the video size and the padding of the content inside this video.
// Principle:
// - scale down the great side of the screen to maxSize (if necessary);
// - scale down the other side so that the aspect ratio is preserved;
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
w &= ~7; // in case it's not a multiple of 8
h &= ~7;
if (maxSize > 0) {
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
throw new AssertionError("Max size must be a multiple of 8");
}
boolean portrait = h > w;
int major = portrait ? h : w;
int minor = portrait ? w : h;
if (major > maxSize) {
int minorExact = minor * maxSize / major;
// +4 to round the value to the nearest multiple of 8
minor = (minorExact + 4) & ~7;
major = maxSize;
}
w = portrait ? minor : major;
h = portrait ? major : minor;
}
return new Size(w, h);
public int getLayerStack() {
return layerStack;
}
public Point getPhysicalPoint(Position position) {
// it hides the field on purpose, to read it with a lock
@SuppressWarnings("checkstyle:HiddenField")
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
Size videoSize = screenInfo.getVideoSize();
Size clientVideoSize = position.getScreenSize();
if (!videoSize.equals(clientVideoSize)) {
// ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation
Size unlockedVideoSize = screenInfo.getUnlockedVideoSize();
int reverseVideoRotation = screenInfo.getReverseVideoRotation();
// reverse the video rotation to apply the events
Position devicePosition = position.rotate(reverseVideoRotation);
Size clientVideoSize = devicePosition.getScreenSize();
if (!unlockedVideoSize.equals(clientVideoSize)) {
// The client sends a click relative to a video with wrong dimensions,
// the device may have been rotated since the event was generated, so ignore the event
return null;
}
Rect contentRect = screenInfo.getContentRect();
Point point = position.getPoint();
int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
return new Point(scaledX, scaledY);
Point point = devicePosition.getPoint();
int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth();
int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight();
return new Point(convertedX, convertedY);
}
public static String getDeviceName() {
return Build.MODEL;
}
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
public boolean supportsInputEvents() {
return supportsInputEvents;
}
public boolean injectEvent(InputEvent inputEvent, int mode) {
if (!supportsInputEvents()) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
}
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) {
return false;
}
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
}
public boolean injectEvent(InputEvent event) {
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
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);
}
public boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
}
public boolean isScreenOn() {
return serviceManager.getPowerManager().isScreenOn();
}
public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher);
}
public synchronized void setRotationListener(RotationListener rotationListener) {
this.rotationListener = rotationListener;
}
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
}
public void expandNotificationPanel() {
serviceManager.getStatusBarManager().expandNotificationsPanel();
}
@ -153,22 +206,23 @@ public final class Device {
return s.toString();
}
public void setClipboardText(String text) {
serviceManager.getClipboardManager().setText(text);
Ln.i("Device clipboard set");
public boolean setClipboardText(String text) {
isSettingClipboard.set(true);
boolean ok = serviceManager.getClipboardManager().setText(text);
isSettingClipboard.set(false);
return ok;
}
/**
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
*/
public void setScreenPowerMode(int mode) {
public boolean setScreenPowerMode(int mode) {
IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) {
Ln.e("Could not get built-in display");
return;
return false;
}
SurfaceControl.setDisplayPowerMode(d, mode);
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
return SurfaceControl.setDisplayPowerMode(d, mode);
}
/**
@ -192,7 +246,7 @@ public final class Device {
}
}
static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
public ContentProvider createSettingsProvider() {
return serviceManager.getActivityManager().createSettingsProvider();
}
}

View file

@ -13,7 +13,6 @@ public class DeviceMessageWriter {
private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
@SuppressWarnings("checkstyle:MagicNumber")
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
buffer.clear();
buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD);

View file

@ -1,12 +1,24 @@
package com.genymobile.scrcpy;
public final class DisplayInfo {
private final int displayId;
private final Size size;
private final int rotation;
private final int layerStack;
private final int flags;
public DisplayInfo(Size size, int rotation) {
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) {
this.displayId = displayId;
this.size = size;
this.rotation = rotation;
this.layerStack = layerStack;
this.flags = flags;
}
public int getDisplayId() {
return displayId;
}
public Size getSize() {
@ -16,5 +28,13 @@ public final class DisplayInfo {
public int getRotation() {
return rotation;
}
public int getLayerStack() {
return layerStack;
}
public int getFlags() {
return flags;
}
}

View file

@ -0,0 +1,21 @@
package com.genymobile.scrcpy;
public class InvalidDisplayIdException extends RuntimeException {
private final int displayId;
private final int[] availableDisplayIds;
public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) {
super("There is no display having id " + displayId);
this.displayId = displayId;
this.availableDisplayIds = availableDisplayIds;
}
public int getDisplayId() {
return displayId;
}
public int[] getAvailableDisplayIds() {
return availableDisplayIds;
}
}

View file

@ -15,14 +15,25 @@ public final class Ln {
DEBUG, INFO, WARN, ERROR
}
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
private static Level threshold = Level.INFO;
private Ln() {
// not instantiable
}
/**
* Initialize the log level.
* <p>
* Must be called before starting any new thread.
*
* @param level the log level
*/
public static void initLogLevel(Level level) {
threshold = level;
}
public static boolean isEnabled(Level level) {
return level.ordinal() >= THRESHOLD.ordinal();
return level.ordinal() >= threshold.ordinal();
}
public static void d(String message) {

View file

@ -3,13 +3,27 @@ package com.genymobile.scrcpy;
import android.graphics.Rect;
public class Options {
private Ln.Level logLevel;
private int maxSize;
private int bitRate;
private int maxFps;
private int lockedVideoOrientation;
private boolean tunnelForward;
private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly
private boolean control;
private int displayId;
private boolean showTouches;
private boolean stayAwake;
private String codecOptions;
public Ln.Level getLogLevel() {
return logLevel;
}
public void setLogLevel(Ln.Level logLevel) {
this.logLevel = logLevel;
}
public int getMaxSize() {
return maxSize;
@ -35,6 +49,14 @@ public class Options {
this.maxFps = maxFps;
}
public int getLockedVideoOrientation() {
return lockedVideoOrientation;
}
public void setLockedVideoOrientation(int lockedVideoOrientation) {
this.lockedVideoOrientation = lockedVideoOrientation;
}
public boolean isTunnelForward() {
return tunnelForward;
}
@ -66,4 +88,36 @@ public class Options {
public void setControl(boolean control) {
this.control = control;
}
public int getDisplayId() {
return displayId;
}
public void setDisplayId(int displayId) {
this.displayId = displayId;
}
public boolean getShowTouches() {
return showTouches;
}
public void setShowTouches(boolean showTouches) {
this.showTouches = showTouches;
}
public boolean getStayAwake() {
return stayAwake;
}
public void setStayAwake(boolean stayAwake) {
this.stayAwake = stayAwake;
}
public String getCodecOptions() {
return codecOptions;
}
public void setCodecOptions(String codecOptions) {
this.codecOptions = codecOptions;
}
}

View file

@ -23,6 +23,19 @@ public class Position {
return screenSize;
}
public Position rotate(int rotation) {
switch (rotation) {
case 1:
return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate());
case 2:
return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize);
case 3:
return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate());
default:
return this;
}
}
@Override
public boolean equals(Object o) {
if (this == o) {

View file

@ -6,40 +6,37 @@ import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
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.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener {
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
private static final int NO_PTS = -1;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private List<CodecOption> codecOptions;
private int bitRate;
private int maxFps;
private int iFrameInterval;
private boolean sendFrameMeta;
private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) {
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions) {
this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate;
this.maxFps = maxFps;
this.iFrameInterval = iFrameInterval;
}
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
this.codecOptions = codecOptions;
}
@Override
@ -53,21 +50,40 @@ public class ScreenEncoder implements Device.RotationListener {
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
Workarounds.prepareMainLooper();
Workarounds.fillAppInfo();
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
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");
Workarounds.fillAppInfo();
internalStreamScreen(device, fd);
}
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
device.setRotationListener(this);
boolean alive;
try {
do {
MediaCodec codec = createCodec();
IBinder display = createDisplay();
Rect contentRect = device.getScreenInfo().getContentRect();
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
// include the locked video orientation
Rect videoRect = screenInfo.getVideoSize().toRect();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setSize(format, videoRect.width(), videoRect.height());
configure(codec, format);
Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, contentRect, videoRect);
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start();
try {
alive = encode(codec, fd);
@ -135,27 +151,49 @@ public class ScreenEncoder implements Device.RotationListener {
}
private static MediaCodec createCodec() throws IOException {
return MediaCodec.createEncoderByType("video/avc");
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
}
@SuppressWarnings("checkstyle:MagicNumber")
private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) {
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
String key = codecOption.getKey();
Object value = codecOption.getValue();
if (value instanceof Integer) {
format.setInteger(key, (Integer) value);
} else if (value instanceof Long) {
format.setLong(key, (Long) value);
} else if (value instanceof Float) {
format.setFloat(key, (Float) value);
} else if (value instanceof String) {
format.setString(key, (String) value);
}
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
}
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "video/avc");
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL);
// display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
if (maxFps > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps);
} else {
Ln.w("Max FPS is only supported since Android 10, the option has been ignored");
// The key existed privately before Android 10:
// <https://android.googlesource.com/platform/frameworks/base/+/625f0aad9f7a259b6881006ad8710adce57d1384%5E%21/>
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
}
if (codecOptions != null) {
for (CodecOption option : codecOptions) {
setCodecOption(format, option);
}
}
return format;
}
@ -172,12 +210,12 @@ public class ScreenEncoder implements Device.RotationListener {
format.setInteger(MediaFormat.KEY_HEIGHT, height);
}
private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
SurfaceControl.openTransaction();
try {
SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, 0);
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, layerStack);
} finally {
SurfaceControl.closeTransaction();
}

View file

@ -3,29 +3,161 @@ package com.genymobile.scrcpy;
import android.graphics.Rect;
public final class ScreenInfo {
/**
* Device (physical) size, possibly cropped
*/
private final Rect contentRect; // device size, possibly cropped
private final Size videoSize;
private final boolean rotated;
public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) {
/**
* Video size, possibly smaller than the device size, already taking the device rotation and crop into account.
* <p>
* However, it does not include the locked video orientation.
*/
private final Size unlockedVideoSize;
/**
* Device rotation, related to the natural device orientation (0, 1, 2 or 3)
*/
private final int deviceRotation;
/**
* The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW)
*/
private final int lockedVideoOrientation;
public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) {
this.contentRect = contentRect;
this.videoSize = videoSize;
this.rotated = rotated;
this.unlockedVideoSize = unlockedVideoSize;
this.deviceRotation = deviceRotation;
this.lockedVideoOrientation = lockedVideoOrientation;
}
public Rect getContentRect() {
return contentRect;
}
public Size getVideoSize() {
return videoSize;
/**
* Return the video size as if locked video orientation was not set.
*
* @return the unlocked video size
*/
public Size getUnlockedVideoSize() {
return unlockedVideoSize;
}
public ScreenInfo withRotation(int rotation) {
boolean newRotated = (rotation & 1) != 0;
if (rotated == newRotated) {
/**
* Return the actual video size if locked video orientation is set.
*
* @return the actual video size
*/
public Size getVideoSize() {
if (getVideoRotation() % 2 == 0) {
return unlockedVideoSize;
}
return unlockedVideoSize.rotate();
}
public int getDeviceRotation() {
return deviceRotation;
}
public ScreenInfo withDeviceRotation(int newDeviceRotation) {
if (newDeviceRotation == deviceRotation) {
return this;
}
return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated);
// true if changed between portrait and landscape
boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0;
Rect newContentRect;
Size newUnlockedVideoSize;
if (orientationChanged) {
newContentRect = flipRect(contentRect);
newUnlockedVideoSize = unlockedVideoSize.rotate();
} else {
newContentRect = contentRect;
newUnlockedVideoSize = unlockedVideoSize;
}
return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation);
}
public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
int rotation = displayInfo.getRotation();
Size deviceSize = displayInfo.getSize();
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
if (crop != null) {
if (rotation % 2 != 0) { // 180s preserve dimensions
// the crop (provided by the user) is expressed in the natural orientation
crop = flipRect(crop);
}
if (!contentRect.intersect(crop)) {
// intersect() changes contentRect so that it is intersected with crop
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
contentRect = new Rect(); // empty
}
}
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation);
}
private static String formatCrop(Rect rect) {
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
}
private static Size computeVideoSize(int w, int h, int maxSize) {
// Compute the video size and the padding of the content inside this video.
// Principle:
// - scale down the great side of the screen to maxSize (if necessary);
// - scale down the other side so that the aspect ratio is preserved;
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
w &= ~7; // in case it's not a multiple of 8
h &= ~7;
if (maxSize > 0) {
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
throw new AssertionError("Max size must be a multiple of 8");
}
boolean portrait = h > w;
int major = portrait ? h : w;
int minor = portrait ? w : h;
if (major > maxSize) {
int minorExact = minor * maxSize / major;
// +4 to round the value to the nearest multiple of 8
minor = (minorExact + 4) & ~7;
major = maxSize;
}
w = portrait ? minor : major;
h = portrait ? major : minor;
}
return new Size(w, h);
}
private static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
}
/**
* Return the rotation to apply to the device rotation to get the requested locked video orientation
*
* @return the rotation offset
*/
public int getVideoRotation() {
if (lockedVideoOrientation == -1) {
// no offset
return 0;
}
return (deviceRotation + 4 - lockedVideoOrientation) % 4;
}
/**
* Return the rotation to apply to the requested locked video orientation to get the device rotation
*
* @return the (reverse) rotation offset
*/
public int getReverseVideoRotation() {
if (lockedVideoOrientation == -1) {
// no offset
return 0;
}
return (lockedVideoOrientation + 4 - deviceRotation) % 4;
}
}

View file

@ -1,32 +1,74 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.os.BatteryManager;
import android.os.Build;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
public final class Server {
private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
private 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());
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");
// If "show touches" was disabled, it must be disabled back on clean up
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
}
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));
try {
restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn == stayOn) {
// No need to restore
restoreStayOn = -1;
}
} catch (NumberFormatException e) {
restoreStayOn = 0;
}
}
}
}
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions);
if (options.getControl()) {
Controller controller = new Controller(device, connection);
final Controller controller = new Controller(device, connection);
// asynchronous
startController(controller);
startDeviceMessageSender(controller.getSender());
device.setClipboardListener(new Device.ClipboardListener() {
@Override
public void onClipboardTextChanged(String text) {
controller.getSender().pushClipboardText(text);
}
});
}
try {
@ -67,7 +109,6 @@ public final class Server {
}).start();
}
@SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) {
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");
@ -76,41 +117,59 @@ public final class Server {
String clientVersion = args[0];
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
throw new IllegalArgumentException(
"The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")");
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
if (args.length != 8) {
throw new IllegalArgumentException("Expecting 8 parameters");
final int expectedParameters = 14;
if (args.length != expectedParameters) {
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
}
Options options = new Options();
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
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[2]);
int bitRate = Integer.parseInt(args[3]);
options.setBitRate(bitRate);
int maxFps = Integer.parseInt(args[3]);
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[4]);
boolean tunnelForward = Boolean.parseBoolean(args[6]);
options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[5]);
Rect crop = parseCrop(args[7]);
options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[6]);
boolean sendFrameMeta = Boolean.parseBoolean(args[8]);
options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[7]);
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);
return options;
}
@SuppressWarnings("checkstyle:MagicNumber")
private static Rect parseCrop(String crop) {
if ("-".equals(crop)) {
return null;
@ -127,15 +186,6 @@ public final class Server {
return new Rect(x, y, x + width, y + height);
}
private static void unlinkSelf() {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Could not unlink server", e);
}
}
@SuppressWarnings("checkstyle:MagicNumber")
private static void suggestFix(Throwable e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e instanceof MediaCodec.CodecException) {
@ -147,6 +197,16 @@ public final class Server {
}
}
}
if (e instanceof InvalidDisplayIdException) {
InvalidDisplayIdException idie = (InvalidDisplayIdException) e;
int[] displayIds = idie.getAvailableDisplayIds();
if (displayIds != null && displayIds.length > 0) {
Ln.e("Try to use one of the available display ids:");
for (int id : displayIds) {
Ln.e(" scrcpy --display " + id);
}
}
}
}
public static void main(String... args) throws Exception {
@ -158,8 +218,10 @@ public final class Server {
}
});
unlinkSelf();
Options options = createOptions(args);
Ln.initLogLevel(options.getLogLevel());
scrcpy(options);
}
}

View file

@ -5,7 +5,6 @@ public final class StringUtils {
// not instantiable
}
@SuppressWarnings("checkstyle:MagicNumber")
public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) {
int len = utf8.length;
if (len <= maxLength) {

View file

@ -28,7 +28,7 @@ public final class Workarounds {
Looper.prepareMainLooper();
}
@SuppressLint("PrivateApi")
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public static void fillAppInfo() {
try {
// ActivityThread activityThread = new ActivityThread();
@ -73,7 +73,7 @@ public final class Workarounds {
mInitialApplicationField.set(activityThread, app);
} catch (Throwable throwable) {
// this is a workaround, so failing is not an error
Ln.w("Could not fill app info: " + throwable.getMessage());
Ln.d("Could not fill app info: " + throwable.getMessage());
}
}
}

View file

@ -0,0 +1,87 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ActivityManager {
private final IInterface manager;
private Method getContentProviderExternalMethod;
private boolean getContentProviderExternalMethodLegacy;
private Method removeContentProviderExternalMethod;
public ActivityManager(IInterface manager) {
this.manager = manager;
}
private Method getGetContentProviderExternalMethod() throws NoSuchMethodException {
if (getContentProviderExternalMethod == null) {
try {
getContentProviderExternalMethod = manager.getClass()
.getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class);
} catch (NoSuchMethodException e) {
// old version
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
getContentProviderExternalMethodLegacy = true;
}
}
return getContentProviderExternalMethod;
}
private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException {
if (removeContentProviderExternalMethod == null) {
removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class);
}
return removeContentProviderExternalMethod;
}
private ContentProvider getContentProviderExternal(String name, IBinder token) {
try {
Method method = getGetContentProviderExternalMethod();
Object[] args;
if (!getContentProviderExternalMethodLegacy) {
// new version
args = new Object[]{name, ServiceManager.USER_ID, token, null};
} else {
// old version
args = new Object[]{name, ServiceManager.USER_ID, token};
}
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
Object providerHolder = method.invoke(manager, args);
if (providerHolder == null) {
return null;
}
// IContentProvider provider = providerHolder.provider;
Field providerField = providerHolder.getClass().getDeclaredField("provider");
providerField.setAccessible(true);
Object provider = providerField.get(providerHolder);
if (provider == null) {
return null;
}
return new ContentProvider(this, provider, name, token);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
void removeContentProviderExternal(String name, IBinder token) {
try {
Method method = getRemoveContentProviderExternalMethod();
method.invoke(manager, name, token);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public ContentProvider createSettingsProvider() {
return getContentProviderExternal("settings", new Binder());
}
}

View file

@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.content.ClipData;
import android.content.IOnPrimaryClipChangedListener;
import android.os.Build;
import android.os.IInterface;
@ -10,13 +11,10 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClipboardManager {
private static final String PACKAGE_NAME = "com.android.shell";
private static final int USER_ID = 0;
private final IInterface manager;
private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener;
public ClipboardManager(IInterface manager) {
this.manager = manager;
@ -46,17 +44,17 @@ public class ClipboardManager {
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, PACKAGE_NAME);
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
}
return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID);
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, PACKAGE_NAME);
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
} else {
method.invoke(manager, clipData, PACKAGE_NAME, USER_ID);
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
}
@ -74,13 +72,48 @@ public class ClipboardManager {
}
}
public void setText(CharSequence text) {
public boolean setText(CharSequence text) {
try {
Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text);
setPrimaryClip(method, manager, clipData);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
} else {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
}
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
if (addPrimaryClipChangedListener == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
} else {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
}
}
return addPrimaryClipChangedListener;
}
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
try {
Method method = getAddPrimaryClipChangedListener();
addPrimaryClipChangedListener(method, manager, listener);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
}

View file

@ -0,0 +1,132 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.Bundle;
import android.os.IBinder;
import java.io.Closeable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ContentProvider implements Closeable {
public static final String TABLE_SYSTEM = "system";
public static final String TABLE_SECURE = "secure";
public static final String TABLE_GLOBAL = "global";
// See android/providerHolder/Settings.java
private static final String CALL_METHOD_GET_SYSTEM = "GET_system";
private static final String CALL_METHOD_GET_SECURE = "GET_secure";
private static final String CALL_METHOD_GET_GLOBAL = "GET_global";
private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system";
private static final String CALL_METHOD_PUT_SECURE = "PUT_secure";
private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global";
private static final String CALL_METHOD_USER_KEY = "_user";
private static final String NAME_VALUE_TABLE_VALUE = "value";
private final ActivityManager manager;
// android.content.IContentProvider
private final Object provider;
private final String name;
private final IBinder token;
private Method callMethod;
private boolean callMethodLegacy;
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
this.manager = manager;
this.provider = provider;
this.name = name;
this.token = token;
}
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;
}
}
return callMethod;
}
private Bundle call(String callMethod, String arg, Bundle extras) {
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};
}
return (Bundle) method.invoke(provider, args);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
public void close() {
manager.removeContentProviderExternal(name, token);
}
private static String getGetMethod(String table) {
switch (table) {
case TABLE_SECURE:
return CALL_METHOD_GET_SECURE;
case TABLE_SYSTEM:
return CALL_METHOD_GET_SYSTEM;
case TABLE_GLOBAL:
return CALL_METHOD_GET_GLOBAL;
default:
throw new IllegalArgumentException("Invalid table: " + table);
}
}
private static String getPutMethod(String table) {
switch (table) {
case TABLE_SECURE:
return CALL_METHOD_PUT_SECURE;
case TABLE_SYSTEM:
return CALL_METHOD_PUT_SYSTEM;
case TABLE_GLOBAL:
return CALL_METHOD_PUT_GLOBAL;
default:
throw new IllegalArgumentException("Invalid table: " + table);
}
}
public String getValue(String table, String key) {
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;
}
return bundle.getString("value");
}
public void putValue(String table, String key, String value) {
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);
}
return oldValue;
}
}

View file

@ -12,15 +12,28 @@ public final class DisplayManager {
this.manager = manager;
}
public DisplayInfo getDisplayInfo() {
public DisplayInfo getDisplayInfo(int displayId) {
try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0);
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
if (displayInfo == null) {
return null;
}
Class<?> cls = displayInfo.getClass();
// width and height already take the rotation into account
int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
return new DisplayInfo(new Size(width, height), rotation);
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public int[] getDisplayIds() {
try {
return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager);
} catch (Exception e) {
throw new AssertionError(e);
}

View file

@ -17,6 +17,8 @@ public final class InputManager {
private final IInterface manager;
private Method injectInputEventMethod;
private static Method setDisplayIdMethod;
public InputManager(IInterface manager) {
this.manager = manager;
}
@ -37,4 +39,22 @@ public final class InputManager {
return false;
}
}
private static Method getSetDisplayIdMethod() throws NoSuchMethodException {
if (setDisplayIdMethod == null) {
setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class);
}
return setDisplayIdMethod;
}
public static boolean setDisplayId(InputEvent inputEvent, int displayId) {
try {
Method method = getSetDisplayIdMethod();
method.invoke(inputEvent, displayId);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Cannot associate a display id to the input event", e);
return false;
}
}
}

View file

@ -6,8 +6,12 @@ import android.os.IInterface;
import java.lang.reflect.Method;
@SuppressLint("PrivateApi")
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class ServiceManager {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int USER_ID = 0;
private final Method getServiceMethod;
private WindowManager windowManager;
@ -16,6 +20,7 @@ public final class ServiceManager {
private PowerManager powerManager;
private StatusBarManager statusBarManager;
private ClipboardManager clipboardManager;
private ActivityManager activityManager;
public ServiceManager() {
try {
@ -76,4 +81,21 @@ public final class ServiceManager {
}
return clipboardManager;
}
public ActivityManager getActivityManager() {
if (activityManager == null) {
try {
// On old Android versions, the ActivityManager is not exposed via AIDL,
// so use ActivityManagerNative.getDefault()
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
IInterface am = (IInterface) getDefaultMethod.invoke(null);
activityManager = new ActivityManager(am);
} catch (Exception e) {
throw new AssertionError(e);
}
}
return activityManager;
}
}

View file

@ -121,12 +121,14 @@ public final class SurfaceControl {
return setDisplayPowerModeMethod;
}
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
public static boolean setDisplayPowerMode(IBinder displayToken, int mode) {
try {
Method method = getSetDisplayPowerModeMethod();
method.invoke(null, displayToken, mode);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}

View file

@ -93,13 +93,13 @@ public final class WindowManager {
}
}
public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
try {
Class<?> cls = manager.getClass();
try {
// display parameter added since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId);
} catch (NoSuchMethodException e) {
// old version
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);

Binary file not shown.