mirror of
https://github.com/barry-ran/QtScrcpy.git
synced 2025-04-22 20:44:59 +00:00
commit
2ca4bef199
60 changed files with 1726 additions and 355 deletions
|
@ -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 \
|
||||
|
|
|
@ -135,7 +135,7 @@ void Controller::onSetDeviceClipboard()
|
|||
if (!controlMsg) {
|
||||
return;
|
||||
}
|
||||
controlMsg->setSetClipboardMsgData(text);
|
||||
controlMsg->setSetClipboardMsgData(text, true);
|
||||
postControlMsg(controlMsg);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 forward;false:直接使用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 forward;false:直接使用adb forward
|
||||
int lockVideoOrientation = -1; // 是否锁定视频方向
|
||||
int stayAwake = false; // 是否保持唤醒
|
||||
};
|
||||
|
||||
explicit Server(QObject *parent = nullptr);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
@ -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.
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -21,6 +21,7 @@ public:
|
|||
QString getPushFilePath();
|
||||
QString getServerPath();
|
||||
QString getAdbPath();
|
||||
QString getLogLevel();
|
||||
|
||||
// user data
|
||||
QString getRecordPath();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
84
QtScrcpy/util/mousetap/xmousetap.cpp
Normal file
84
QtScrcpy/util/mousetap/xmousetap.cpp
Normal 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);
|
||||
}
|
||||
}
|
17
QtScrcpy/util/mousetap/xmousetap.h
Normal file
17
QtScrcpy/util/mousetap/xmousetap.h
Normal 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
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
28
docs/FAQ.md
28
docs/FAQ.md
|
@ -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调试里打开了允许模拟点击
|
||||
|
||||
.jpg)
|
||||
|
||||
# 其它
|
||||
## 支持声音(软件不做支持)
|
||||
[关于转发安卓声音到PC的讨论](https://github.com/Genymobile/scrcpy/issues/14#issuecomment-543204526)
|
||||
|
||||
|
@ -21,19 +41,11 @@ QtScrcpy.exe>属性>兼容性>更改高DPI设置>覆盖高DPI缩放行为>由以
|
|||
## 无法输入中文
|
||||
手机端安装搜狗输入法/QQ输入法就可以支持输入中文了
|
||||
|
||||
## 可以看到画面,但无法控制
|
||||
有些手机(小米等手机)需要额外打开控制权限,检查是否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进程并杀死,重新操作即可
|
||||
|
||||
|
|
11
docs/TODO.md
11
docs/TODO.md
|
@ -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
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
- KMT_CLICK
|
||||
- key 要映射的按键码
|
||||
- pos 模拟触摸的位置
|
||||
- switchMap 是否切换按键模式,点击此按键后,除了默认的模拟触摸映射,是否附带切换按键映射模式。(可以参考和平精英映射中M地图映射的效果)
|
||||
- switchMap 是否释放出鼠标,点击此按键后,除了默认的模拟触摸映射,是否释放出鼠标操作。(可以参考和平精英映射中M地图映射的效果)
|
||||
|
||||
- KMT_CLICK_TWICE
|
||||
- key 要映射的按键码
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
77
server/src/main/java/com/genymobile/scrcpy/CleanUp.java
Normal file
77
server/src/main/java/com/genymobile/scrcpy/CleanUp.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal file
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
BIN
third_party/scrcpy-server
vendored
BIN
third_party/scrcpy-server
vendored
Binary file not shown.
Loading…
Add table
Reference in a new issue