/**************************************************************************** ** ** Copyright (C) 2021 The Qt Company Ltd. ** Copyright (C) 2016 BogDan Vatra ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "androiddevice.h" #include "androidavdmanager.h" #include "androidconfigurations.h" #include "androidconstants.h" #include "androidmanager.h" #include "androidsignaloperation.h" #include "avddialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ProjectExplorer; 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 { class AndroidDeviceWidget : public IDeviceWidget { public: AndroidDeviceWidget(const ProjectExplorer::IDevice::Ptr &device); void updateDeviceFromUi() final {} static QString dialogTitle(); static bool criticalDialog(const QString &error); static bool questionDialog(const QString &question); }; AndroidDeviceWidget::AndroidDeviceWidget(const IDevice::Ptr &device) : IDeviceWidget(device) { const auto dev = qSharedPointerCast(device); const auto formLayout = new QFormLayout(this); formLayout->setFormAlignment(Qt::AlignLeft); formLayout->setContentsMargins(0, 0, 0, 0); setLayout(formLayout); formLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow); if (!dev->isValid()) return; formLayout->addRow(AndroidDevice::tr("Device name:"), new QLabel(dev->displayName())); formLayout->addRow(AndroidDevice::tr("Device type:"), new QLabel(dev->deviceTypeName())); const QString serialNumber = dev->serialNumber(); const QString printableSerialNumber = serialNumber.isEmpty() ? tr("Unknown") : serialNumber; formLayout->addRow(AndroidDevice::tr("Serial number:"), new QLabel(printableSerialNumber)); const QString abis = dev->supportedAbis().join(", "); formLayout->addRow(AndroidDevice::tr("CPU architecture:"), new QLabel(abis)); const auto osString = QString("%1 (SDK %2)").arg(dev->androidVersion()).arg(dev->sdkLevel()); formLayout->addRow(AndroidDevice::tr("OS version:"), new QLabel(osString)); if (dev->machineType() == IDevice::Emulator) { const QString targetName = dev->androidTargetName(); formLayout->addRow(AndroidDevice::tr("Android target flavor:"), new QLabel(targetName)); formLayout->addRow(AndroidDevice::tr("SD card size:"), new QLabel(dev->sdcardSize())); formLayout->addRow(AndroidDevice::tr("Skin type:"), new QLabel(dev->skinName())); const QString openGlStatus = dev->openGlStatusString(); formLayout->addRow(AndroidDevice::tr("OpenGL status:"), new QLabel(openGlStatus)); } } QString AndroidDeviceWidget::dialogTitle() { return tr("Android Device Manager"); } bool AndroidDeviceWidget::criticalDialog(const QString &error) { qCDebug(androidDeviceLog) << error; QMessageBox box(Core::ICore::dialogParent()); box.QDialog::setWindowTitle(dialogTitle()); box.setText(error); box.setIcon(QMessageBox::Critical); box.setWindowFlag(Qt::WindowTitleHint); return box.exec(); } bool AndroidDeviceWidget::questionDialog(const QString &question) { QMessageBox box(Core::ICore::dialogParent()); box.QDialog::setWindowTitle(dialogTitle()); box.setText(question); box.setIcon(QMessageBox::Question); QPushButton *YesButton = box.addButton(QMessageBox::Yes); box.addButton(QMessageBox::No); box.setWindowFlag(Qt::WindowTitleHint); box.exec(); if (box.clickedButton() == YesButton) return true; return false; } AndroidDevice::AndroidDevice() { setupId(IDevice::AutoDetected, Constants::ANDROID_DEVICE_ID); setType(Constants::ANDROID_DEVICE_TYPE); setDefaultDisplayName(tr("Run on Android")); setDisplayType(tr("Android")); setMachineType(IDevice::Hardware); setOsType(Utils::OsTypeOtherUnix); setDeviceState(DeviceConnected); addDeviceAction({tr("Refresh"), [](const IDevice::Ptr &device, QWidget *parent) { AndroidDeviceManager::instance()->updateDevicesListOnce(); }}); addDeviceAction({tr("Start AVD"), [](const IDevice::Ptr &device, QWidget *parent) { if (device->machineType() == IDevice::Emulator) AndroidDeviceManager::instance()->startAvd(device); }}); addDeviceAction({tr("Erase AVD"), [](const IDevice::Ptr &device, QWidget *parent) { if (device->machineType() == IDevice::Emulator) AndroidDeviceManager::instance()->eraseAvd(device); }}); addDeviceAction({tr("AVD Arguments"), [](const IDevice::Ptr &device, QWidget *parent) { AndroidDeviceManager::instance()->setEmulatorArguments(); }}); } IDevice::Ptr AndroidDevice::create() { return IDevice::Ptr(new AndroidDevice); } AndroidDeviceInfo AndroidDevice::androidDeviceInfoFromIDevice(const IDevice *dev) { AndroidDeviceInfo info; AndroidDeviceInfo::State state; if (dev->deviceState() == IDevice::DeviceReadyToUse) state = AndroidDeviceInfo::OkState; else if (dev->deviceState() == IDevice::DeviceDisconnected) state = AndroidDeviceInfo::OfflineState; else if (dev->deviceState() == IDevice::DeviceConnected) state = AndroidDeviceInfo::UnAuthorizedState; info.state = state; info.avdname = dev->extraData(Constants::AndroidAvdName).toString(); info.serialNumber = dev->extraData(Constants::AndroidSerialNumber).toString(); info.cpuAbi = dev->extraData(Constants::AndroidCpuAbi).toStringList(); info.avdTarget = dev->extraData(Constants::AndroidAvdTarget).toString(); info.avdDevice = dev->extraData(Constants::AndroidAvdDevice).toString(); info.avdSkin = dev->extraData(Constants::AndroidAvdSkin).toString(); info.avdSdcardSize = dev->extraData(Constants::AndroidAvdSdcard).toString(); info.sdk = dev->extraData(Constants::AndroidSdk).toInt(); info.type = (dev->machineType() == ProjectExplorer::IDevice::Hardware ? AndroidDeviceInfo::Hardware : AndroidDeviceInfo::Emulator); return info; } void AndroidDevice::setAndroidDeviceInfoExtras(IDevice *dev, const AndroidDeviceInfo &info) { dev->setMachineType(info.type == AndroidDeviceInfo::Hardware ? ProjectExplorer::IDevice::Hardware : ProjectExplorer::IDevice::Emulator); dev->setDeviceState(deviceStateFromInfo(info.state)); dev->setExtraData(Constants::AndroidAvdName, info.avdname); dev->setExtraData(Constants::AndroidSerialNumber, info.serialNumber); dev->setExtraData(Constants::AndroidCpuAbi, info.cpuAbi); dev->setExtraData(Constants::AndroidAvdTarget, info.avdTarget); dev->setExtraData(Constants::AndroidAvdDevice, info.avdDevice); dev->setExtraData(Constants::AndroidAvdSkin, info.avdSkin); dev->setExtraData(Constants::AndroidAvdSdcard, info.avdSdcardSize); dev->setExtraData(Constants::AndroidSdk, info.sdk); } QString AndroidDevice::displayNameFromInfo(const AndroidDeviceInfo &info) { return info.type == AndroidDeviceInfo::Hardware ? AndroidConfigurations::currentConfig().getProductModel(info.serialNumber) : info.avdname; } Utils::Id AndroidDevice::idFromDeviceInfo(const AndroidDeviceInfo &info) { const QString id = (info.type == AndroidDeviceInfo::Hardware ? info.serialNumber : info.avdname); return Utils::Id(Constants::ANDROID_DEVICE_ID).withSuffix(':' + id); } Utils::Id AndroidDevice::idFromAvdInfo(const CreateAvdInfo &info) { return Utils::Id(Constants::ANDROID_DEVICE_ID).withSuffix(':' + info.name); } IDevice::DeviceState AndroidDevice::deviceStateFromInfo(AndroidDeviceInfo::State state) { if (state == AndroidDeviceInfo::OkState) return IDevice::DeviceReadyToUse; if (state == AndroidDeviceInfo::OfflineState) return IDevice::DeviceDisconnected; return IDevice::DeviceConnected; } QStringList AndroidDevice::supportedAbis() const { return extraData(Constants::AndroidCpuAbi).toStringList(); } bool AndroidDevice::canSupportAbis(const QStringList &abis) const { // If the list is empty, no valid decision can be made, this means something is wrong // somewhere, but let's not stop deployment. QTC_ASSERT(!abis.isEmpty(), return true); const QStringList ourAbis = supportedAbis(); QTC_ASSERT(!ourAbis.isEmpty(), return false); for (const QString &abi : abis) if (ourAbis.contains(abi)) return true; // it's enough if only one abi match is found // If no exact match is found, let's take ABI backward compatibility into account // https://developer.android.com/ndk/guides/abis#android-platform-abi-support // arm64 usually can run {arm, armv7}, x86 can support {arm, armv7}, and 64-bit devices // can support their 32-bit variants. using namespace ProjectExplorer::Constants; const bool isTheirsArm = abis.contains(ANDROID_ABI_ARMEABI_V7A) || abis.contains(ANDROID_ABI_ARMEABI_V7A); // The primary ABI at the first index const bool oursSupportsArm = ourAbis.first() == ANDROID_ABI_ARM64_V8A || ourAbis.first() == ANDROID_ABI_X86; // arm64 and x86 can run armv7 and arm if (isTheirsArm && oursSupportsArm) return true; // x64 can run x86 if (ourAbis.first() == ANDROID_ABI_X86_64 && abis.contains(ANDROID_ABI_X86)) return true; return false; } bool AndroidDevice::canHandleDeployments() const { // If hardware and disconned, it wouldn't be possilbe to start it, unlike an emulator if (machineType() == Hardware && deviceState() == DeviceDisconnected) return false; return true; } bool AndroidDevice::isValid() const { return !serialNumber().isEmpty() || !avdName().isEmpty(); } QString AndroidDevice::serialNumber() const { return extraData(Constants::AndroidSerialNumber).toString(); } QString AndroidDevice::avdName() const { return extraData(Constants::AndroidAvdName).toString(); } int AndroidDevice::sdkLevel() const { return extraData(Constants::AndroidSdk).toInt(); } QString AndroidDevice::androidVersion() const { return AndroidManager::androidNameForApiLevel(sdkLevel()); } QString AndroidDevice::deviceTypeName() const { if (machineType() == Emulator) return tr("Emulator for ") + extraData(Constants::AndroidAvdDevice).toString(); return tr("Physical device"); } QString AndroidDevice::skinName() const { const QString skin = extraData(Constants::AndroidAvdSkin).toString(); return skin.isEmpty() ? tr("None") : skin; } QString AndroidDevice::androidTargetName() const { const QString target = extraData(Constants::AndroidAvdTarget).toString(); return target.isEmpty() ? tr("Unknown") : target; } QString AndroidDevice::sdcardSize() const { const QString size = extraData(Constants::AndroidAvdSdcard).toString(); return size.isEmpty() ? tr("Unknown") : size; } AndroidConfig::OpenGl AndroidDevice::openGlStatus() const { return AndroidConfigurations::currentConfig().getOpenGLEnabled(displayName()); } QString AndroidDevice::openGlStatusString() const { const AndroidConfig::OpenGl glStatus = AndroidConfigurations::currentConfig() .getOpenGLEnabled(displayName()); switch (glStatus) { case (AndroidConfig::OpenGl::Enabled): return tr("Enabled"); case (AndroidConfig::OpenGl::Disabled): return tr("Disabled"); case (AndroidConfig::OpenGl::Unknown): return tr("Unknown"); } return tr("Unknown"); } IDevice::DeviceInfo AndroidDevice::deviceInformation() const { return IDevice::DeviceInfo(); } IDeviceWidget *AndroidDevice::createWidget() { return new AndroidDeviceWidget(sharedFromThis()); } bool AndroidDevice::canAutoDetectPorts() const { return true; } DeviceProcessSignalOperation::Ptr AndroidDevice::signalOperation() const { return DeviceProcessSignalOperation::Ptr(new AndroidSignalOperation()); } QUrl AndroidDevice::toolControlChannel(const ControlChannelHint &) const { QUrl url; url.setScheme(Utils::urlTcpScheme()); url.setHost("localhost"); return url; } void AndroidDeviceManager::updateDevicesList() { connect(&m_devicesUpdaterTimer, &QTimer::timeout, this, [this]() { updateDevicesListOnce(); }); updateDevicesListOnce(); m_devicesUpdaterTimer.start(deviceUpdaterMsInterval); } void AndroidDeviceManager::updateDevicesListOnce() { if (!m_avdsFutureWatcher.isRunning() && m_androidConfig.adbToolPath().exists()) { m_avdsFutureWatcher.setFuture((new AndroidAvdManager)->avdList()); m_devicesFutureWatcher.setFuture(Utils::runAsync([this]() { return m_androidConfig.connectedDevices(); })); } } void AndroidDeviceManager::startAvd(const ProjectExplorer::IDevice::Ptr &device) { const AndroidDevice *androidDev = static_cast(device.data()); const QString name = androidDev->avdName(); qCDebug(androidDeviceLog, "Starting Android AVD id \"%s\".", qPrintable(name)); Utils::runAsync([this, name, device]() { const QString serialNumber = m_avdManager.startAvd(name); // Mark the AVD as ReadyToUse once we know it's started if (!serialNumber.isEmpty()) { DeviceManager *const devMgr = DeviceManager::instance(); devMgr->setDeviceState(device->id(), IDevice::DeviceReadyToUse); } }); } void AndroidDeviceManager::eraseAvd(const IDevice::Ptr &device) { if (device.isNull()) return; if (device->machineType() == IDevice::Hardware) return; const QString name = static_cast(device.data())->avdName(); const QString question = tr("Erase the Android AVD \"%1\"?\nThis cannot be undone.").arg(name); if (!AndroidDeviceWidget::questionDialog(question)) return; qCDebug(androidDeviceLog) << QString("Erasing Android AVD \"%1\" from the system.").arg(name); m_removeAvdFutureWatcher.setFuture(Utils::runAsync([this, name, device]() { QPair pair; pair.first = device; pair.second = false; if (m_avdManager.removeAvd(name)) pair.second = true; return pair; })); } void AndroidDeviceManager::handleAvdRemoved() { const QPair result = m_removeAvdFutureWatcher.result(); const QString name = result.first->displayName(); if (result.second) { qCDebug(androidDeviceLog, "Android AVD id \"%s\" removed from the system.", qPrintable(name)); // Remove the device from QtC after it's been removed using avdmanager. DeviceManager::instance()->removeDevice(result.first->id()); } else { AndroidDeviceWidget::criticalDialog(QObject::tr("An error occurred while removing the " "Android AVD \"%1\" using avdmanager tool.").arg(name)); } } void AndroidDeviceManager::setEmulatorArguments() { const QString helpUrl = "https://developer.android.com/studio/run/emulator-commandline#startup-options"; QInputDialog dialog(Core::ICore::dialogParent()); dialog.setWindowTitle(tr("Emulator Command-line Startup Options")); dialog.setLabelText(tr("Emulator command-line startup options " "(Help Web Page):").arg(helpUrl)); dialog.setTextValue(m_androidConfig.emulatorArgs().join(' ')); if (auto label = dialog.findChild()) { label->setOpenExternalLinks(true); label->setMinimumWidth(500); } if (dialog.exec() != QDialog::Accepted) return; m_androidConfig.setEmulatorArgs(Utils::ProcessArgs::splitArgs(dialog.textValue())); } void AndroidDeviceManager::setupDevicesWatcher() { // 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); updateDevicesList(); } void AndroidDeviceManager::devicesListUpdated() { QVector 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 startedAvds = Utils::transform(connectedDevicesInfos, &AndroidDeviceInfo::avdname); for (const AndroidDeviceInfo &dev : devices) if (!startedAvds.contains(dev.avdname)) connectedDevicesInfos << dev; DeviceManager *const devMgr = DeviceManager::instance(); QVector existingDevs; QVector 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); } } for (auto item : connectedDevicesInfos) { 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 = AndroidDevice::deviceStateFromInfo(item.state); if (dev->deviceState() != newState) { 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()); } } AndroidDevice *newDev = new AndroidDevice(); newDev->setupId(IDevice::AutoDetected, deviceId); newDev->setDisplayName(displayName); AndroidDevice::setAndroidDeviceInfoExtras(newDev, item); qCDebug(androidDeviceLog, "Registering new Android device id \"%s\".", newDev->id().toString().toUtf8().data()); const IDevice::ConstPtr constNewDev = IDevice::ConstPtr(newDev); devMgr->addDevice(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); } } } AndroidDeviceManager *AndroidDeviceManager::instance() { static AndroidDeviceManager obj; return &obj; } AndroidDeviceManager::AndroidDeviceManager(QObject *parent) : QObject(parent), m_androidConfig(AndroidConfigurations::currentConfig()), m_avdManager(m_androidConfig) { connect(qApp, &QCoreApplication::aboutToQuit, this, [this]() { m_devicesUpdaterTimer.stop(); m_avdsFutureWatcher.waitForFinished(); m_devicesFutureWatcher.waitForFinished(); m_removeAvdFutureWatcher.waitForFinished(); }); connect(&m_removeAvdFutureWatcher, &QFutureWatcherBase::finished, this, &AndroidDeviceManager::handleAvdRemoved); } // Factory AndroidDeviceFactory::AndroidDeviceFactory() : ProjectExplorer::IDeviceFactory(Constants::ANDROID_DEVICE_TYPE), m_androidConfig(AndroidConfigurations::currentConfig()) { setDisplayName(AndroidDevice::tr("Android Virtual Device")); setCombinedIcon(":/android/images/androiddevicesmall.png", ":/android/images/androiddevice.png"); setConstructionFunction(&AndroidDevice::create); setCanCreate(m_androidConfig.sdkToolsOk()); } IDevice::Ptr AndroidDeviceFactory::create() const { AvdDialog dialog = AvdDialog(m_androidConfig, Core::ICore::dialogParent()); if (dialog.exec() != QDialog::Accepted) return ProjectExplorer::IDevice::Ptr(); const ProjectExplorer::IDevice::Ptr dev = dialog.device(); const AndroidDevice *androidDev = static_cast(dev.data()); if (androidDev) { qCDebug(androidDeviceLog, "Created new Android AVD id \"%s\".", qPrintable(androidDev->avdName())); } else { AndroidDeviceWidget::criticalDialog( QObject::tr("The device info returned from AvdDialog is invalid.")); } return IDevice::Ptr(dev); } } // namespace Internal } // namespace Android