forked from qt-creator/qt-creator
Android: Enclose internals inside a storage
Prepare for internal QThread removal. Change-Id: I29f3a1d2a5e182eb305a95b9e810a079f16b7a2f Reviewed-by: Alessandro Portale <alessandro.portale@qt.io>
This commit is contained in:
@@ -13,13 +13,15 @@
|
|||||||
#include <projectexplorer/buildconfiguration.h>
|
#include <projectexplorer/buildconfiguration.h>
|
||||||
#include <projectexplorer/environmentaspect.h>
|
#include <projectexplorer/environmentaspect.h>
|
||||||
#include <projectexplorer/projectexplorerconstants.h>
|
#include <projectexplorer/projectexplorerconstants.h>
|
||||||
#include <projectexplorer/runconfigurationaspects.h>
|
|
||||||
#include <projectexplorer/runcontrol.h>
|
#include <projectexplorer/runcontrol.h>
|
||||||
#include <projectexplorer/target.h>
|
#include <projectexplorer/target.h>
|
||||||
|
|
||||||
|
#include <qmldebug/qmldebugcommandlinearguments.h>
|
||||||
|
|
||||||
#include <qtsupport/baseqtversion.h>
|
#include <qtsupport/baseqtversion.h>
|
||||||
#include <qtsupport/qtkitaspect.h>
|
#include <qtsupport/qtkitaspect.h>
|
||||||
|
|
||||||
|
#include <solutions/tasking/barrier.h>
|
||||||
#include <solutions/tasking/conditional.h>
|
#include <solutions/tasking/conditional.h>
|
||||||
|
|
||||||
#include <utils/hostosinfo.h>
|
#include <utils/hostosinfo.h>
|
||||||
@@ -27,10 +29,9 @@
|
|||||||
#include <utils/qtcprocess.h>
|
#include <utils/qtcprocess.h>
|
||||||
#include <utils/url.h>
|
#include <utils/url.h>
|
||||||
|
|
||||||
#include <QDate>
|
#include <QDateTime>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QScopeGuard>
|
|
||||||
#include <QTcpServer>
|
#include <QTcpServer>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -130,56 +131,108 @@ static FilePath debugServer(bool useLldb, const Target *target)
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
AndroidRunnerWorker::AndroidRunnerWorker(RunControl *runControl, const QString &deviceSerialNumber,
|
class RunnerStorage : public QObject
|
||||||
int apiLevel)
|
|
||||||
{
|
{
|
||||||
m_useLldb = Debugger::DebuggerKitAspect::engineType(runControl->kit())
|
Q_OBJECT
|
||||||
== Debugger::LldbEngineType;
|
|
||||||
|
public:
|
||||||
|
QStringList selector() const { return AndroidDeviceInfo::adbSelector(m_deviceSerialNumber); }
|
||||||
|
bool isPreNougat() const { return m_apiLevel > 0 && m_apiLevel <= 23; }
|
||||||
|
Utils::CommandLine adbCommand(std::initializer_list<Utils::CommandLine::ArgRef> args) const
|
||||||
|
{
|
||||||
|
CommandLine cmd{AndroidConfig::adbToolPath(), args};
|
||||||
|
cmd.prependArgs(selector());
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
QStringList userArgs() const
|
||||||
|
{
|
||||||
|
return m_processUser > 0 ? QStringList{"--user", QString::number(m_processUser)} : QStringList{};
|
||||||
|
}
|
||||||
|
QStringList packageArgs() const
|
||||||
|
{
|
||||||
|
// run-as <package-name> pwd fails on API 22 so route the pwd through shell.
|
||||||
|
return QStringList{"shell", "run-as", m_packageName} + userArgs();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString m_packageName;
|
||||||
|
QString m_intentName;
|
||||||
|
QStringList m_beforeStartAdbCommands;
|
||||||
|
QStringList m_afterFinishAdbCommands;
|
||||||
|
QStringList m_amStartExtraArgs;
|
||||||
|
qint64 m_processPID = -1;
|
||||||
|
qint64 m_processUser = -1;
|
||||||
|
bool m_useCppDebugger = false;
|
||||||
|
bool m_useLldb = false;
|
||||||
|
QmlDebug::QmlDebugServicesPreset m_qmlDebugServices;
|
||||||
|
QUrl m_qmlServer;
|
||||||
|
QString m_deviceSerialNumber;
|
||||||
|
int m_apiLevel = -1;
|
||||||
|
QString m_extraAppParams;
|
||||||
|
Utils::Environment m_extraEnvVars;
|
||||||
|
Utils::FilePath m_debugServerPath; // On build device, typically as part of ndk
|
||||||
|
bool m_useAppParamsForQmlDebugger = false;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void remoteProcessStarted(Utils::Port debugServerPort, const QUrl &qmlServer, qint64 pid);
|
||||||
|
void remoteProcessFinished(const QString &errString = QString());
|
||||||
|
|
||||||
|
void remoteOutput(const QString &output);
|
||||||
|
void remoteErrorOutput(const QString &output);
|
||||||
|
|
||||||
|
void cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
static void setupStorage(RunnerStorage *storage, RunControl *runControl,
|
||||||
|
const QString &deviceSerialNumber, int apiLevel)
|
||||||
|
{
|
||||||
|
storage->m_useLldb = Debugger::DebuggerKitAspect::engineType(runControl->kit())
|
||||||
|
== Debugger::LldbEngineType;
|
||||||
auto aspect = runControl->aspectData<Debugger::DebuggerRunConfigurationAspect>();
|
auto aspect = runControl->aspectData<Debugger::DebuggerRunConfigurationAspect>();
|
||||||
const Id runMode = runControl->runMode();
|
const Id runMode = runControl->runMode();
|
||||||
const bool debuggingMode = runMode == ProjectExplorer::Constants::DEBUG_RUN_MODE;
|
const bool debuggingMode = runMode == ProjectExplorer::Constants::DEBUG_RUN_MODE;
|
||||||
m_useCppDebugger = debuggingMode && aspect->useCppDebugger;
|
storage->m_useCppDebugger = debuggingMode && aspect->useCppDebugger;
|
||||||
if (debuggingMode && aspect->useQmlDebugger)
|
if (debuggingMode && aspect->useQmlDebugger)
|
||||||
m_qmlDebugServices = QmlDebug::QmlDebuggerServices;
|
storage->m_qmlDebugServices = QmlDebug::QmlDebuggerServices;
|
||||||
else if (runMode == ProjectExplorer::Constants::QML_PROFILER_RUN_MODE)
|
else if (runMode == ProjectExplorer::Constants::QML_PROFILER_RUN_MODE)
|
||||||
m_qmlDebugServices = QmlDebug::QmlProfilerServices;
|
storage->m_qmlDebugServices = QmlDebug::QmlProfilerServices;
|
||||||
else if (runMode == ProjectExplorer::Constants::QML_PREVIEW_RUN_MODE)
|
else if (runMode == ProjectExplorer::Constants::QML_PREVIEW_RUN_MODE)
|
||||||
m_qmlDebugServices = QmlDebug::QmlPreviewServices;
|
storage->m_qmlDebugServices = QmlDebug::QmlPreviewServices;
|
||||||
else
|
else
|
||||||
m_qmlDebugServices = QmlDebug::NoQmlDebugServices;
|
storage->m_qmlDebugServices = QmlDebug::NoQmlDebugServices;
|
||||||
if (m_qmlDebugServices != QmlDebug::NoQmlDebugServices) {
|
|
||||||
|
if (storage->m_qmlDebugServices != QmlDebug::NoQmlDebugServices) {
|
||||||
qCDebug(androidRunWorkerLog) << "QML debugging enabled";
|
qCDebug(androidRunWorkerLog) << "QML debugging enabled";
|
||||||
QTcpServer server;
|
QTcpServer server;
|
||||||
const bool isListening = server.listen(QHostAddress::LocalHost);
|
const bool isListening = server.listen(QHostAddress::LocalHost);
|
||||||
QTC_ASSERT(isListening,
|
QTC_ASSERT(isListening,
|
||||||
qDebug() << Tr::tr("No free ports available on host for QML debugging."));
|
qDebug() << Tr::tr("No free ports available on host for QML debugging."));
|
||||||
m_qmlServer.setScheme(Utils::urlTcpScheme());
|
storage->m_qmlServer.setScheme(Utils::urlTcpScheme());
|
||||||
m_qmlServer.setHost(server.serverAddress().toString());
|
storage->m_qmlServer.setHost(server.serverAddress().toString());
|
||||||
m_qmlServer.setPort(server.serverPort());
|
storage->m_qmlServer.setPort(server.serverPort());
|
||||||
qCDebug(androidRunWorkerLog) << "QML server:" << m_qmlServer.toDisplayString();
|
qCDebug(androidRunWorkerLog) << "QML server:" << storage->m_qmlServer.toDisplayString();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto target = runControl->target();
|
auto target = runControl->target();
|
||||||
m_packageName = AndroidManager::packageName(target);
|
storage->m_packageName = AndroidManager::packageName(target);
|
||||||
m_intentName = m_packageName + '/' + AndroidManager::activityName(target);
|
storage->m_intentName = storage->m_packageName + '/' + AndroidManager::activityName(target);
|
||||||
m_deviceSerialNumber = deviceSerialNumber;
|
storage->m_deviceSerialNumber = deviceSerialNumber;
|
||||||
m_apiLevel = apiLevel;
|
storage->m_apiLevel = apiLevel;
|
||||||
qCDebug(androidRunWorkerLog) << "Intent name:" << m_intentName
|
qCDebug(androidRunWorkerLog) << "Intent name:" << storage->m_intentName
|
||||||
<< "Package name:" << m_packageName;
|
<< "Package name:" << storage->m_packageName;
|
||||||
qCDebug(androidRunWorkerLog) << "Device API:" << m_apiLevel;
|
qCDebug(androidRunWorkerLog) << "Device API:" << storage->m_apiLevel;
|
||||||
|
|
||||||
m_extraEnvVars = runControl->aspectData<EnvironmentAspect>()->environment;
|
storage->m_extraEnvVars = runControl->aspectData<EnvironmentAspect>()->environment;
|
||||||
qCDebug(androidRunWorkerLog).noquote() << "Environment variables for the app"
|
qCDebug(androidRunWorkerLog).noquote() << "Environment variables for the app"
|
||||||
<< m_extraEnvVars.toStringList();
|
<< storage->m_extraEnvVars.toStringList();
|
||||||
|
|
||||||
if (target->buildConfigurations().first()->buildType() != BuildConfiguration::BuildType::Release)
|
if (target->buildConfigurations().first()->buildType() != BuildConfiguration::BuildType::Release)
|
||||||
m_extraAppParams = runControl->commandLine().arguments();
|
storage->m_extraAppParams = runControl->commandLine().arguments();
|
||||||
|
|
||||||
if (const Store sd = runControl->settingsData(Constants::ANDROID_AM_START_ARGS);
|
if (const Store sd = runControl->settingsData(Constants::ANDROID_AM_START_ARGS);
|
||||||
!sd.isEmpty()) {
|
!sd.isEmpty()) {
|
||||||
QTC_CHECK(sd.first().typeId() == QMetaType::QString);
|
QTC_CHECK(sd.first().typeId() == QMetaType::QString);
|
||||||
const QString startArgs = sd.first().toString();
|
const QString startArgs = sd.first().toString();
|
||||||
m_amStartExtraArgs = ProcessArgs::splitArgs(startArgs, OsTypeOtherUnix);
|
storage->m_amStartExtraArgs = ProcessArgs::splitArgs(startArgs, OsTypeOtherUnix);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const Store sd = runControl->settingsData(Constants::ANDROID_PRESTARTSHELLCMDLIST);
|
if (const Store sd = runControl->settingsData(Constants::ANDROID_PRESTARTSHELLCMDLIST);
|
||||||
@@ -188,7 +241,7 @@ AndroidRunnerWorker::AndroidRunnerWorker(RunControl *runControl, const QString &
|
|||||||
QTC_CHECK(first.typeId() == QMetaType::QStringList);
|
QTC_CHECK(first.typeId() == QMetaType::QStringList);
|
||||||
const QStringList commands = first.toStringList();
|
const QStringList commands = first.toStringList();
|
||||||
for (const QString &shellCmd : commands)
|
for (const QString &shellCmd : commands)
|
||||||
m_beforeStartAdbCommands.append(QString("shell %1").arg(shellCmd));
|
storage->m_beforeStartAdbCommands.append(QString("shell %1").arg(shellCmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const Store sd = runControl->settingsData(Constants::ANDROID_POSTFINISHSHELLCMDLIST);
|
if (const Store sd = runControl->settingsData(Constants::ANDROID_POSTFINISHSHELLCMDLIST);
|
||||||
@@ -197,76 +250,67 @@ AndroidRunnerWorker::AndroidRunnerWorker(RunControl *runControl, const QString &
|
|||||||
QTC_CHECK(first.typeId() == QMetaType::QStringList);
|
QTC_CHECK(first.typeId() == QMetaType::QStringList);
|
||||||
const QStringList commands = first.toStringList();
|
const QStringList commands = first.toStringList();
|
||||||
for (const QString &shellCmd : commands)
|
for (const QString &shellCmd : commands)
|
||||||
m_afterFinishAdbCommands.append(QString("shell %1").arg(shellCmd));
|
storage->m_afterFinishAdbCommands.append(QString("shell %1").arg(shellCmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_debugServerPath = debugServer(m_useLldb, target);
|
storage->m_debugServerPath = debugServer(storage->m_useLldb, target);
|
||||||
qCDebug(androidRunWorkerLog).noquote() << "Device Serial:" << m_deviceSerialNumber
|
qCDebug(androidRunWorkerLog).noquote() << "Device Serial:" << storage->m_deviceSerialNumber
|
||||||
<< ", API level:" << m_apiLevel
|
<< ", API level:" << storage->m_apiLevel
|
||||||
<< ", Extra Start Args:" << m_amStartExtraArgs
|
<< ", Extra Start Args:" << storage->m_amStartExtraArgs
|
||||||
<< ", Before Start ADB cmds:" << m_beforeStartAdbCommands
|
<< ", Before Start ADB cmds:" << storage->m_beforeStartAdbCommands
|
||||||
<< ", After finish ADB cmds:" << m_afterFinishAdbCommands
|
<< ", After finish ADB cmds:" << storage->m_afterFinishAdbCommands
|
||||||
<< ", Debug server path:" << m_debugServerPath;
|
<< ", Debug server path:" << storage->m_debugServerPath;
|
||||||
|
|
||||||
QtSupport::QtVersion *version = QtSupport::QtKitAspect::qtVersion(target->kit());
|
QtSupport::QtVersion *version = QtSupport::QtKitAspect::qtVersion(target->kit());
|
||||||
m_useAppParamsForQmlDebugger = version->qtVersion() >= QVersionNumber(5, 12);
|
storage->m_useAppParamsForQmlDebugger = version->qtVersion() >= QVersionNumber(5, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidRunnerWorker::AndroidRunnerWorker(RunControl *runControl, const QString &deviceSerialNumber,
|
||||||
|
int apiLevel)
|
||||||
|
: m_storage(new RunnerStorage)
|
||||||
|
{
|
||||||
|
m_storage->setParent(this); // Move m_storage object together with *this into a separate thread.
|
||||||
|
setupStorage(m_storage.get(), runControl, deviceSerialNumber, apiLevel);
|
||||||
m_taskTreeRunner.setParent(this); // Move m_taskTreeRunner object together with *this into a separate thread.
|
m_taskTreeRunner.setParent(this); // Move m_taskTreeRunner object together with *this into a separate thread.
|
||||||
|
|
||||||
|
connect(m_storage.get(), &RunnerStorage::remoteProcessStarted,
|
||||||
|
this, &AndroidRunnerWorker::remoteProcessStarted);
|
||||||
|
connect(m_storage.get(), &RunnerStorage::remoteProcessFinished,
|
||||||
|
this, &AndroidRunnerWorker::remoteProcessFinished);
|
||||||
|
connect(m_storage.get(), &RunnerStorage::remoteOutput,
|
||||||
|
this, &AndroidRunnerWorker::remoteOutput);
|
||||||
|
connect(m_storage.get(), &RunnerStorage::remoteErrorOutput,
|
||||||
|
this, &AndroidRunnerWorker::remoteErrorOutput);
|
||||||
|
|
||||||
|
connect(this, &AndroidRunnerWorker::cancel, m_storage.get(), &RunnerStorage::cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
AndroidRunnerWorker::~AndroidRunnerWorker()
|
static ExecutableItem forceStopRecipe(RunnerStorage *storage)
|
||||||
{
|
{
|
||||||
if (m_processPID != -1)
|
const auto onForceStopSetup = [storage](Process &process) {
|
||||||
TaskTree::runBlocking(Group { forceStopRecipe(), postDoneRecipe() });
|
process.setCommand(storage->adbCommand({"shell", "am", "force-stop", storage->m_packageName}));
|
||||||
}
|
|
||||||
|
|
||||||
QStringList AndroidRunnerWorker::selector() const
|
|
||||||
{
|
|
||||||
return AndroidDeviceInfo::adbSelector(m_deviceSerialNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandLine AndroidRunnerWorker::adbCommand(std::initializer_list<CommandLine::ArgRef> args) const
|
|
||||||
{
|
|
||||||
CommandLine cmd{AndroidConfig::adbToolPath(), args};
|
|
||||||
cmd.prependArgs(selector());
|
|
||||||
return cmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList AndroidRunnerWorker::userArgs() const
|
|
||||||
{
|
|
||||||
return m_processUser > 0 ? QStringList{"--user", QString::number(m_processUser)} : QStringList{};
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList AndroidRunnerWorker::packageArgs() const
|
|
||||||
{
|
|
||||||
// run-as <package-name> pwd fails on API 22 so route the pwd through shell.
|
|
||||||
return QStringList{"shell", "run-as", m_packageName} + userArgs();
|
|
||||||
}
|
|
||||||
|
|
||||||
ExecutableItem AndroidRunnerWorker::forceStopRecipe()
|
|
||||||
{
|
|
||||||
const auto onForceStopSetup = [this](Process &process) {
|
|
||||||
process.setCommand(adbCommand({"shell", "am", "force-stop", m_packageName}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto pidCheckSync = Sync([this] { return m_processPID != -1; });
|
const auto pidCheckSync = Sync([storage] { return storage->m_processPID != -1; });
|
||||||
|
|
||||||
const auto onPidOfSetup = [this](Process &process) {
|
const auto onPidOfSetup = [storage](Process &process) {
|
||||||
process.setCommand(adbCommand({"shell", "pidof", m_packageName}));
|
process.setCommand(storage->adbCommand({"shell", "pidof", storage->m_packageName}));
|
||||||
};
|
};
|
||||||
const auto onPidOfDone = [this](const Process &process) {
|
const auto onPidOfDone = [storage](const Process &process) {
|
||||||
const QString pid = process.cleanedStdOut().trimmed();
|
const QString pid = process.cleanedStdOut().trimmed();
|
||||||
return pid == QString::number(m_processPID);
|
return pid == QString::number(storage->m_processPID);
|
||||||
};
|
};
|
||||||
const auto pidOfTask = ProcessTask(onPidOfSetup, onPidOfDone, CallDoneIf::Success);
|
const auto pidOfTask = ProcessTask(onPidOfSetup, onPidOfDone, CallDoneIf::Success);
|
||||||
|
|
||||||
const auto onRunAsSetup = [this](Process &process) {
|
const auto onRunAsSetup = [storage](Process &process) {
|
||||||
process.setCommand(adbCommand({"shell", "run-as", m_packageName, "kill", "-9",
|
process.setCommand(storage->adbCommand({"shell", "run-as", storage->m_packageName, "kill", "-9",
|
||||||
QString::number(m_processPID)}));
|
QString::number(storage->m_processPID)}));
|
||||||
};
|
};
|
||||||
const auto runAsTask = ProcessTask(onRunAsSetup);
|
const auto runAsTask = ProcessTask(onRunAsSetup);
|
||||||
|
|
||||||
const auto onKillSetup = [this](Process &process) {
|
const auto onKillSetup = [storage](Process &process) {
|
||||||
process.setCommand(adbCommand({"shell", "kill", "-9", QString::number(m_processPID)}));
|
process.setCommand(storage->adbCommand({"shell", "kill", "-9",
|
||||||
|
QString::number(storage->m_processPID)}));
|
||||||
};
|
};
|
||||||
|
|
||||||
return Group {
|
return Group {
|
||||||
@@ -277,8 +321,8 @@ ExecutableItem AndroidRunnerWorker::forceStopRecipe()
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ExecutableItem AndroidRunnerWorker::removeForwardPortRecipe(
|
static ExecutableItem removeForwardPortRecipe(RunnerStorage *storage, const QString &port,
|
||||||
const QString &port, const QString &adbArg, const QString &portType)
|
const QString &adbArg, const QString &portType)
|
||||||
{
|
{
|
||||||
const auto onForwardListSetup = [](Process &process) {
|
const auto onForwardListSetup = [](Process &process) {
|
||||||
process.setCommand({AndroidConfig::adbToolPath(), {"forward", "--list"}});
|
process.setCommand({AndroidConfig::adbToolPath(), {"forward", "--list"}});
|
||||||
@@ -287,22 +331,22 @@ ExecutableItem AndroidRunnerWorker::removeForwardPortRecipe(
|
|||||||
return process.cleanedStdOut().trimmed().contains(port);
|
return process.cleanedStdOut().trimmed().contains(port);
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onForwardRemoveSetup = [this, port](Process &process) {
|
const auto onForwardRemoveSetup = [storage, port](Process &process) {
|
||||||
process.setCommand(adbCommand({"--remove", port}));
|
process.setCommand(storage->adbCommand({"--remove", port}));
|
||||||
};
|
};
|
||||||
const auto onForwardRemoveDone = [this](const Process &process) {
|
const auto onForwardRemoveDone = [storage](const Process &process) {
|
||||||
emit remoteErrorOutput(process.cleanedStdErr().trimmed());
|
emit storage->remoteErrorOutput(process.cleanedStdErr().trimmed());
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onForwardPortSetup = [this, port, adbArg](Process &process) {
|
const auto onForwardPortSetup = [storage, port, adbArg](Process &process) {
|
||||||
process.setCommand(adbCommand({"forward", port, adbArg}));
|
process.setCommand(storage->adbCommand({"forward", port, adbArg}));
|
||||||
};
|
};
|
||||||
const auto onForwardPortDone = [this, port, portType](DoneWith result) {
|
const auto onForwardPortDone = [storage, port, portType](DoneWith result) {
|
||||||
if (result == DoneWith::Success)
|
if (result == DoneWith::Success)
|
||||||
m_afterFinishAdbCommands.push_back("forward --remove " + port);
|
storage->m_afterFinishAdbCommands.push_back("forward --remove " + port);
|
||||||
else
|
else
|
||||||
emit remoteProcessFinished(Tr::tr("Failed to forward %1 debugging ports.").arg(portType));
|
emit storage->remoteProcessFinished(Tr::tr("Failed to forward %1 debugging ports.").arg(portType));
|
||||||
};
|
};
|
||||||
|
|
||||||
return Group {
|
return Group {
|
||||||
@@ -315,17 +359,17 @@ ExecutableItem AndroidRunnerWorker::removeForwardPortRecipe(
|
|||||||
|
|
||||||
// The startBarrier is passed when logcat process received "Sending WAIT chunk" message.
|
// The startBarrier is passed when logcat process received "Sending WAIT chunk" message.
|
||||||
// The settledBarrier is passed when logcat process received "debugger has settled" message.
|
// The settledBarrier is passed when logcat process received "debugger has settled" message.
|
||||||
ExecutableItem AndroidRunnerWorker::jdbRecipe(const SingleBarrier &startBarrier,
|
static ExecutableItem jdbRecipe(RunnerStorage *storage, const SingleBarrier &startBarrier,
|
||||||
const SingleBarrier &settledBarrier)
|
const SingleBarrier &settledBarrier)
|
||||||
{
|
{
|
||||||
const auto onSetup = [this] {
|
const auto onSetup = [storage] {
|
||||||
return m_useCppDebugger ? SetupResult::Continue : SetupResult::StopWithSuccess;
|
return storage->m_useCppDebugger ? SetupResult::Continue : SetupResult::StopWithSuccess;
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onTaskTreeSetup = [this](TaskTree &taskTree) {
|
const auto onTaskTreeSetup = [storage](TaskTree &taskTree) {
|
||||||
taskTree.setRecipe({
|
taskTree.setRecipe({
|
||||||
removeForwardPortRecipe("tcp:" + s_localJdbServerPort.toString(),
|
removeForwardPortRecipe(storage, "tcp:" + s_localJdbServerPort.toString(),
|
||||||
"jdwp:" + QString::number(m_processPID), "JDB")
|
"jdwp:" + QString::number(storage->m_processPID), "JDB")
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -338,7 +382,7 @@ ExecutableItem AndroidRunnerWorker::jdbRecipe(const SingleBarrier &startBarrier,
|
|||||||
process.setProcessMode(ProcessMode::Writer);
|
process.setProcessMode(ProcessMode::Writer);
|
||||||
process.setProcessChannelMode(QProcess::MergedChannels);
|
process.setProcessChannelMode(QProcess::MergedChannels);
|
||||||
process.setReaperTimeout(s_jdbTimeout);
|
process.setReaperTimeout(s_jdbTimeout);
|
||||||
connect(settledBarrier->barrier(), &Barrier::done, &process, [processPtr = &process] {
|
QObject::connect(settledBarrier->barrier(), &Barrier::done, &process, [processPtr = &process] {
|
||||||
processPtr->write("ignore uncaught java.lang.Throwable\n"
|
processPtr->write("ignore uncaught java.lang.Throwable\n"
|
||||||
"threads\n"
|
"threads\n"
|
||||||
"cont\n"
|
"cont\n"
|
||||||
@@ -359,7 +403,7 @@ ExecutableItem AndroidRunnerWorker::jdbRecipe(const SingleBarrier &startBarrier,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ExecutableItem AndroidRunnerWorker::logcatRecipe()
|
static ExecutableItem logcatRecipe(RunnerStorage *storage)
|
||||||
{
|
{
|
||||||
struct Buffer {
|
struct Buffer {
|
||||||
QStringList timeArgs;
|
QStringList timeArgs;
|
||||||
@@ -367,24 +411,24 @@ ExecutableItem AndroidRunnerWorker::logcatRecipe()
|
|||||||
QByteArray stdErrBuffer;
|
QByteArray stdErrBuffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Storage<Buffer> storage;
|
const Storage<Buffer> bufferStorage;
|
||||||
const SingleBarrier startJdbBarrier; // When logcat received "Sending WAIT chunk".
|
const SingleBarrier startJdbBarrier; // When logcat received "Sending WAIT chunk".
|
||||||
const SingleBarrier settledJdbBarrier; // When logcat received "debugger has settled".
|
const SingleBarrier settledJdbBarrier; // When logcat received "debugger has settled".
|
||||||
|
|
||||||
const auto onTimeSetup = [this](Process &process) {
|
const auto onTimeSetup = [storage](Process &process) {
|
||||||
process.setCommand(adbCommand({"shell", "date", "+%s"}));
|
process.setCommand(storage->adbCommand({"shell", "date", "+%s"}));
|
||||||
};
|
};
|
||||||
const auto onTimeDone = [storage](const Process &process) {
|
const auto onTimeDone = [bufferStorage](const Process &process) {
|
||||||
storage->timeArgs = {"-T", QDateTime::fromSecsSinceEpoch(
|
bufferStorage->timeArgs = {"-T", QDateTime::fromSecsSinceEpoch(
|
||||||
process.cleanedStdOut().trimmed().toInt()).toString("MM-dd hh:mm:ss.mmm")};
|
process.cleanedStdOut().trimmed().toInt()).toString("MM-dd hh:mm:ss.mmm")};
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onLogcatSetup = [this, storage, startJdbBarrier, settledJdbBarrier](Process &process) {
|
const auto onLogcatSetup = [storage, bufferStorage, startJdbBarrier, settledJdbBarrier](Process &process) {
|
||||||
Buffer *bufferPtr = storage.activeStorage();
|
Buffer *bufferPtr = bufferStorage.activeStorage();
|
||||||
const auto parseLogcat = [this, bufferPtr, start = startJdbBarrier->barrier(),
|
const auto parseLogcat = [storage, bufferPtr, start = startJdbBarrier->barrier(),
|
||||||
settled = settledJdbBarrier->barrier(), processPtr = &process](
|
settled = settledJdbBarrier->barrier(), processPtr = &process](
|
||||||
QProcess::ProcessChannel channel) {
|
QProcess::ProcessChannel channel) {
|
||||||
if (m_processPID == -1)
|
if (storage->m_processPID == -1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
QByteArray &buffer = channel == QProcess::StandardOutput ? bufferPtr->stdOutBuffer
|
QByteArray &buffer = channel == QProcess::StandardOutput ? bufferPtr->stdOutBuffer
|
||||||
@@ -400,13 +444,13 @@ ExecutableItem AndroidRunnerWorker::logcatRecipe()
|
|||||||
else
|
else
|
||||||
buffer = lines.takeLast(); // incomplete line
|
buffer = lines.takeLast(); // incomplete line
|
||||||
|
|
||||||
const QString pidString = QString::number(m_processPID);
|
const QString pidString = QString::number(storage->m_processPID);
|
||||||
for (const QByteArray &msg : std::as_const(lines)) {
|
for (const QByteArray &msg : std::as_const(lines)) {
|
||||||
const QString line = QString::fromUtf8(msg).trimmed() + QLatin1Char('\n');
|
const QString line = QString::fromUtf8(msg).trimmed() + QLatin1Char('\n');
|
||||||
if (!line.contains(pidString))
|
if (!line.contains(pidString))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (m_useCppDebugger) {
|
if (storage->m_useCppDebugger) {
|
||||||
if (start->current() == 0 && msg.trimmed().endsWith("Sending WAIT chunk"))
|
if (start->current() == 0 && msg.trimmed().endsWith("Sending WAIT chunk"))
|
||||||
start->advance();
|
start->advance();
|
||||||
else if (settled->current() == 0 && msg.indexOf("debugger has settled") > 0)
|
else if (settled->current() == 0 && msg.indexOf("debugger has settled") > 0)
|
||||||
@@ -437,27 +481,27 @@ ExecutableItem AndroidRunnerWorker::logcatRecipe()
|
|||||||
const QString msgType = match.captured(2);
|
const QString msgType = match.captured(2);
|
||||||
const QString output = line.mid(match.capturedStart(2));
|
const QString output = line.mid(match.capturedStart(2));
|
||||||
if (onlyError || msgType == "F" || msgType == "E" || msgType == "W")
|
if (onlyError || msgType == "F" || msgType == "E" || msgType == "W")
|
||||||
emit remoteErrorOutput(output);
|
emit storage->remoteErrorOutput(output);
|
||||||
else
|
else
|
||||||
emit remoteOutput(output);
|
emit storage->remoteOutput(output);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (onlyError || line.startsWith("F/") || line.startsWith("E/")
|
if (onlyError || line.startsWith("F/") || line.startsWith("E/")
|
||||||
|| line.startsWith("W/")) {
|
|| line.startsWith("W/")) {
|
||||||
emit remoteErrorOutput(line);
|
emit storage->remoteErrorOutput(line);
|
||||||
} else {
|
} else {
|
||||||
emit remoteOutput(line);
|
emit storage->remoteOutput(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
connect(&process, &Process::readyReadStandardOutput, this, [parseLogcat] {
|
QObject::connect(&process, &Process::readyReadStandardOutput, &process, [parseLogcat] {
|
||||||
parseLogcat(QProcess::StandardOutput);
|
parseLogcat(QProcess::StandardOutput);
|
||||||
});
|
});
|
||||||
connect(&process, &Process::readyReadStandardError, this, [parseLogcat] {
|
QObject::connect(&process, &Process::readyReadStandardError, &process, [parseLogcat] {
|
||||||
parseLogcat(QProcess::StandardError);
|
parseLogcat(QProcess::StandardError);
|
||||||
});
|
});
|
||||||
process.setCommand(adbCommand({"logcat", storage->timeArgs}));
|
process.setCommand(storage->adbCommand({"logcat", bufferStorage->timeArgs}));
|
||||||
};
|
};
|
||||||
|
|
||||||
return Group {
|
return Group {
|
||||||
@@ -465,73 +509,73 @@ ExecutableItem AndroidRunnerWorker::logcatRecipe()
|
|||||||
startJdbBarrier,
|
startJdbBarrier,
|
||||||
settledJdbBarrier,
|
settledJdbBarrier,
|
||||||
Group {
|
Group {
|
||||||
storage,
|
bufferStorage,
|
||||||
ProcessTask(onTimeSetup, onTimeDone, CallDoneIf::Success) || successItem,
|
ProcessTask(onTimeSetup, onTimeDone, CallDoneIf::Success) || successItem,
|
||||||
ProcessTask(onLogcatSetup)
|
ProcessTask(onLogcatSetup)
|
||||||
},
|
},
|
||||||
jdbRecipe(startJdbBarrier, settledJdbBarrier)
|
jdbRecipe(storage, startJdbBarrier, settledJdbBarrier)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ExecutableItem AndroidRunnerWorker::preStartRecipe()
|
static ExecutableItem preStartRecipe(RunnerStorage *storage)
|
||||||
{
|
{
|
||||||
const QString port = "tcp:" + QString::number(m_qmlServer.port());
|
const QString port = "tcp:" + QString::number(storage->m_qmlServer.port());
|
||||||
|
|
||||||
const Storage<QStringList> argsStorage;
|
const Storage<QStringList> argsStorage;
|
||||||
const LoopList iterator(m_beforeStartAdbCommands);
|
const LoopList iterator(storage->m_beforeStartAdbCommands);
|
||||||
|
|
||||||
const auto onArgsSetup = [this, argsStorage] {
|
const auto onArgsSetup = [storage, argsStorage] {
|
||||||
*argsStorage = {"shell", "am", "start", "-n", m_intentName};
|
*argsStorage = {"shell", "am", "start", "-n", storage->m_intentName};
|
||||||
if (m_useCppDebugger)
|
if (storage->m_useCppDebugger)
|
||||||
*argsStorage << "-D";
|
*argsStorage << "-D";
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onPreCommandSetup = [this, iterator](Process &process) {
|
const auto onPreCommandSetup = [storage, iterator](Process &process) {
|
||||||
process.setCommand(adbCommand({iterator->split(' ', Qt::SkipEmptyParts)}));
|
process.setCommand(storage->adbCommand({iterator->split(' ', Qt::SkipEmptyParts)}));
|
||||||
};
|
};
|
||||||
const auto onPreCommandDone = [this](const Process &process) {
|
const auto onPreCommandDone = [storage](const Process &process) {
|
||||||
emit remoteErrorOutput(process.cleanedStdErr().trimmed());
|
emit storage->remoteErrorOutput(process.cleanedStdErr().trimmed());
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onQmlDebugSetup = [this] {
|
const auto onQmlDebugSetup = [storage] {
|
||||||
return m_qmlDebugServices == QmlDebug::NoQmlDebugServices ? SetupResult::StopWithSuccess
|
return storage->m_qmlDebugServices == QmlDebug::NoQmlDebugServices ? SetupResult::StopWithSuccess
|
||||||
: SetupResult::Continue;
|
: SetupResult::Continue;
|
||||||
};
|
};
|
||||||
const auto onQmlDebugDone = [this, argsStorage] {
|
const auto onQmlDebugDone = [storage, argsStorage] {
|
||||||
const QString qmljsdebugger = QString("port:%1,block,services:%2")
|
const QString qmljsdebugger = QString("port:%1,block,services:%2")
|
||||||
.arg(m_qmlServer.port()).arg(QmlDebug::qmlDebugServices(m_qmlDebugServices));
|
.arg(storage->m_qmlServer.port()).arg(QmlDebug::qmlDebugServices(storage->m_qmlDebugServices));
|
||||||
|
|
||||||
if (m_useAppParamsForQmlDebugger) {
|
if (storage->m_useAppParamsForQmlDebugger) {
|
||||||
if (!m_extraAppParams.isEmpty())
|
if (!storage->m_extraAppParams.isEmpty())
|
||||||
m_extraAppParams.prepend(' ');
|
storage->m_extraAppParams.prepend(' ');
|
||||||
m_extraAppParams.prepend("-qmljsdebugger=" + qmljsdebugger);
|
storage->m_extraAppParams.prepend("-qmljsdebugger=" + qmljsdebugger);
|
||||||
} else {
|
} else {
|
||||||
*argsStorage << "-e" << "qml_debug" << "true"
|
*argsStorage << "-e" << "qml_debug" << "true"
|
||||||
<< "-e" << "qmljsdebugger" << qmljsdebugger;
|
<< "-e" << "qmljsdebugger" << qmljsdebugger;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onActivitySetup = [this, argsStorage](Process &process) {
|
const auto onActivitySetup = [storage, argsStorage](Process &process) {
|
||||||
QStringList args = *argsStorage;
|
QStringList args = *argsStorage;
|
||||||
args << m_amStartExtraArgs;
|
args << storage->m_amStartExtraArgs;
|
||||||
|
|
||||||
if (!m_extraAppParams.isEmpty()) {
|
if (!storage->m_extraAppParams.isEmpty()) {
|
||||||
const QStringList appArgs =
|
const QStringList appArgs =
|
||||||
ProcessArgs::splitArgs(m_extraAppParams, Utils::OsType::OsTypeLinux);
|
ProcessArgs::splitArgs(storage->m_extraAppParams, Utils::OsType::OsTypeLinux);
|
||||||
qCDebug(androidRunWorkerLog).noquote() << "Using application arguments: " << appArgs;
|
qCDebug(androidRunWorkerLog).noquote() << "Using application arguments: " << appArgs;
|
||||||
args << "-e" << "extraappparams"
|
args << "-e" << "extraappparams"
|
||||||
<< QString::fromLatin1(appArgs.join(' ').toUtf8().toBase64());
|
<< QString::fromLatin1(appArgs.join(' ').toUtf8().toBase64());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_extraEnvVars.hasChanges()) {
|
if (storage->m_extraEnvVars.hasChanges()) {
|
||||||
args << "-e" << "extraenvvars"
|
args << "-e" << "extraenvvars"
|
||||||
<< QString::fromLatin1(m_extraEnvVars.toStringList().join('\t')
|
<< QString::fromLatin1(storage->m_extraEnvVars.toStringList().join('\t')
|
||||||
.toUtf8().toBase64());
|
.toUtf8().toBase64());
|
||||||
}
|
}
|
||||||
process.setCommand(adbCommand({args}));
|
process.setCommand(storage->adbCommand({args}));
|
||||||
};
|
};
|
||||||
const auto onActivityDone = [this](const Process &process) {
|
const auto onActivityDone = [storage](const Process &process) {
|
||||||
emit remoteProcessFinished(Tr::tr("Activity Manager error: %1")
|
emit storage->remoteProcessFinished(Tr::tr("Activity Manager error: %1")
|
||||||
.arg(process.cleanedStdErr().trimmed()));
|
.arg(process.cleanedStdErr().trimmed()));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -544,28 +588,28 @@ ExecutableItem AndroidRunnerWorker::preStartRecipe()
|
|||||||
},
|
},
|
||||||
Group {
|
Group {
|
||||||
onGroupSetup(onQmlDebugSetup),
|
onGroupSetup(onQmlDebugSetup),
|
||||||
removeForwardPortRecipe(port, port, "QML"),
|
removeForwardPortRecipe(storage, port, port, "QML"),
|
||||||
onGroupDone(onQmlDebugDone, CallDoneIf::Success)
|
onGroupDone(onQmlDebugDone, CallDoneIf::Success)
|
||||||
},
|
},
|
||||||
ProcessTask(onActivitySetup, onActivityDone, CallDoneIf::Error)
|
ProcessTask(onActivitySetup, onActivityDone, CallDoneIf::Error)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ExecutableItem AndroidRunnerWorker::postDoneRecipe()
|
static ExecutableItem postDoneRecipe(RunnerStorage *storage)
|
||||||
{
|
{
|
||||||
const LoopUntil iterator([this](int iteration) {
|
const LoopUntil iterator([storage](int iteration) {
|
||||||
return iteration < m_afterFinishAdbCommands.size();
|
return iteration < storage->m_afterFinishAdbCommands.size();
|
||||||
});
|
});
|
||||||
|
|
||||||
const auto onProcessSetup = [this, iterator](Process &process) {
|
const auto onProcessSetup = [storage, iterator](Process &process) {
|
||||||
process.setCommand(adbCommand(
|
process.setCommand(storage->adbCommand(
|
||||||
{m_afterFinishAdbCommands.at(iterator.iteration()).split(' ', Qt::SkipEmptyParts)}));
|
{storage->m_afterFinishAdbCommands.at(iterator.iteration()).split(' ', Qt::SkipEmptyParts)}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onDone = [this] {
|
const auto onDone = [storage] {
|
||||||
m_processPID = -1;
|
storage->m_processPID = -1;
|
||||||
m_processUser = -1;
|
storage->m_processUser = -1;
|
||||||
emit remoteProcessFinished("\n\n" + Tr::tr("\"%1\" died.").arg(m_packageName));
|
emit storage->remoteProcessFinished("\n\n" + Tr::tr("\"%1\" died.").arg(storage->m_packageName));
|
||||||
};
|
};
|
||||||
|
|
||||||
return Group {
|
return Group {
|
||||||
@@ -578,97 +622,21 @@ ExecutableItem AndroidRunnerWorker::postDoneRecipe()
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ExecutableItem AndroidRunnerWorker::pidRecipe()
|
|
||||||
{
|
|
||||||
using PidUserPair = std::pair<qint64, qint64>;
|
|
||||||
const Storage<PidUserPair> pidStorage;
|
|
||||||
|
|
||||||
const QString pidScript = isPreNougat()
|
|
||||||
? QString("for p in /proc/[0-9]*; do cat <$p/cmdline && echo :${p##*/}; done")
|
|
||||||
: QString("pidof -s '%1'").arg(m_packageName);
|
|
||||||
|
|
||||||
const auto onPidSetup = [this, pidScript](Process &process) {
|
|
||||||
process.setCommand(adbCommand({"shell", pidScript}));
|
|
||||||
};
|
|
||||||
const auto onPidDone = [pidStorage, packageName = m_packageName,
|
|
||||||
isPreNougat = isPreNougat()](const Process &process) {
|
|
||||||
const QString out = process.allOutput();
|
|
||||||
if (isPreNougat)
|
|
||||||
pidStorage->first = extractPID(out, packageName);
|
|
||||||
else if (!out.isEmpty())
|
|
||||||
pidStorage->first = out.trimmed().toLongLong();
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto onUserSetup = [this, pidStorage](Process &process) {
|
|
||||||
process.setCommand(
|
|
||||||
adbCommand({"shell", "ps", "-o", "user", "-p", QString::number(pidStorage->first)}));
|
|
||||||
};
|
|
||||||
const auto onUserDone = [pidStorage](const Process &process) {
|
|
||||||
const QString out = process.allOutput();
|
|
||||||
if (out.isEmpty())
|
|
||||||
return DoneResult::Error;
|
|
||||||
|
|
||||||
QRegularExpressionMatch match;
|
|
||||||
qsizetype matchPos = out.indexOf(userIdPattern, 0, &match);
|
|
||||||
if (matchPos >= 0 && match.capturedLength(1) > 0) {
|
|
||||||
bool ok = false;
|
|
||||||
const qint64 processUser = match.captured(1).toInt(&ok);
|
|
||||||
if (ok) {
|
|
||||||
pidStorage->second = processUser;
|
|
||||||
return DoneResult::Success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DoneResult::Error;
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto onPidSync = [this, pidStorage] {
|
|
||||||
qCDebug(androidRunWorkerLog) << "Process ID changed from:" << m_processPID
|
|
||||||
<< "to:" << pidStorage->first;
|
|
||||||
m_processPID = pidStorage->first;
|
|
||||||
m_processUser = pidStorage->second;
|
|
||||||
emit remoteProcessStarted(s_localDebugServerPort, m_qmlServer, m_processPID);
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto onIsAliveSetup = [this, pidStorage](Process &process) {
|
|
||||||
process.setProcessChannelMode(QProcess::MergedChannels);
|
|
||||||
process.setCommand(adbCommand({"shell", pidPollingScript.arg(pidStorage->first)}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return Group {
|
|
||||||
pidStorage,
|
|
||||||
onGroupSetup([pidStorage] { *pidStorage = {-1, 0}; }),
|
|
||||||
Forever {
|
|
||||||
stopOnSuccess,
|
|
||||||
ProcessTask(onPidSetup, onPidDone, CallDoneIf::Success),
|
|
||||||
TimeoutTask([](std::chrono::milliseconds &timeout) { timeout = 200ms; },
|
|
||||||
DoneResult::Error)
|
|
||||||
}.withTimeout(45s),
|
|
||||||
ProcessTask(onUserSetup, onUserDone, CallDoneIf::Success),
|
|
||||||
Sync(onPidSync),
|
|
||||||
Group {
|
|
||||||
parallel,
|
|
||||||
startNativeDebuggingRecipe(),
|
|
||||||
ProcessTask(onIsAliveSetup)
|
|
||||||
},
|
|
||||||
postDoneRecipe()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static QString tempDebugServerPath(int count)
|
static QString tempDebugServerPath(int count)
|
||||||
{
|
{
|
||||||
static const QString tempDebugServerPathTemplate = "/data/local/tmp/%1";
|
static const QString tempDebugServerPathTemplate = "/data/local/tmp/%1";
|
||||||
return tempDebugServerPathTemplate.arg(count);
|
return tempDebugServerPathTemplate.arg(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
ExecutableItem AndroidRunnerWorker::uploadDebugServerRecipe(const QString &debugServerFileName)
|
static ExecutableItem uploadDebugServerRecipe(RunnerStorage *storage, const QString &debugServerFileName)
|
||||||
{
|
{
|
||||||
const Storage<QString> tempDebugServerPathStorage;
|
const Storage<QString> tempDebugServerPathStorage;
|
||||||
const LoopUntil iterator([tempDebugServerPathStorage](int iteration) {
|
const LoopUntil iterator([tempDebugServerPathStorage](int iteration) {
|
||||||
return tempDebugServerPathStorage->isEmpty() && iteration <= GdbTempFileMaxCounter;
|
return tempDebugServerPathStorage->isEmpty() && iteration <= GdbTempFileMaxCounter;
|
||||||
});
|
});
|
||||||
const auto onDeviceFileExistsSetup = [this, iterator](Process &process) {
|
const auto onDeviceFileExistsSetup = [storage, iterator](Process &process) {
|
||||||
process.setCommand(
|
process.setCommand(
|
||||||
adbCommand({"shell", "ls", tempDebugServerPath(iterator.iteration()), "2>/dev/null"}));
|
storage->adbCommand({"shell", "ls", tempDebugServerPath(iterator.iteration()), "2>/dev/null"}));
|
||||||
};
|
};
|
||||||
const auto onDeviceFileExistsDone = [iterator, tempDebugServerPathStorage](
|
const auto onDeviceFileExistsDone = [iterator, tempDebugServerPathStorage](
|
||||||
const Process &process, DoneWith result) {
|
const Process &process, DoneWith result) {
|
||||||
@@ -683,25 +651,25 @@ ExecutableItem AndroidRunnerWorker::uploadDebugServerRecipe(const QString &debug
|
|||||||
return tempDirOK;
|
return tempDirOK;
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onCleanupSetup = [this, tempDebugServerPathStorage](Process &process) {
|
const auto onCleanupSetup = [storage, tempDebugServerPathStorage](Process &process) {
|
||||||
process.setCommand(adbCommand({"shell", "rm", "-f", *tempDebugServerPathStorage}));
|
process.setCommand(storage->adbCommand({"shell", "rm", "-f", *tempDebugServerPathStorage}));
|
||||||
};
|
};
|
||||||
const auto onCleanupDone = [] {
|
const auto onCleanupDone = [] {
|
||||||
qCDebug(androidRunWorkerLog) << "Debug server cleanup failed.";
|
qCDebug(androidRunWorkerLog) << "Debug server cleanup failed.";
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onServerUploadSetup = [this, tempDebugServerPathStorage](Process &process) {
|
const auto onServerUploadSetup = [storage, tempDebugServerPathStorage](Process &process) {
|
||||||
process.setCommand(
|
process.setCommand(
|
||||||
adbCommand({"push", m_debugServerPath.toString(), *tempDebugServerPathStorage}));
|
storage->adbCommand({"push", storage->m_debugServerPath.toString(), *tempDebugServerPathStorage}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onServerCopySetup = [this, tempDebugServerPathStorage, debugServerFileName](Process &process) {
|
const auto onServerCopySetup = [storage, tempDebugServerPathStorage, debugServerFileName](Process &process) {
|
||||||
process.setCommand(adbCommand({packageArgs(), "cp", *tempDebugServerPathStorage,
|
process.setCommand(storage->adbCommand({storage->packageArgs(), "cp",
|
||||||
debugServerFileName}));
|
*tempDebugServerPathStorage, debugServerFileName}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onServerChmodSetup = [this, debugServerFileName](Process &process) {
|
const auto onServerChmodSetup = [storage, debugServerFileName](Process &process) {
|
||||||
process.setCommand(adbCommand({packageArgs(), "chmod", "777", debugServerFileName}));
|
process.setCommand(storage->adbCommand({storage->packageArgs(), "chmod", "777", debugServerFileName}));
|
||||||
};
|
};
|
||||||
|
|
||||||
return Group {
|
return Group {
|
||||||
@@ -724,64 +692,64 @@ ExecutableItem AndroidRunnerWorker::uploadDebugServerRecipe(const QString &debug
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ExecutableItem AndroidRunnerWorker::startNativeDebuggingRecipe()
|
static ExecutableItem startNativeDebuggingRecipe(RunnerStorage *storage)
|
||||||
{
|
{
|
||||||
const auto onSetup = [this] {
|
const auto onSetup = [storage] {
|
||||||
return m_useCppDebugger ? SetupResult::Continue : SetupResult::StopWithSuccess;
|
return storage->m_useCppDebugger ? SetupResult::Continue : SetupResult::StopWithSuccess;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Storage<QString> packageDirStorage;
|
const Storage<QString> packageDirStorage;
|
||||||
const Storage<QString> debugServerFileStorage;
|
const Storage<QString> debugServerFileStorage;
|
||||||
|
|
||||||
const auto onAppDirSetup = [this](Process &process) {
|
const auto onAppDirSetup = [storage](Process &process) {
|
||||||
process.setCommand(adbCommand({packageArgs(), "/system/bin/sh", "-c", "pwd"}));
|
process.setCommand(storage->adbCommand({storage->packageArgs(), "/system/bin/sh", "-c", "pwd"}));
|
||||||
};
|
};
|
||||||
const auto onAppDirDone = [this, packageDirStorage](const Process &process, DoneWith result) {
|
const auto onAppDirDone = [storage, packageDirStorage](const Process &process, DoneWith result) {
|
||||||
if (result == DoneWith::Success)
|
if (result == DoneWith::Success)
|
||||||
*packageDirStorage = process.stdOut();
|
*packageDirStorage = process.stdOut();
|
||||||
else
|
else
|
||||||
emit remoteProcessFinished(Tr::tr("Failed to find application directory."));
|
emit storage->remoteProcessFinished(Tr::tr("Failed to find application directory."));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add executable flag to package dir. Gdb can't connect to running server on device on
|
// Add executable flag to package dir. Gdb can't connect to running server on device on
|
||||||
// e.g. on Android 8 with NDK 10e
|
// e.g. on Android 8 with NDK 10e
|
||||||
const auto onChmodSetup = [this, packageDirStorage](Process &process) {
|
const auto onChmodSetup = [storage, packageDirStorage](Process &process) {
|
||||||
process.setCommand(adbCommand({packageArgs(), "chmod", "a+x", packageDirStorage->trimmed()}));
|
process.setCommand(storage->adbCommand({storage->packageArgs(), "chmod", "a+x", packageDirStorage->trimmed()}));
|
||||||
};
|
};
|
||||||
const auto onServerPathCheck = [this] {
|
const auto onServerPathCheck = [storage] {
|
||||||
if (m_debugServerPath.exists())
|
if (storage->m_debugServerPath.exists())
|
||||||
return true;
|
return true;
|
||||||
QString msg = Tr::tr("Cannot find C++ debug server in NDK installation.");
|
QString msg = Tr::tr("Cannot find C++ debug server in NDK installation.");
|
||||||
if (m_useLldb)
|
if (storage->m_useLldb)
|
||||||
msg += "\n" + Tr::tr("The lldb-server binary has not been found.");
|
msg += "\n" + Tr::tr("The lldb-server binary has not been found.");
|
||||||
emit remoteProcessFinished(msg);
|
emit storage->remoteProcessFinished(msg);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto useLldb = Sync([this] { return m_useLldb; });
|
const auto useLldb = Sync([storage] { return storage->m_useLldb; });
|
||||||
const auto killAll = [this](const QString &name) {
|
const auto killAll = [storage](const QString &name) {
|
||||||
return ProcessTask([this, name](Process &process) {
|
return ProcessTask([storage, name](Process &process) {
|
||||||
process.setCommand(adbCommand({packageArgs(), "killall", name}));
|
process.setCommand(storage->adbCommand({storage->packageArgs(), "killall", name}));
|
||||||
}) || successItem;
|
}) || successItem;
|
||||||
};
|
};
|
||||||
const auto setDebugServer = [debugServerFileStorage](const QString &fileName) {
|
const auto setDebugServer = [debugServerFileStorage](const QString &fileName) {
|
||||||
return Sync([debugServerFileStorage, fileName] { *debugServerFileStorage = fileName; });
|
return Sync([debugServerFileStorage, fileName] { *debugServerFileStorage = fileName; });
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto uploadDebugServer = [this, setDebugServer](const QString &debugServerFileName) {
|
const auto uploadDebugServer = [storage, setDebugServer](const QString &debugServerFileName) {
|
||||||
return If (uploadDebugServerRecipe(debugServerFileName)) >> Then {
|
return If (uploadDebugServerRecipe(storage, debugServerFileName)) >> Then {
|
||||||
setDebugServer(debugServerFileName)
|
setDebugServer(debugServerFileName)
|
||||||
} >> Else {
|
} >> Else {
|
||||||
Sync([this] {
|
Sync([storage] {
|
||||||
emit remoteProcessFinished(Tr::tr("Cannot copy C++ debug server."));
|
emit storage->remoteProcessFinished(Tr::tr("Cannot copy C++ debug server."));
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto packageFileExists = [this](const QString &filePath) {
|
const auto packageFileExists = [storage](const QString &filePath) {
|
||||||
const auto onProcessSetup = [this, filePath](Process &process) {
|
const auto onProcessSetup = [storage, filePath](Process &process) {
|
||||||
process.setCommand(adbCommand({packageArgs(), "ls", filePath, "2>/dev/null"}));
|
process.setCommand(storage->adbCommand({storage->packageArgs(), "ls", filePath, "2>/dev/null"}));
|
||||||
};
|
};
|
||||||
const auto onProcessDone = [](const Process &process) {
|
const auto onProcessDone = [](const Process &process) {
|
||||||
return !process.stdOut().trimmed().isEmpty();
|
return !process.stdOut().trimmed().isEmpty();
|
||||||
@@ -789,25 +757,25 @@ ExecutableItem AndroidRunnerWorker::startNativeDebuggingRecipe()
|
|||||||
return ProcessTask(onProcessSetup, onProcessDone, CallDoneIf::Success);
|
return ProcessTask(onProcessSetup, onProcessDone, CallDoneIf::Success);
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onRemoveGdbServerSetup = [this, packageDirStorage](Process &process) {
|
const auto onRemoveGdbServerSetup = [storage, packageDirStorage](Process &process) {
|
||||||
const QString gdbServerSocket = *packageDirStorage + "/debug-socket";
|
const QString gdbServerSocket = *packageDirStorage + "/debug-socket";
|
||||||
process.setCommand(adbCommand({packageArgs(), "rm", gdbServerSocket}));
|
process.setCommand(storage->adbCommand({storage->packageArgs(), "rm", gdbServerSocket}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onDebugServerSetup = [this, packageDirStorage, debugServerFileStorage](Process &process) {
|
const auto onDebugServerSetup = [storage, packageDirStorage, debugServerFileStorage](Process &process) {
|
||||||
if (m_useLldb) {
|
if (storage->m_useLldb) {
|
||||||
process.setCommand(adbCommand({packageArgs(), *debugServerFileStorage, "platform",
|
process.setCommand(storage->adbCommand({storage->packageArgs(), *debugServerFileStorage, "platform",
|
||||||
"--listen", QString("*:%1").arg(s_localDebugServerPort.toString())}));
|
"--listen", QString("*:%1").arg(s_localDebugServerPort.toString())}));
|
||||||
} else {
|
} else {
|
||||||
const QString gdbServerSocket = *packageDirStorage + "/debug-socket";
|
const QString gdbServerSocket = *packageDirStorage + "/debug-socket";
|
||||||
process.setCommand(adbCommand({packageArgs(), *debugServerFileStorage, "--multi",
|
process.setCommand(storage->adbCommand({storage->packageArgs(), *debugServerFileStorage, "--multi",
|
||||||
QString("+%1").arg(gdbServerSocket)}));
|
QString("+%1").arg(gdbServerSocket)}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto onTaskTreeSetup = [this, packageDirStorage](TaskTree &taskTree) {
|
const auto onTaskTreeSetup = [storage, packageDirStorage](TaskTree &taskTree) {
|
||||||
const QString gdbServerSocket = *packageDirStorage + "/debug-socket";
|
const QString gdbServerSocket = *packageDirStorage + "/debug-socket";
|
||||||
taskTree.setRecipe({removeForwardPortRecipe("tcp:" + s_localDebugServerPort.toString(),
|
taskTree.setRecipe({removeForwardPortRecipe(storage, "tcp:" + s_localDebugServerPort.toString(),
|
||||||
"localfilesystem:" + gdbServerSocket, "C++")});
|
"localfilesystem:" + gdbServerSocket, "C++")});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -844,28 +812,117 @@ ExecutableItem AndroidRunnerWorker::startNativeDebuggingRecipe()
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void AndroidRunnerWorker::asyncStart()
|
static ExecutableItem pidRecipe(RunnerStorage *storage)
|
||||||
{
|
{
|
||||||
const Group recipe {
|
using PidUserPair = std::pair<qint64, qint64>;
|
||||||
forceStopRecipe(),
|
const Storage<PidUserPair> pidStorage;
|
||||||
Group {
|
|
||||||
parallel,
|
const QString pidScript = storage->isPreNougat()
|
||||||
logcatRecipe(),
|
? QString("for p in /proc/[0-9]*; do cat <$p/cmdline && echo :${p##*/}; done")
|
||||||
Group {
|
: QString("pidof -s '%1'").arg(storage->m_packageName);
|
||||||
preStartRecipe(),
|
|
||||||
pidRecipe()
|
const auto onPidSetup = [storage, pidScript](Process &process) {
|
||||||
|
process.setCommand(storage->adbCommand({"shell", pidScript}));
|
||||||
|
};
|
||||||
|
const auto onPidDone = [pidStorage, packageName = storage->m_packageName,
|
||||||
|
isPreNougat = storage->isPreNougat()](const Process &process) {
|
||||||
|
const QString out = process.allOutput();
|
||||||
|
if (isPreNougat)
|
||||||
|
pidStorage->first = extractPID(out, packageName);
|
||||||
|
else if (!out.isEmpty())
|
||||||
|
pidStorage->first = out.trimmed().toLongLong();
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto onUserSetup = [storage, pidStorage](Process &process) {
|
||||||
|
process.setCommand(
|
||||||
|
storage->adbCommand({"shell", "ps", "-o", "user", "-p", QString::number(pidStorage->first)}));
|
||||||
|
};
|
||||||
|
const auto onUserDone = [pidStorage](const Process &process) {
|
||||||
|
const QString out = process.allOutput();
|
||||||
|
if (out.isEmpty())
|
||||||
|
return DoneResult::Error;
|
||||||
|
|
||||||
|
QRegularExpressionMatch match;
|
||||||
|
qsizetype matchPos = out.indexOf(userIdPattern, 0, &match);
|
||||||
|
if (matchPos >= 0 && match.capturedLength(1) > 0) {
|
||||||
|
bool ok = false;
|
||||||
|
const qint64 processUser = match.captured(1).toInt(&ok);
|
||||||
|
if (ok) {
|
||||||
|
pidStorage->second = processUser;
|
||||||
|
return DoneResult::Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return DoneResult::Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto onPidSync = [storage, pidStorage] {
|
||||||
|
qCDebug(androidRunWorkerLog) << "Process ID changed from:" << storage->m_processPID
|
||||||
|
<< "to:" << pidStorage->first;
|
||||||
|
storage->m_processPID = pidStorage->first;
|
||||||
|
storage->m_processUser = pidStorage->second;
|
||||||
|
emit storage->remoteProcessStarted(s_localDebugServerPort, storage->m_qmlServer, storage->m_processPID);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto onIsAliveSetup = [storage, pidStorage](Process &process) {
|
||||||
|
process.setProcessChannelMode(QProcess::MergedChannels);
|
||||||
|
process.setCommand(storage->adbCommand({"shell", pidPollingScript.arg(pidStorage->first)}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return Group {
|
||||||
|
pidStorage,
|
||||||
|
onGroupSetup([pidStorage] { *pidStorage = {-1, 0}; }),
|
||||||
|
Forever {
|
||||||
|
stopOnSuccess,
|
||||||
|
ProcessTask(onPidSetup, onPidDone, CallDoneIf::Success),
|
||||||
|
TimeoutTask([](std::chrono::milliseconds &timeout) { timeout = 200ms; },
|
||||||
|
DoneResult::Error)
|
||||||
|
}.withTimeout(45s),
|
||||||
|
ProcessTask(onUserSetup, onUserDone, CallDoneIf::Success),
|
||||||
|
Sync(onPidSync),
|
||||||
|
Group {
|
||||||
|
parallel,
|
||||||
|
startNativeDebuggingRecipe(storage),
|
||||||
|
ProcessTask(onIsAliveSetup)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidRunnerWorker::~AndroidRunnerWorker()
|
||||||
|
{
|
||||||
|
if (m_storage->m_processPID != -1)
|
||||||
|
TaskTree::runBlocking(Group { forceStopRecipe(m_storage.get()), postDoneRecipe(m_storage.get()) });
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidRunnerWorker::asyncStart()
|
||||||
|
{
|
||||||
|
// TODO: Instead of asyncStop recipe, add a barrier storage.
|
||||||
|
const Group recipe {
|
||||||
|
finishAllAndSuccess,
|
||||||
|
Group {
|
||||||
|
forceStopRecipe(m_storage.get()),
|
||||||
|
Group {
|
||||||
|
parallel,
|
||||||
|
stopOnSuccessOrError,
|
||||||
|
logcatRecipe(m_storage.get()),
|
||||||
|
Group {
|
||||||
|
preStartRecipe(m_storage.get()),
|
||||||
|
pidRecipe(m_storage.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.withCancel([storage = m_storage.get()] {
|
||||||
|
return std::make_pair(storage, &RunnerStorage::cancel);
|
||||||
|
}),
|
||||||
|
forceStopRecipe(m_storage.get()),
|
||||||
|
postDoneRecipe(m_storage.get())
|
||||||
};
|
};
|
||||||
m_taskTreeRunner.start(recipe);
|
m_taskTreeRunner.start(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AndroidRunnerWorker::asyncStop()
|
void AndroidRunnerWorker::asyncStop()
|
||||||
{
|
{
|
||||||
if (m_processPID != -1)
|
emit cancel();
|
||||||
m_taskTreeRunner.start(Group { forceStopRecipe(), postDoneRecipe() });
|
|
||||||
else
|
|
||||||
m_taskTreeRunner.reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Android::Internal
|
} // namespace Android::Internal
|
||||||
|
|
||||||
|
#include "androidrunnerworker.moc"
|
||||||
|
@@ -3,23 +3,19 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <qmldebug/qmldebugcommandlinearguments.h>
|
|
||||||
|
|
||||||
#include <solutions/tasking/barrier.h>
|
|
||||||
#include <solutions/tasking/tasktreerunner.h>
|
#include <solutions/tasking/tasktreerunner.h>
|
||||||
|
|
||||||
#include <utils/commandline.h>
|
|
||||||
#include <utils/environment.h>
|
|
||||||
|
|
||||||
namespace Android { class AndroidDeviceInfo; }
|
|
||||||
namespace ProjectExplorer { class RunControl; }
|
namespace ProjectExplorer { class RunControl; }
|
||||||
namespace Utils { class Port; }
|
namespace Utils { class Port; }
|
||||||
|
|
||||||
namespace Android::Internal {
|
namespace Android::Internal {
|
||||||
|
|
||||||
|
class RunnerStorage;
|
||||||
|
|
||||||
class AndroidRunnerWorker : public QObject
|
class AndroidRunnerWorker : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AndroidRunnerWorker(ProjectExplorer::RunControl *runControl, const QString &deviceSerialNumber,
|
AndroidRunnerWorker(ProjectExplorer::RunControl *runControl, const QString &deviceSerialNumber,
|
||||||
int apiLevel);
|
int apiLevel);
|
||||||
@@ -35,46 +31,11 @@ signals:
|
|||||||
void remoteOutput(const QString &output);
|
void remoteOutput(const QString &output);
|
||||||
void remoteErrorOutput(const QString &output);
|
void remoteErrorOutput(const QString &output);
|
||||||
|
|
||||||
|
void cancel();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QStringList selector() const;
|
std::unique_ptr<RunnerStorage> m_storage;
|
||||||
|
|
||||||
bool isPreNougat() const { return m_apiLevel > 0 && m_apiLevel <= 23; }
|
|
||||||
|
|
||||||
Utils::CommandLine adbCommand(std::initializer_list<Utils::CommandLine::ArgRef> args) const;
|
|
||||||
QStringList userArgs() const;
|
|
||||||
QStringList packageArgs() const;
|
|
||||||
|
|
||||||
Tasking::ExecutableItem forceStopRecipe();
|
|
||||||
Tasking::ExecutableItem removeForwardPortRecipe(const QString &port, const QString &adbArg,
|
|
||||||
const QString &portType);
|
|
||||||
Tasking::ExecutableItem jdbRecipe(const Tasking::SingleBarrier &startBarrier,
|
|
||||||
const Tasking::SingleBarrier &settledBarrier);
|
|
||||||
Tasking::ExecutableItem logcatRecipe();
|
|
||||||
Tasking::ExecutableItem preStartRecipe();
|
|
||||||
Tasking::ExecutableItem postDoneRecipe();
|
|
||||||
Tasking::ExecutableItem pidRecipe();
|
|
||||||
Tasking::ExecutableItem uploadDebugServerRecipe(const QString &debugServerFileName);
|
|
||||||
Tasking::ExecutableItem startNativeDebuggingRecipe();
|
|
||||||
|
|
||||||
// Create the processes and timer in the worker thread, for correct thread affinity
|
|
||||||
QString m_packageName;
|
|
||||||
QString m_intentName;
|
|
||||||
QStringList m_beforeStartAdbCommands;
|
|
||||||
QStringList m_afterFinishAdbCommands;
|
|
||||||
QStringList m_amStartExtraArgs;
|
|
||||||
qint64 m_processPID = -1;
|
|
||||||
qint64 m_processUser = -1;
|
|
||||||
Tasking::TaskTreeRunner m_taskTreeRunner;
|
Tasking::TaskTreeRunner m_taskTreeRunner;
|
||||||
bool m_useCppDebugger = false;
|
|
||||||
bool m_useLldb = false; // FIXME: Un-implemented currently.
|
|
||||||
QmlDebug::QmlDebugServicesPreset m_qmlDebugServices;
|
|
||||||
QUrl m_qmlServer;
|
|
||||||
QString m_deviceSerialNumber;
|
|
||||||
int m_apiLevel = -1;
|
|
||||||
QString m_extraAppParams;
|
|
||||||
Utils::Environment m_extraEnvVars;
|
|
||||||
Utils::FilePath m_debugServerPath; // On build device, typically as part of ndk
|
|
||||||
bool m_useAppParamsForQmlDebugger = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Android::Internal
|
} // namespace Android::Internal
|
||||||
|
Reference in New Issue
Block a user