Monitor Android devices in non-polling method

Use ADB's track-devices command to watch for device
events which would replace the current polling method
with a timer. For AVDs, a QFileSystemWatcher is used
to watch for changes in the AVDs home folder which
would allow updating the AVDs only when a change is done
like edition, deletion, start, stop, even from outside
Qt Creator.

This method would also make device updates faster,
instead of unexpected waits due to timer use.

Task-number: QTCREATORBUG-23991
Change-Id: I08a92252c99c02bc111e597d671f2350817458c7
Reviewed-by: Alessandro Portale <alessandro.portale@qt.io>
This commit is contained in:
Assam Boudjelthia
2021-10-29 18:19:54 +03:00
parent d8b5d2b7a5
commit c43ba1ae32
4 changed files with 222 additions and 155 deletions

View File

@@ -1,6 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Copyright (C) 2022 The Qt Company Ltd.
** Copyright (C) 2016 BogDan Vatra <bog_dan_ro@yahoo.com>
** Contact: https://www.qt.io/licensing/
**
@@ -52,6 +52,9 @@
#include <QLoggingCategory>
#include <QMessageBox>
#include <QPushButton>
#include <QTimer>
#include <utils/qtcprocess.h>
using namespace ProjectExplorer;
@@ -59,9 +62,6 @@ namespace {
static Q_LOGGING_CATEGORY(androidDeviceLog, "qtc.android.androiddevice", QtWarningMsg)
}
// interval for updating the list of connected Android devices and emulators
constexpr int deviceUpdaterMsInterval = 30000;
namespace Android {
namespace Internal {
@@ -328,7 +328,7 @@ QString AndroidDevice::serialNumber() const
if (machineType() == Hardware)
return serialNumber;
return AndroidConfigurations::currentConfig().getRunningAvdsSerialNumber(avdName());
return AndroidDeviceManager::instance()->getRunningAvdsSerialNumber(avdName());
}
QString AndroidDevice::avdName() const
@@ -419,36 +419,28 @@ QUrl AndroidDevice::toolControlChannel(const ControlChannelHint &) const
return url;
}
void AndroidDeviceManager::updateDevicesList()
void AndroidDeviceManager::updateAvdsList()
{
// If a non-Android Kit is currently active, skip the device list update
const Target *startupTarget = SessionManager::startupTarget();
if (!startupTarget)
return;
const Kit *kit = startupTarget->kit();
if (!kit)
return;
if (DeviceTypeKitAspect::deviceTypeId(kit) != Constants::ANDROID_DEVICE_TYPE)
return;
updateDevicesListOnce();
}
void AndroidDeviceManager::updateDevicesListOnce()
{
if (!m_avdsFutureWatcher.isRunning() && m_androidConfig.adbToolPath().exists()) {
if (!m_avdsFutureWatcher.isRunning() && m_androidConfig.adbToolPath().exists())
m_avdsFutureWatcher.setFuture(m_avdManager.avdList());
m_devicesFutureWatcher.setFuture(Utils::runAsync([this]() {
return m_androidConfig.connectedDevices();
}));
}
}
void AndroidDeviceManager::updateDeviceState(const ProjectExplorer::IDevice::Ptr &device)
IDevice::DeviceState AndroidDeviceManager::getDeviceState(const QString &serial,
IDevice::MachineType type) const
{
const AndroidDevice *dev = static_cast<AndroidDevice *>(device.data());
const QStringList args = AndroidDeviceInfo::adbSelector(serial) << "shell" << "echo 1";
const SdkToolResult result = AndroidManager::runAdbCommand(args);
if (result.success())
return IDevice::DeviceReadyToUse;
else if (type == IDevice::Emulator || result.stdErr().contains("unauthorized"))
return IDevice::DeviceConnected;
return IDevice::DeviceDisconnected;
}
void AndroidDeviceManager::updateDeviceState(const ProjectExplorer::IDevice::ConstPtr &device)
{
const AndroidDevice *dev = static_cast<const AndroidDevice *>(device.data());
const QString serial = dev->serialNumber();
DeviceManager *const devMgr = DeviceManager::instance();
const Utils::Id id = dev->id();
@@ -457,15 +449,7 @@ void AndroidDeviceManager::updateDeviceState(const ProjectExplorer::IDevice::Ptr
return;
}
const QStringList args = AndroidDeviceInfo::adbSelector(serial) << "shell" << "echo" << "1";
const SdkToolResult result = AndroidManager::runAdbCommand(args);
const int success = result.success();
if (success)
devMgr->setDeviceState(id, IDevice::DeviceReadyToUse);
else if (dev->machineType() == IDevice::Emulator || result.stdErr().contains("unauthorized"))
devMgr->setDeviceState(id, IDevice::DeviceConnected);
else
devMgr->setDeviceState(id, IDevice::DeviceDisconnected);
devMgr->setDeviceState(id, getDeviceState(serial, dev->machineType()));
}
void AndroidDeviceManager::startAvd(const ProjectExplorer::IDevice::Ptr &device, QWidget *parent)
@@ -523,6 +507,13 @@ void AndroidDeviceManager::handleAvdRemoved()
}
}
QString AndroidDeviceManager::emulatorName(const QString &serialNumber) const
{
QStringList args = AndroidDeviceInfo::adbSelector(serialNumber);
args.append({"emu", "avd", "name"});
return AndroidManager::runAdbCommand(args).stdOut();
}
void AndroidDeviceManager::setEmulatorArguments(QWidget *parent)
{
const QString helpUrl =
@@ -546,73 +537,121 @@ void AndroidDeviceManager::setEmulatorArguments(QWidget *parent)
m_androidConfig.setEmulatorArgs(Utils::ProcessArgs::splitArgs(dialog.textValue()));
}
void AndroidDeviceManager::setupDevicesWatcher()
QString AndroidDeviceManager::getRunningAvdsSerialNumber(const QString &name) const
{
if (!m_devicesUpdaterTimer.isActive()) {
// The call to avdmanager is always slower than the call to adb devices,
// so connecting the slot to the slower call should be enough.
connect(&m_avdsFutureWatcher, &QFutureWatcherBase::finished,
this, &AndroidDeviceManager::devicesListUpdated);
connect(&m_devicesUpdaterTimer, &QTimer::timeout, this, [this]() {
updateDevicesList();
});
m_devicesUpdaterTimer.start(deviceUpdaterMsInterval);
for (const AndroidDeviceInfo &dev : m_androidConfig.connectedDevices()) {
if (!dev.serialNumber.startsWith("emulator"))
continue;
const QString stdOut = emulatorName(dev.serialNumber);
if (stdOut.isEmpty())
continue; // Not an avd
const QStringList outputLines = stdOut.split('\n');
if (outputLines.size() > 1 && outputLines.first() == name)
return dev.serialNumber;
}
updateDevicesListOnce();
return {};
}
void AndroidDeviceManager::devicesListUpdated()
void AndroidDeviceManager::setupDevicesWatcher()
{
QVector<AndroidDeviceInfo> connectedDevicesInfos;
connectedDevicesInfos = m_devicesFutureWatcher.result();
// For checking the state of avds, since running avds are assigned a serial number of
// the form emulator-xxxx, thus we have to manually check for the names.
const QStringList runningAvds = m_androidConfig.getRunningAvdsFromDevices(connectedDevicesInfos);
AndroidDeviceInfoList devices = m_avdsFutureWatcher.result();
const QSet<QString> startedAvds = Utils::transform<QSet>(connectedDevicesInfos,
&AndroidDeviceInfo::avdname);
for (const AndroidDeviceInfo &dev : devices)
if (!startedAvds.contains(dev.avdname))
connectedDevicesInfos << dev;
DeviceManager *const devMgr = DeviceManager::instance();
QVector<IDevice::ConstPtr> existingDevs;
QVector<IDevice::ConstPtr> connectedDevs;
for (int i = 0; i < devMgr->deviceCount(); ++i) {
const IDevice::ConstPtr dev = devMgr->deviceAt(i);
if (dev->id().toString().startsWith(Constants::ANDROID_DEVICE_ID)) {
existingDevs.append(dev);
}
if (!m_androidConfig.adbToolPath().exists()) {
qCDebug(androidDeviceLog) << "Cannot start ADB device watcher"
<< "because adb path does not exist.";
return;
}
for (auto item : connectedDevicesInfos) {
if (!m_adbDeviceWatcherProcess)
m_adbDeviceWatcherProcess.reset(new Utils::QtcProcess(this));
if (m_adbDeviceWatcherProcess->isRunning()) {
qCDebug(androidDeviceLog) << "ADB device watcher is already running.";
return;
}
connect(m_adbDeviceWatcherProcess.get(), &Utils::QtcProcess::finished, this,
[]() { qCDebug(androidDeviceLog) << "ADB device watcher finished."; });
connect(m_adbDeviceWatcherProcess.get(), &Utils::QtcProcess::errorOccurred, this,
[this](QProcess::ProcessError error) {
qCDebug(androidDeviceLog) << "ADB device watcher encountered an error:"
<< m_adbDeviceWatcherProcess->errorString();
if (!m_adbDeviceWatcherProcess->isRunning()) {
qCDebug(androidDeviceLog) << "Restarting the ADB device watcher now.";
QTimer::singleShot(0, m_adbDeviceWatcherProcess.get(), &Utils::QtcProcess::start);
}
});
m_adbDeviceWatcherProcess->setStdErrLineCallback([](const QString &error) {
qCDebug(androidDeviceLog) << "ADB device watcher error" << error; });
m_adbDeviceWatcherProcess->setStdOutLineCallback([this](const QString &output) {
HandleDevicesListChange(output);
});
const Utils::CommandLine command = Utils::CommandLine(m_androidConfig.adbToolPath(),
{"track-devices"});
m_adbDeviceWatcherProcess->setCommand(command);
m_adbDeviceWatcherProcess->setEnvironment(AndroidConfigurations::toolsEnvironment(m_androidConfig));
m_adbDeviceWatcherProcess->start();
// Setup AVD filesystem watcher to listen for changes when an avd is created/deleted,
// or started/stopped
QString avdEnvVar = qEnvironmentVariable("ANDROID_AVD_HOME");
if (avdEnvVar.isEmpty()) {
avdEnvVar = qEnvironmentVariable("ANDROID_SDK_HOME");
if (avdEnvVar.isEmpty())
avdEnvVar = qEnvironmentVariable("HOME");
avdEnvVar.append("/.android/avd");
}
const Utils::FilePath avdPath = Utils::FilePath::fromUserInput(avdEnvVar);
m_avdFileSystemWatcher.addPath(avdPath.toString());
connect(&m_avdsFutureWatcher, &QFutureWatcherBase::finished,
this, &AndroidDeviceManager::HandleAvdsListChange);
connect(&m_avdFileSystemWatcher, &QFileSystemWatcher::directoryChanged, this, [this]() {
// If the avd list upate command is running no need to call it again.
if (!m_avdsFutureWatcher.isRunning())
updateAvdsList();
});
// Call initial update
updateAvdsList();
}
void AndroidDeviceManager::HandleAvdsListChange()
{
DeviceManager *const devMgr = DeviceManager::instance();
QVector<IDevice::ConstPtr> existingAvds;
for (int i = 0; i < devMgr->deviceCount(); ++i) {
const IDevice::ConstPtr dev = devMgr->deviceAt(i);
const bool isEmulator = dev->machineType() == IDevice::Emulator;
if (isEmulator && dev->type() == Constants::ANDROID_DEVICE_TYPE)
existingAvds.append(dev);
}
QVector<IDevice::ConstPtr> connectedDevs;
for (auto item : m_avdsFutureWatcher.result()) {
const Utils::Id deviceId = AndroidDevice::idFromDeviceInfo(item);
const QString displayName = AndroidDevice::displayNameFromInfo(item);
IDevice::ConstPtr dev = devMgr->find(deviceId);
if (!dev.isNull()) {
if (dev->displayName() == displayName) {
IDevice::DeviceState newState;
// If an AVD is not already running set its state to Connected instead of
// ReadyToUse.
if (dev->machineType() == IDevice::Emulator && !runningAvds.contains(displayName))
newState = IDevice::DeviceConnected;
else
newState = item.state;
if (dev->deviceState() != newState) {
const auto androidDev = static_cast<const AndroidDevice *>(dev.data());
// DeviceManager doens't seem to hav a way to directly update the name, if the name
// of the device has changed, remove it and register it again with the new name.
// Also account for the case of an AVD registered through old QC which might have
// invalid data by checking the sdcard size value.
if (dev->displayName() != displayName || androidDev->sdcardSize() == tr("Unknown")) {
devMgr->removeDevice(dev->id());
} else {
// Find the state of the AVD retrieved from the AVD watcher
const QString serial = getRunningAvdsSerialNumber(item.avdname);
const IDevice::DeviceState state = getDeviceState(serial, IDevice::Emulator);
if (dev->deviceState() != state) {
devMgr->setDeviceState(dev->id(), state);
qCDebug(androidDeviceLog, "Device id \"%s\" changed its state.",
dev->id().toString().toUtf8().data());
devMgr->setDeviceState(dev->id(), newState);
}
connectedDevs.append(dev);
continue;
} else {
// DeviceManager doens't seem to hav a way to directly update the name, if the name
// of the device has changed, remove it and register it again with the new name.
devMgr->removeDevice(dev->id());
}
}
@@ -625,17 +664,79 @@ void AndroidDeviceManager::devicesListUpdated()
qCDebug(androidDeviceLog, "Registering new Android device id \"%s\".",
newDev->id().toString().toUtf8().data());
const IDevice::ConstPtr constNewDev = IDevice::ConstPtr(newDev);
devMgr->addDevice(constNewDev);
devMgr->addDevice(IDevice::ConstPtr(constNewDev));
connectedDevs.append(constNewDev);
}
// Set devices no longer connected to disconnected state.
for (const IDevice::ConstPtr &dev : existingDevs) {
if (dev->id() != Constants::ANDROID_DEVICE_ID && !connectedDevs.contains(dev)
&& dev->deviceState() != IDevice::DeviceDisconnected) {
qCDebug(androidDeviceLog, "Device id \"%s\" is no longer connected.",
dev->id().toString().toUtf8().data());
devMgr->setDeviceState(dev->id(), IDevice::DeviceDisconnected);
// Set devices no longer connected to disconnected state.
for (const IDevice::ConstPtr &dev : existingAvds) {
if (!connectedDevs.contains(dev)) {
qCDebug(androidDeviceLog, "Removing AVD id \"%s\" because it no longer exists.",
dev->id().toString().toUtf8().data());
devMgr->removeDevice(dev->id());
}
}
}
}
void AndroidDeviceManager::HandleDevicesListChange(const QString &serialNumber)
{
DeviceManager *const devMgr = DeviceManager::instance();
const QStringList serialBits = serialNumber.split('\t');
if (serialBits.size() < 2)
return;
// Sample output of adb track-devices, the first 4 digits are for state type
// and sometimes 4 zeros are reported as part for the serial number.
// 00546db0e8d7 authorizing
// 00546db0e8d7 device
// 0000001711201JEC207789 offline
// emulator-5554 device
QString dirtySerial = serialBits.first().trimmed();
if (dirtySerial.startsWith("0000"))
dirtySerial = dirtySerial.mid(4);
if (dirtySerial.startsWith("00"))
dirtySerial = dirtySerial.mid(4);
const bool isEmulator = dirtySerial.startsWith("emulator");
const QString &serial = dirtySerial;
const QString stateStr = serialBits.at(1).trimmed();
IDevice::DeviceState state;
if (stateStr == "device")
state = IDevice::DeviceReadyToUse;
else if (stateStr == "offline")
state = IDevice::DeviceDisconnected;
else
state = IDevice::DeviceConnected;
if (isEmulator) {
const QString avdName = emulatorName(serial);
const Utils::Id avdId = Utils::Id(Constants::ANDROID_DEVICE_ID).withSuffix(':' + avdName);
devMgr->setDeviceState(avdId, state);
} else {
const Utils::Id id = Utils::Id(Constants::ANDROID_DEVICE_ID).withSuffix(':' + serial);
const QString displayName = AndroidConfigurations::currentConfig().getProductModel(serial);
if (IDevice::ConstPtr dev = devMgr->find(id)) {
// DeviceManager doens't seem to have a way to directly update the name, if the name
// of the device has changed, remove it and register it again with the new name.
if (dev->displayName() == displayName)
devMgr->setDeviceState(id, state);
else
devMgr->removeDevice(id);
} else {
AndroidDevice *newDev = new AndroidDevice();
newDev->setupId(IDevice::AutoDetected, id);
newDev->setDisplayName(displayName);
newDev->setMachineType(IDevice::Hardware);
newDev->setDeviceState(state);
newDev->setExtraData(Constants::AndroidSerialNumber, serial);
newDev->setExtraData(Constants::AndroidCpuAbi, m_androidConfig.getAbis(serial));
newDev->setExtraData(Constants::AndroidSdk, m_androidConfig.getSDKVersion(serial));
qCDebug(androidDeviceLog, "Registering new Android device id \"%s\".",
newDev->id().toString().toUtf8().data());
devMgr->addDevice(IDevice::ConstPtr(newDev));
}
}
}
@@ -652,9 +753,10 @@ AndroidDeviceManager::AndroidDeviceManager(QObject *parent)
m_avdManager(m_androidConfig)
{
connect(qApp, &QCoreApplication::aboutToQuit, this, [this]() {
m_devicesUpdaterTimer.stop();
m_adbDeviceWatcherProcess->terminate();
m_adbDeviceWatcherProcess->waitForFinished();
m_adbDeviceWatcherProcess.reset();
m_avdsFutureWatcher.waitForFinished();
m_devicesFutureWatcher.waitForFinished();
m_removeAvdFutureWatcher.waitForFinished();
});