diff options
author | Eike Ziller <eike.ziller@qt.io> | 2017-04-19 09:56:14 +0200 |
---|---|---|
committer | Eike Ziller <eike.ziller@qt.io> | 2017-04-19 09:56:14 +0200 |
commit | 88897f3a870de7b756356907801f09a90902e490 (patch) | |
tree | a947c7fdb92d9050e9ade763dcb87db192b4585e /src/plugins/android | |
parent | 8b6eb5aabb48fb83c3cd92be9a77401ca26a810b (diff) | |
parent | 01b2ed7904132f845819e78c84477ac9a66bd1e3 (diff) | |
download | qt-creator-88897f3a870de7b756356907801f09a90902e490.tar.gz |
Merge remote-tracking branch 'origin/4.3'
Conflicts:
src/plugins/genericprojectmanager/genericproject.cpp
src/plugins/genericprojectmanager/genericproject.h
src/plugins/genericprojectmanager/genericprojectnodes.cpp
src/plugins/genericprojectmanager/genericprojectnodes.h
Change-Id: Ie0c870f68c8d200a75489b75860987655b2f6175
Diffstat (limited to 'src/plugins/android')
30 files changed, 2177 insertions, 581 deletions
diff --git a/src/plugins/android/android.pro b/src/plugins/android/android.pro index 0df9a4a4ae..3d63124af5 100644 --- a/src/plugins/android/android.pro +++ b/src/plugins/android/android.pro @@ -47,7 +47,10 @@ HEADERS += \ android_global.h \ androidbuildapkstep.h \ androidbuildapkwidget.h \ - androidrunnable.h + androidrunnable.h \ + androidtoolmanager.h \ + androidsdkmanager.h \ + androidavdmanager.h SOURCES += \ androidconfigurations.cpp \ @@ -88,7 +91,10 @@ SOURCES += \ androidbuildapkstep.cpp \ androidbuildapkwidget.cpp \ androidqtsupport.cpp \ - androidrunnable.cpp + androidrunnable.cpp \ + androidtoolmanager.cpp \ + androidsdkmanager.cpp \ + androidavdmanager.cpp FORMS += \ androidsettingswidget.ui \ diff --git a/src/plugins/android/android.qbs b/src/plugins/android/android.qbs index 1bb3296453..c3a2551bae 100644 --- a/src/plugins/android/android.qbs +++ b/src/plugins/android/android.qbs @@ -22,6 +22,8 @@ Project { "android.qrc", "androidanalyzesupport.cpp", "androidanalyzesupport.h", + "androidavdmanager.cpp", + "androidavdmanager.h", "androidconfigurations.cpp", "androidconfigurations.h", "androidconstants.h", @@ -84,6 +86,8 @@ Project { "androidrunnable.h", "androidrunner.cpp", "androidrunner.h", + "androidsdkmanager.cpp", + "androidsdkmanager.h", "androidsettingspage.cpp", "androidsettingspage.h", "androidsettingswidget.cpp", @@ -93,6 +97,8 @@ Project { "androidsignaloperation.h", "androidtoolchain.cpp", "androidtoolchain.h", + "androidtoolmanager.cpp", + "androidtoolmanager.h", "avddialog.cpp", "avddialog.h", "certificatesmodel.cpp", diff --git a/src/plugins/android/androidavdmanager.cpp b/src/plugins/android/androidavdmanager.cpp new file mode 100644 index 0000000000..028fe2c797 --- /dev/null +++ b/src/plugins/android/androidavdmanager.cpp @@ -0,0 +1,441 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 "androidavdmanager.h" + +#include "androidtoolmanager.h" + +#include "utils/algorithm.h" +#include "utils/qtcassert.h" +#include "utils/runextensions.h" +#include "utils/synchronousprocess.h" + +#include <QApplication> +#include <QFileInfo> +#include <QLoggingCategory> +#include <QSettings> + +#include <chrono> + +namespace { +Q_LOGGING_CATEGORY(avdManagerLog, "qtc.android.avdManager") +} + +namespace Android { +namespace Internal { + +using namespace std; + +// Avd list keys to parse avd +const char avdInfoNameKey[] = "Name:"; +const char avdInfoPathKey[] = "Path:"; +const char avdInfoAbiKey[] = "abi.type"; +const char avdInfoTargetKey[] = "target"; +const char avdInfoErrorKey[] = "Error:"; + +const QVersionNumber avdManagerIntroVersion(25, 3 ,0); + +const int avdCreateTimeoutMs = 30000; + +/*! + Runs the \c avdmanager tool specific to configuration \a config with arguments \a args. Returns + \c true if the command is successfully executed. Output is copied into \a output. The function + blocks the calling thread. + */ +static bool avdManagerCommand(const AndroidConfig config, const QStringList &args, QString *output) +{ + QString avdManagerToolPath = config.avdManagerToolPath().toString(); + Utils::SynchronousProcess proc; + Utils::SynchronousProcessResponse response = proc.runBlocking(avdManagerToolPath, args); + if (response.result == Utils::SynchronousProcessResponse::Finished) { + if (output) + *output = response.allOutput(); + return true; + } + return false; +} + +/*! + Parses the \a line for a [spaces]key[spaces]value[spaces] pattern and returns + \c true if the key is found, \c false otherwise. The value is copied into \a value. + */ +static bool valueForKey(QString key, const QString &line, QString *value = nullptr) +{ + auto trimmedInput = line.trimmed(); + if (trimmedInput.startsWith(key)) { + if (value) + *value = trimmedInput.section(key, 1, 1).trimmed(); + return true; + } + return false; +} + +static bool checkForTimeout(const chrono::steady_clock::time_point &start, + int msecs = 3000) +{ + bool timedOut = false; + auto end = chrono::steady_clock::now(); + if (chrono::duration_cast<chrono::milliseconds>(end-start).count() > msecs) + timedOut = true; + return timedOut; +} + +static AndroidConfig::CreateAvdInfo createAvdCommand(const AndroidConfig config, + const AndroidConfig::CreateAvdInfo &info) +{ + AndroidConfig::CreateAvdInfo result = info; + + if (!result.isValid()) { + qCDebug(avdManagerLog) << "AVD Create failed. Invalid CreateAvdInfo" << result.name + << result.target.name << result.target.apiLevel; + result.error = QApplication::translate("AndroidAvdManager", + "Cannot create AVD. Invalid input."); + return result; + } + + QStringList arguments({"create", "avd", "-k", result.target.package, "-n", result.name}); + + if (!result.abi.isEmpty()) { + SystemImage image = Utils::findOrDefault(result.target.systemImages, + Utils::equal(&SystemImage::abiName, result.abi)); + if (image.isValid()) { + arguments << "-k" << image.package; + } else { + qCDebug(avdManagerLog) << "AVD Create failed. Cannot find system image for the platform" + << result.abi << result.target.name; + result.error = QApplication::translate("AndroidAvdManager", + "Cannot create AVD. Cannot find system image for " + "the ABI %1(%2).").arg(result.abi).arg(result.target.name); + return result; + } + + } else { + arguments << "-k" << result.target.package; + } + + if (result.sdcardSize > 0) + arguments << "-c" << QString::fromLatin1("%1M").arg(result.sdcardSize); + + QProcess proc; + proc.start(config.avdManagerToolPath().toString(), arguments); + if (!proc.waitForStarted()) { + result.error = QApplication::translate("AndroidAvdManager", + "Could not start process \"%1 %2\"") + .arg(config.avdManagerToolPath().toString(), arguments.join(' ')); + return result; + } + QTC_CHECK(proc.state() == QProcess::Running); + proc.write(QByteArray("yes\n")); // yes to "Do you wish to create a custom hardware profile" + + auto start = chrono::steady_clock::now(); + QString errorOutput; + QByteArray question; + while (errorOutput.isEmpty()) { + proc.waitForReadyRead(500); + question += proc.readAllStandardOutput(); + if (question.endsWith(QByteArray("]:"))) { + // truncate to last line + int index = question.lastIndexOf(QByteArray("\n")); + if (index != -1) + question = question.mid(index); + if (question.contains("hw.gpu.enabled")) + proc.write(QByteArray("yes\n")); + else + proc.write(QByteArray("\n")); + question.clear(); + } + // The exit code is always 0, so we need to check stderr + // For now assume that any output at all indicates a error + errorOutput = QString::fromLocal8Bit(proc.readAllStandardError()); + if (proc.state() != QProcess::Running) + break; + + // For a sane input and command, process should finish before timeout. + if (checkForTimeout(start, avdCreateTimeoutMs)) { + result.error = QApplication::translate("AndroidAvdManager", + "Cannot create AVD. Command timed out."); + } + } + + // Kill the running process. + if (proc.state() != QProcess::NotRunning) { + proc.terminate(); + if (!proc.waitForFinished(3000)) + proc.kill(); + } + + QTC_CHECK(proc.state() == QProcess::NotRunning); + result.error = errorOutput; + return result; +} + +/*! + \class AvdManagerOutputParser + \brief The AvdManagerOutputParser class is a helper class to parse the output of the avdmanager + commands. + */ +class AvdManagerOutputParser +{ +public: + AndroidDeviceInfoList listVirtualDevices(const AndroidConfig &config); + AndroidDeviceInfoList parseAvdList(const QString &output); + +private: + bool parseAvd(const QStringList &deviceInfo, AndroidDeviceInfo *avd); +}; + + +AndroidAvdManager::AndroidAvdManager(const AndroidConfig &config): + m_config(config), + m_androidTool(new AndroidToolManager(m_config)), + m_parser(new AvdManagerOutputParser) +{ + +} + +AndroidAvdManager::~AndroidAvdManager() +{ + +} + +bool AndroidAvdManager::avdManagerUiToolAvailable() const +{ + return m_config.sdkToolsVersion() < avdManagerIntroVersion; +} + +void AndroidAvdManager::launchAvdManagerUiTool() const +{ + if (avdManagerUiToolAvailable()) { + m_androidTool->launchAvdManager(); + } else { + qCDebug(avdManagerLog) << "AVD Ui tool launch failed. UI tool not available" + << m_config.sdkToolsVersion(); + } +} + +QFuture<AndroidConfig::CreateAvdInfo> +AndroidAvdManager::createAvd(AndroidConfig::CreateAvdInfo info) const +{ + if (m_config.sdkToolsVersion() < avdManagerIntroVersion) + return m_androidTool->createAvd(info); + + return Utils::runAsync(&createAvdCommand, m_config, info); +} + +bool AndroidAvdManager::removeAvd(const QString &name) const +{ + if (m_config.sdkToolsVersion() < avdManagerIntroVersion) + return m_androidTool->removeAvd(name); + + Utils::SynchronousProcess proc; + proc.setTimeoutS(5); + Utils::SynchronousProcessResponse response + = proc.runBlocking(m_config.avdManagerToolPath().toString(), + QStringList({"delete", "avd", "-n", name})); + return response.result == Utils::SynchronousProcessResponse::Finished && response.exitCode == 0; +} + +QFuture<AndroidDeviceInfoList> AndroidAvdManager::avdList() const +{ + if (m_config.sdkToolsVersion() < avdManagerIntroVersion) + return m_androidTool->androidVirtualDevicesFuture(); + + return Utils::runAsync(&AvdManagerOutputParser::listVirtualDevices, m_parser.get(), m_config); +} + +QString AndroidAvdManager::startAvd(const QString &name) const +{ + if (!findAvd(name).isEmpty() || startAvdAsync(name)) + return waitForAvd(name); + return QString(); +} + +bool AndroidAvdManager::startAvdAsync(const QString &avdName) const +{ + QProcess *avdProcess = new QProcess(); + QObject::connect(avdProcess, static_cast<void (QProcess::*)(int)>(&QProcess::finished), + avdProcess, &QObject::deleteLater); + + // start the emulator + QStringList arguments; + if (AndroidConfigurations::force32bitEmulator()) + arguments << "-force-32bit"; + + arguments << "-partition-size" << QString::number(m_config.partitionSize()) + << "-avd" << avdName; + avdProcess->start(m_config.emulatorToolPath().toString(), arguments); + if (!avdProcess->waitForStarted(-1)) { + delete avdProcess; + return false; + } + return true; +} + +QString AndroidAvdManager::findAvd(const QString &avdName) const +{ + QVector<AndroidDeviceInfo> devices = m_config.connectedDevices(); + foreach (AndroidDeviceInfo device, devices) { + if (device.type != AndroidDeviceInfo::Emulator) + continue; + if (device.avdname == avdName) + return device.serialNumber; + } + return QString(); +} + +QString AndroidAvdManager::waitForAvd(const QString &avdName, const QFutureInterface<bool> &fi) const +{ + // we cannot use adb -e wait-for-device, since that doesn't work if a emulator is already running + // 60 rounds of 2s sleeping, two minutes for the avd to start + QString serialNumber; + for (int i = 0; i < 60; ++i) { + if (fi.isCanceled()) + return QString(); + serialNumber = findAvd(avdName); + if (!serialNumber.isEmpty()) + return waitForBooted(serialNumber, fi) ? serialNumber : QString(); + QThread::sleep(2); + } + return QString(); +} + +bool AndroidAvdManager::isAvdBooted(const QString &device) const +{ + QStringList arguments = AndroidDeviceInfo::adbSelector(device); + arguments << "shell" << "getprop" << "init.svc.bootanim"; + + Utils::SynchronousProcess adbProc; + adbProc.setTimeoutS(10); + Utils::SynchronousProcessResponse response = + adbProc.runBlocking(m_config.adbToolPath().toString(), arguments); + if (response.result != Utils::SynchronousProcessResponse::Finished) + return false; + QString value = response.allOutput().trimmed(); + return value == "stopped"; +} + +bool AndroidAvdManager::waitForBooted(const QString &serialNumber, const QFutureInterface<bool> &fi) const +{ + // found a serial number, now wait until it's done booting... + for (int i = 0; i < 60; ++i) { + if (fi.isCanceled()) + return false; + if (isAvdBooted(serialNumber)) { + return true; + } else { + QThread::sleep(2); + if (!m_config.isConnected(serialNumber)) // device was disconnected + return false; + } + } + return false; +} + +AndroidDeviceInfoList AvdManagerOutputParser::listVirtualDevices(const AndroidConfig &config) +{ + QString output; + if (!avdManagerCommand(config, QStringList({"list", "avd"}), &output)) { + qCDebug(avdManagerLog) << "Avd list command failed" << output << config.sdkToolsVersion(); + return {}; + } + return parseAvdList(output); +} + +AndroidDeviceInfoList AvdManagerOutputParser::parseAvdList(const QString &output) +{ + AndroidDeviceInfoList avdList; + QStringList avdInfo; + auto parseAvdInfo = [&avdInfo, &avdList, this] () { + AndroidDeviceInfo avd; + if (parseAvd(avdInfo, &avd)) { + // armeabi-v7a devices can also run armeabi code + if (avd.cpuAbi.contains("armeabi-v7a")) + avd.cpuAbi << "armeabi"; + avd.state = AndroidDeviceInfo::OkState; + avd.type = AndroidDeviceInfo::Emulator; + avdList << avd; + } else { + qCDebug(avdManagerLog) << "Avd Parsing: Parsing failed: " << avdInfo; + } + avdInfo.clear(); + }; + + foreach (QString line, output.split('\n')) { + if (line.startsWith("---------") || line.isEmpty()) { + parseAvdInfo(); + } else { + avdInfo << line; + } + } + + if (!avdInfo.isEmpty()) + parseAvdInfo(); + + Utils::sort(avdList); + + return avdList; +} + +bool AvdManagerOutputParser::parseAvd(const QStringList &deviceInfo, AndroidDeviceInfo *avd) +{ + QTC_ASSERT(avd, return false); + foreach (const QString &line, deviceInfo) { + QString value; + if (valueForKey(avdInfoErrorKey, line)) { + qCDebug(avdManagerLog) << "Avd Parsing: Skip avd device. Error key found:" << line; + return false; + } else if (valueForKey(avdInfoNameKey, line, &value)) { + avd->avdname = value; + } else if (valueForKey(avdInfoPathKey, line, &value)) { + const Utils::FileName avdPath = Utils::FileName::fromString(value); + if (avdPath.exists()) + { + // Get ABI. + Utils::FileName configFile = avdPath; + configFile.appendPath("config.ini"); + QSettings config(configFile.toString(), QSettings::IniFormat); + value = config.value(avdInfoAbiKey).toString(); + if (!value.isEmpty()) + avd->cpuAbi << value; + else + qCDebug(avdManagerLog) << "Avd Parsing: Cannot find ABI:" << configFile; + + // Get Target + Utils::FileName avdInfoFile = avdPath.parentDir(); + QString avdInfoFileName = avdPath.toFileInfo().baseName() + ".ini"; + avdInfoFile.appendPath(avdInfoFileName); + QSettings avdInfo(avdInfoFile.toString(), QSettings::IniFormat); + value = avdInfo.value(avdInfoTargetKey).toString(); + if (!value.isEmpty()) + avd->sdk = value.section('-', -1).toInt(); + else + qCDebug(avdManagerLog) << "Avd Parsing: Cannot find sdk API:" << avdInfoFile.toString(); + } + } + } + return true; +} + +} // namespace Internal +} // namespace Android diff --git a/src/plugins/android/androidavdmanager.h b/src/plugins/android/androidavdmanager.h new file mode 100644 index 0000000000..4e8633efda --- /dev/null +++ b/src/plugins/android/androidavdmanager.h @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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. +** +****************************************************************************/ +#pragma once + +#include "androidconfigurations.h" + +#include <memory> + +namespace Android { +namespace Internal { + +class AndroidToolManager; +class AvdManagerOutputParser; + +class AndroidAvdManager +{ +public: + AndroidAvdManager(const AndroidConfig& config = AndroidConfigurations::currentConfig()); + ~AndroidAvdManager(); + + bool avdManagerUiToolAvailable() const; + void launchAvdManagerUiTool() const; + QFuture<AndroidConfig::CreateAvdInfo> createAvd(AndroidConfig::CreateAvdInfo info) const; + bool removeAvd(const QString &name) const; + QFuture<AndroidDeviceInfoList> avdList() const; + + QString startAvd(const QString &name) const; + bool startAvdAsync(const QString &avdName) const; + QString findAvd(const QString &avdName) const; + QString waitForAvd(const QString &avdName, + const QFutureInterface<bool> &fi = QFutureInterface<bool>()) const; + bool isAvdBooted(const QString &device) const; + +private: + bool waitForBooted(const QString &serialNumber, const QFutureInterface<bool> &fi) const; + +private: + const AndroidConfig &m_config; + std::unique_ptr<AndroidToolManager> m_androidTool; + std::unique_ptr<AvdManagerOutputParser> m_parser; +}; + +} // namespace Internal +} // namespace Android diff --git a/src/plugins/android/androidbuildapkstep.cpp b/src/plugins/android/androidbuildapkstep.cpp index 0ff998809f..9966b21faf 100644 --- a/src/plugins/android/androidbuildapkstep.cpp +++ b/src/plugins/android/androidbuildapkstep.cpp @@ -60,6 +60,8 @@ namespace Android { using namespace Internal; +const QVersionNumber gradleScriptRevokedSdkVersion(25, 3, 0); +const QVersionNumber gradleScriptsContainedQtVersion(5, 9, 0); const QLatin1String DeployActionKey("Qt4ProjectManager.AndroidDeployQtStep.DeployQtAction"); const QLatin1String KeystoreLocationKey("KeystoreLocation"); const QLatin1String BuildTargetSdkKey("BuildTargetSdk"); @@ -140,6 +142,15 @@ bool AndroidBuildApkStep::init(QList<const BuildStep *> &earlierSteps) if (!version) return false; + if (AndroidConfigurations::currentConfig().sdkToolsVersion() >= gradleScriptRevokedSdkVersion && + QVersionNumber::fromString(version->qtVersionString()) < gradleScriptsContainedQtVersion) { + emit addOutput(tr("The installed SDK tools version (%1) does not include Gradle scripts. The " + "minimum Qt version required for Gradle build to work is %2") + .arg(gradleScriptRevokedSdkVersion.toString()) + .arg(gradleScriptsContainedQtVersion.toString()), OutputFormat::Stderr); + return false; + } + int minSDKForKit = AndroidManager::minimumSDK(target()->kit()); if (AndroidManager::minimumSDK(target()) < minSDKForKit) { emit addOutput(tr("The API level set for the APK is less than the minimum required by the kit." @@ -342,6 +353,16 @@ void AndroidBuildApkStep::setUseGradle(bool b) } } +bool AndroidBuildApkStep::addDebugger() const +{ + return m_addDebugger; +} + +void AndroidBuildApkStep::setAddDebugger(bool debug) +{ + m_addDebugger = debug; +} + bool AndroidBuildApkStep::verboseOutput() const { return m_verbose; diff --git a/src/plugins/android/androidbuildapkstep.h b/src/plugins/android/androidbuildapkstep.h index 0d044c0ccc..eae827eb15 100644 --- a/src/plugins/android/androidbuildapkstep.h +++ b/src/plugins/android/androidbuildapkstep.h @@ -73,6 +73,9 @@ public: bool useGradle() const; void setUseGradle(bool b); + bool addDebugger() const; + void setAddDebugger(bool debug); + QString buildTargetSdk() const; void setBuildTargetSdk(const QString &sdk); @@ -99,9 +102,10 @@ protected: AndroidDeployAction m_deployAction = BundleLibrariesDeployment; bool m_signPackage = false; bool m_verbose = false; - bool m_useGradle = false; + bool m_useGradle = true; // Ant builds are deprecated. bool m_openPackageLocation = false; bool m_openPackageLocationForRun = false; + bool m_addDebugger = true; QString m_buildTargetSdk; Utils::FileName m_keystorePath; diff --git a/src/plugins/android/androidbuildapkwidget.cpp b/src/plugins/android/androidbuildapkwidget.cpp index 2e85c243fc..97dd4695bd 100644 --- a/src/plugins/android/androidbuildapkwidget.cpp +++ b/src/plugins/android/androidbuildapkwidget.cpp @@ -54,9 +54,12 @@ AndroidBuildApkWidget::AndroidBuildApkWidget(AndroidBuildApkStep *step) { m_ui->setupUi(this); + m_ui->deprecatedInfoIconLabel->setPixmap(Utils::Icons::INFO.pixmap()); + // Target sdk combobox int minApiLevel = 9; - QStringList targets = AndroidConfig::apiLevelNamesFor(AndroidConfigurations::currentConfig().sdkTargets(minApiLevel)); + const AndroidConfig &config = AndroidConfigurations::currentConfig(); + QStringList targets = AndroidConfig::apiLevelNamesFor(config.sdkTargets(minApiLevel)); targets.removeDuplicates(); m_ui->targetSDKComboBox->addItems(targets); m_ui->targetSDKComboBox->setCurrentIndex(targets.indexOf(AndroidManager::buildTargetSDK(step->target()))); @@ -91,9 +94,12 @@ AndroidBuildApkWidget::AndroidBuildApkWidget(AndroidBuildApkStep *step) m_ui->signingDebugDeployErrorIcon->setPixmap(Utils::Icons::CRITICAL.pixmap()); signPackageCheckBoxToggled(m_step->signPackage()); - m_ui->useGradleCheckBox->setChecked(m_step->useGradle()); + m_ui->useGradleCheckBox->setEnabled(config.antScriptsAvailable()); + m_ui->useGradleCheckBox->setChecked(!config.antScriptsAvailable() || + m_step->useGradle()); m_ui->verboseOutputCheckBox->setChecked(m_step->verboseOutput()); m_ui->openPackageLocationCheckBox->setChecked(m_step->openPackageLocation()); + m_ui->addDebuggerCheckBox->setChecked(m_step->addDebugger()); // target sdk connect(m_ui->targetSDKComboBox, @@ -120,6 +126,8 @@ AndroidBuildApkWidget::AndroidBuildApkWidget(AndroidBuildApkStep *step) this, &AndroidBuildApkWidget::openPackageLocationCheckBoxToggled); connect(m_ui->verboseOutputCheckBox, &QAbstractButton::toggled, this, &AndroidBuildApkWidget::verboseOutputCheckBoxToggled); + connect(m_ui->addDebuggerCheckBox, &QAbstractButton::toggled, + m_step, &AndroidBuildApkStep::setAddDebugger); //signing connect(m_ui->signPackageCheckBox, &QAbstractButton::toggled, @@ -185,6 +193,7 @@ void AndroidBuildApkWidget::signPackageCheckBoxToggled(bool checked) { m_ui->certificatesAliasComboBox->setEnabled(checked); m_step->setSignPackage(checked); + m_ui->addDebuggerCheckBox->setChecked(!checked); updateSigningWarning(); if (!checked) return; diff --git a/src/plugins/android/androidbuildapkwidget.ui b/src/plugins/android/androidbuildapkwidget.ui index e5565873af..fa3c1ef3d6 100644 --- a/src/plugins/android/androidbuildapkwidget.ui +++ b/src/plugins/android/androidbuildapkwidget.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>819</width> - <height>390</height> + <height>478</height> </rect> </property> <property name="windowTitle"> @@ -176,24 +176,75 @@ Deploying local Qt libraries is incompatible with Android 5.</string> <string>Advanced Actions</string> </property> <layout class="QGridLayout" name="gridLayout"> - <item row="2" column="0"> - <widget class="QCheckBox" name="verboseOutputCheckBox"> + <item row="0" column="0"> + <widget class="QCheckBox" name="useGradleCheckBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="text"> - <string>Verbose output</string> + <string>Use Gradle (Ant builds are deprecated)</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="deprecatedInfoIconLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Gradle builds are forced from Android SDK tools version 25.3.0 onwards as Ant scripts are no longer available.</string> + </property> + <property name="text"> + <string/> </property> </widget> </item> - <item row="1" column="0"> + <item row="0" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Preferred</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="0" colspan="3"> <widget class="QCheckBox" name="openPackageLocationCheckBox"> <property name="text"> <string>Open package location after build</string> </property> </widget> </item> - <item row="0" column="0"> - <widget class="QCheckBox" name="useGradleCheckBox"> + <item row="2" column="0" colspan="3"> + <widget class="QCheckBox" name="verboseOutputCheckBox"> + <property name="text"> + <string>Verbose output</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="3"> + <widget class="QCheckBox" name="addDebuggerCheckBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Packages debug server with the APK to enable debugging. For the signed APK this option is unchecked by default.</string> + </property> <property name="text"> - <string>Use Gradle</string> + <string>Add debug server</string> </property> </widget> </item> @@ -254,5 +305,22 @@ The APK will not be usable on any other device.</string> </customwidget> </customwidgets> <resources/> - <connections/> + <connections> + <connection> + <sender>signPackageCheckBox</sender> + <signal>clicked(bool)</signal> + <receiver>addDebuggerCheckBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>113</x> + <y>178</y> + </hint> + <hint type="destinationlabel"> + <x>510</x> + <y>452</y> + </hint> + </hints> + </connection> + </connections> </ui> diff --git a/src/plugins/android/androidconfigurations.cpp b/src/plugins/android/androidconfigurations.cpp index 851b0f2350..42027743b8 100644 --- a/src/plugins/android/androidconfigurations.cpp +++ b/src/plugins/android/androidconfigurations.cpp @@ -28,8 +28,11 @@ #include "androidtoolchain.h" #include "androiddevice.h" #include "androidgdbserverkitinformation.h" +#include "androidmanager.h" #include "androidqtversion.h" #include "androiddevicedialog.h" +#include "androidsdkmanager.h" +#include "androidtoolmanager.h" #include "avddialog.h" #include <coreplugin/icore.h> @@ -73,6 +76,9 @@ namespace Android { using namespace Internal; namespace { + + const QVersionNumber sdkToolsAntMissingVersion(25, 3, 0); + const QLatin1String SettingsGroup("AndroidConfigurations"); const QLatin1String SDKLocationKey("SDKLocation"); const QLatin1String NDKLocationKey("NDKLocation"); @@ -107,39 +113,14 @@ namespace { const QLatin1String keytoolName("keytool"); const QLatin1String changeTimeStamp("ChangeTimeStamp"); + const QLatin1String sdkToolsVersionKey("Pkg.Revision"); + static QString sdkSettingsFileName() { return QFileInfo(Core::ICore::settings(QSettings::SystemScope)->fileName()).absolutePath() + QLatin1String("/qtcreator/android.xml"); } - bool androidDevicesLessThan(const AndroidDeviceInfo &dev1, const AndroidDeviceInfo &dev2) - { - if (dev1.serialNumber.contains(QLatin1String("????")) != dev2.serialNumber.contains(QLatin1String("????"))) - return !dev1.serialNumber.contains(QLatin1String("????")); - if (dev1.type != dev2.type) - return dev1.type == AndroidDeviceInfo::Hardware; - if (dev1.sdk != dev2.sdk) - return dev1.sdk < dev2.sdk; - if (dev1.avdname != dev2.avdname) - return dev1.avdname < dev2.avdname; - - return dev1.serialNumber < dev2.serialNumber; - } - - static QStringList cleanAndroidABIs(const QStringList &abis) - { - QStringList res; - foreach (const QString &abi, abis) { - int index = abi.lastIndexOf(QLatin1Char('/')); - if (index == -1) - res << abi; - else - res << abi.mid(index + 1); - } - return res; - } - static bool is32BitUserSpace() { // Do the exact same check as android's emulator is doing: @@ -162,25 +143,6 @@ namespace { } return false; } - - // Some preview sdks use a non integer version - int apiLevelFromAndroidList(const QString &string) - { - bool ok; - int result = string.toInt(&ok); - if (ok) - return result; - Utils::FileName sdkLocation = AndroidConfigurations::currentConfig().sdkLocation(); - sdkLocation.appendPath(QLatin1String("/platforms/android-") + string + QLatin1String("/source.properties")); - result = QSettings(sdkLocation.toString(), QSettings::IniFormat).value(QLatin1String("AndroidVersion.ApiLevel")).toInt(&ok); - if (ok) - return result; - if (string == QLatin1String("L")) - return 21; - if (string == QLatin1String("MNC")) - return 22; - return 23; // At least - } } ////////////////////////////////// @@ -359,61 +321,14 @@ void AndroidConfig::updateNdkInformation() const m_NdkInformationUpToDate = true; } -bool AndroidConfig::sortSdkPlatformByApiLevel(const SdkPlatform &a, const SdkPlatform &b) -{ - if (a.apiLevel != b.apiLevel) - return a.apiLevel > b.apiLevel; - if (a.name != b.name) - return a.name < b.name; - return false; -} - void AndroidConfig::updateAvailableSdkPlatforms() const { if (m_availableSdkPlatformsUpToDate) return; - m_availableSdkPlatforms.clear(); - - SynchronousProcess proc; - proc.setProcessEnvironment(androidToolEnvironment().toProcessEnvironment()); - SynchronousProcessResponse response - = proc.runBlocking(androidToolPath().toString(), - QStringList({"list", "target"})); // list available AVDs - if (response.result != SynchronousProcessResponse::Finished) - return; - - SdkPlatform platform; - foreach (const QString &l, response.allOutput().split('\n')) { - const QString line = l.trimmed(); - if (line.startsWith(QLatin1String("id:")) && line.contains(QLatin1String("android-"))) { - int index = line.indexOf(QLatin1String("\"android-")); - if (index == -1) - continue; - QString androidTarget = line.mid(index + 1, line.length() - index - 2); - const QString tmp = androidTarget.mid(androidTarget.lastIndexOf(QLatin1Char('-')) + 1); - platform.apiLevel = apiLevelFromAndroidList(tmp); - } else if (line.startsWith(QLatin1String("Name:"))) { - platform.name = line.mid(6); - } else if (line.startsWith(QLatin1String("Tag/ABIs :"))) { - platform.abis = cleanAndroidABIs(line.mid(10).trimmed().split(QLatin1String(", "))); - } else if (line.startsWith(QLatin1String("ABIs"))) { - platform.abis = cleanAndroidABIs(line.mid(6).trimmed().split(QLatin1String(", "))); - } else if (line.startsWith(QLatin1String("---")) || line.startsWith(QLatin1String("==="))) { - if (platform.apiLevel == -1) - continue; - auto it = std::lower_bound(m_availableSdkPlatforms.begin(), m_availableSdkPlatforms.end(), - platform, sortSdkPlatformByApiLevel); - m_availableSdkPlatforms.insert(it, platform); - platform = SdkPlatform(); - } - } - - if (platform.apiLevel != -1) { - auto it = std::lower_bound(m_availableSdkPlatforms.begin(), m_availableSdkPlatforms.end(), - platform, sortSdkPlatformByApiLevel); - m_availableSdkPlatforms.insert(it, platform); - } + m_availableSdkPlatforms.clear(); + AndroidSdkManager sdkManager(*this); + m_availableSdkPlatforms = sdkManager.availableSdkPlatforms(); m_availableSdkPlatformsUpToDate = true; } @@ -446,18 +361,6 @@ FileName AndroidConfig::adbToolPath() const return path.appendPath(QLatin1String("platform-tools/adb" QTC_HOST_EXE_SUFFIX)); } -Environment AndroidConfig::androidToolEnvironment() const -{ - Environment env = Environment::systemEnvironment(); - if (!m_openJDKLocation.isEmpty()) { - env.set(QLatin1String("JAVA_HOME"), m_openJDKLocation.toUserOutput()); - Utils::FileName binPath = m_openJDKLocation; - binPath.appendPath(QLatin1String("bin")); - env.prependOrSetPath(binPath.toUserOutput()); - } - return env; -} - FileName AndroidConfig::androidToolPath() const { if (HostOsInfo::isWindowsHost()) { @@ -486,7 +389,10 @@ FileName AndroidConfig::antToolPath() const FileName AndroidConfig::emulatorToolPath() const { FileName path = m_sdkLocation; - return path.appendPath(QLatin1String("tools/emulator" QTC_HOST_EXE_SUFFIX)); + QString relativePath = "emulator/emulator"; + if (sdkToolsVersion() < QVersionNumber(25, 3, 0)) + relativePath = "tools/emulator"; + return path.appendPath(relativePath + QTC_HOST_EXE_SUFFIX); } FileName AndroidConfig::toolPath(const Abi &abi, const QString &ndkToolChainVersion) const @@ -499,6 +405,26 @@ FileName AndroidConfig::toolPath(const Abi &abi, const QString &ndkToolChainVers .arg(toolsPrefix(abi))); } +FileName AndroidConfig::sdkManagerToolPath() const +{ + FileName sdkPath = m_sdkLocation; + QString toolPath = "tools/bin/sdkmanager"; + if (HostOsInfo::isWindowsHost()) + toolPath += ANDROID_BAT_SUFFIX; + sdkPath = sdkPath.appendPath(toolPath); + return sdkPath; +} + +FileName AndroidConfig::avdManagerToolPath() const +{ + FileName avdManagerPath = m_sdkLocation; + QString toolPath = "tools/bin/avdmanager"; + if (HostOsInfo::isWindowsHost()) + toolPath += ANDROID_BAT_SUFFIX; + avdManagerPath = avdManagerPath.appendPath(toolPath); + return avdManagerPath; +} + FileName AndroidConfig::gccPath(const Abi &abi, Core::Id lang, const QString &ndkToolChainVersion) const { @@ -583,7 +509,7 @@ QVector<AndroidDeviceInfo> AndroidConfig::connectedDevices(const QString &adbToo devices.push_back(dev); } - Utils::sort(devices, androidDevicesLessThan); + Utils::sort(devices); if (devices.isEmpty() && error) *error = QApplication::translate("AndroidConfiguration", "No devices found in output of: %1") @@ -605,197 +531,6 @@ AndroidConfig::CreateAvdInfo AndroidConfig::gatherCreateAVDInfo(QWidget *parent, return result; } -QFuture<AndroidConfig::CreateAvdInfo> AndroidConfig::createAVD(CreateAvdInfo info) const -{ - return Utils::runAsync(&AndroidConfig::createAVDImpl, info, - androidToolPath(), androidToolEnvironment()); -} - -AndroidConfig::CreateAvdInfo AndroidConfig::createAVDImpl(CreateAvdInfo info, FileName androidToolPath, Environment env) -{ - QProcess proc; - proc.setProcessEnvironment(env.toProcessEnvironment()); - QStringList arguments; - arguments << QLatin1String("create") << QLatin1String("avd") - << QLatin1String("-t") << info.target - << QLatin1String("-n") << info.name - << QLatin1String("-b") << info.abi; - if (info.sdcardSize > 0) - arguments << QLatin1String("-c") << QString::fromLatin1("%1M").arg(info.sdcardSize); - proc.start(androidToolPath.toString(), arguments); - if (!proc.waitForStarted()) { - info.error = QApplication::translate("AndroidConfig", "Could not start process \"%1 %2\"") - .arg(androidToolPath.toString(), arguments.join(QLatin1Char(' '))); - return info; - } - QTC_CHECK(proc.state() == QProcess::Running); - proc.write(QByteArray("yes\n")); // yes to "Do you wish to create a custom hardware profile" - - QByteArray question; - while (true) { - proc.waitForReadyRead(500); - question += proc.readAllStandardOutput(); - if (question.endsWith(QByteArray("]:"))) { - // truncate to last line - int index = question.lastIndexOf(QByteArray("\n")); - if (index != -1) - question = question.mid(index); - if (question.contains("hw.gpu.enabled")) - proc.write(QByteArray("yes\n")); - else - proc.write(QByteArray("\n")); - question.clear(); - } - - if (proc.state() != QProcess::Running) - break; - } - QTC_CHECK(proc.state() == QProcess::NotRunning); - - QString errorOutput = QString::fromLocal8Bit(proc.readAllStandardError()); - // The exit code is always 0, so we need to check stderr - // For now assume that any output at all indicates a error - if (!errorOutput.isEmpty()) { - info.error = errorOutput; - } - - return info; -} - -bool AndroidConfig::removeAVD(const QString &name) const -{ - SynchronousProcess proc; - proc.setTimeoutS(5); - proc.setProcessEnvironment(androidToolEnvironment().toProcessEnvironment()); - SynchronousProcessResponse response - = proc.runBlocking(androidToolPath().toString(), QStringList({"delete", "avd", "-n", name})); - return response.result == SynchronousProcessResponse::Finished && response.exitCode == 0; -} - -QFuture<QVector<AndroidDeviceInfo>> AndroidConfig::androidVirtualDevicesFuture() const -{ - return Utils::runAsync(&AndroidConfig::androidVirtualDevices, - androidToolPath().toString(), androidToolEnvironment()); -} - -QVector<AndroidDeviceInfo> AndroidConfig::androidVirtualDevices(const QString &androidTool, const Environment &environment) -{ - QVector<AndroidDeviceInfo> devices; - SynchronousProcess proc; - proc.setTimeoutS(20); - proc.setProcessEnvironment(environment.toProcessEnvironment()); - SynchronousProcessResponse response = proc.run(androidTool, {"list", "avd"}); // list available AVDs - if (response.result != SynchronousProcessResponse::Finished) - return devices; - - QStringList avds = response.allOutput().split('\n'); - if (avds.empty()) - return devices; - - while (avds.first().startsWith(QLatin1String("* daemon"))) - avds.removeFirst(); // remove the daemon logs - avds.removeFirst(); // remove "List of devices attached" header line - - bool nextLineIsTargetLine = false; - - AndroidDeviceInfo dev; - for (int i = 0; i < avds.size(); i++) { - QString line = avds.at(i); - if (!line.contains(QLatin1String("Name:"))) - continue; - - int index = line.indexOf(QLatin1Char(':')) + 2; - if (index >= line.size()) - break; - dev.avdname = line.mid(index).trimmed(); - dev.sdk = -1; - dev.cpuAbi.clear(); - ++i; - for (; i < avds.size(); ++i) { - line = avds.at(i); - if (line.contains(QLatin1String("---------"))) - break; - - if (line.contains(QLatin1String("Target:")) || nextLineIsTargetLine) { - if (line.contains(QLatin1String("Google APIs"))) { - nextLineIsTargetLine = true; - continue; - } - - nextLineIsTargetLine = false; - - int lastIndex = line.lastIndexOf(QLatin1Char(' ')); - if (lastIndex == -1) // skip line - break; - QString tmp = line.mid(lastIndex).remove(QLatin1Char(')')).trimmed(); - dev.sdk = apiLevelFromAndroidList(tmp); - } - if (line.contains(QLatin1String("Tag/ABI:"))) { - int lastIndex = line.lastIndexOf(QLatin1Char('/')) + 1; - if (lastIndex >= line.size()) - break; - dev.cpuAbi = QStringList(line.mid(lastIndex)); - } else if (line.contains(QLatin1String("ABI:"))) { - int lastIndex = line.lastIndexOf(QLatin1Char(' ')) + 1; - if (lastIndex >= line.size()) - break; - dev.cpuAbi = QStringList(line.mid(lastIndex).trimmed()); - } - } - // armeabi-v7a devices can also run armeabi code - if (dev.cpuAbi == QStringList("armeabi-v7a")) - dev.cpuAbi << QLatin1String("armeabi"); - dev.state = AndroidDeviceInfo::OkState; - dev.type = AndroidDeviceInfo::Emulator; - if (dev.cpuAbi.isEmpty() || dev.sdk == -1) - continue; - devices.push_back(dev); - } - Utils::sort(devices, androidDevicesLessThan); - - return devices; -} - -QString AndroidConfig::startAVD(const QString &name) const -{ - if (!findAvd(name).isEmpty() || startAVDAsync(name)) - return waitForAvd(name); - return QString(); -} - -bool AndroidConfig::startAVDAsync(const QString &avdName) const -{ - QProcess *avdProcess = new QProcess(); - QObject::connect(avdProcess, static_cast<void (QProcess::*)(int)>(&QProcess::finished), - avdProcess, &QObject::deleteLater); - - // start the emulator - QStringList arguments; - if (AndroidConfigurations::force32bitEmulator()) - arguments << QLatin1String("-force-32bit"); - - arguments << QLatin1String("-partition-size") << QString::number(partitionSize()) - << QLatin1String("-avd") << avdName; - avdProcess->start(emulatorToolPath().toString(), arguments); - if (!avdProcess->waitForStarted(-1)) { - delete avdProcess; - return false; - } - return true; -} - -QString AndroidConfig::findAvd(const QString &avdName) const -{ - QVector<AndroidDeviceInfo> devices = connectedDevices(); - foreach (AndroidDeviceInfo device, devices) { - if (device.type != AndroidDeviceInfo::Emulator) - continue; - if (device.avdname == avdName) - return device.serialNumber; - } - return QString(); -} - bool AndroidConfig::isConnected(const QString &serialNumber) const { QVector<AndroidDeviceInfo> devices = connectedDevices(); @@ -806,39 +541,6 @@ bool AndroidConfig::isConnected(const QString &serialNumber) const return false; } -bool AndroidConfig::waitForBooted(const QString &serialNumber, const QFutureInterface<bool> &fi) const -{ - // found a serial number, now wait until it's done booting... - for (int i = 0; i < 60; ++i) { - if (fi.isCanceled()) - return false; - if (hasFinishedBooting(serialNumber)) { - return true; - } else { - QThread::sleep(2); - if (!isConnected(serialNumber)) // device was disconnected - return false; - } - } - return false; -} - -QString AndroidConfig::waitForAvd(const QString &avdName, const QFutureInterface<bool> &fi) const -{ - // we cannot use adb -e wait-for-device, since that doesn't work if a emulator is already running - // 60 rounds of 2s sleeping, two minutes for the avd to start - QString serialNumber; - for (int i = 0; i < 60; ++i) { - if (fi.isCanceled()) - return QString(); - serialNumber = findAvd(avdName); - if (!serialNumber.isEmpty()) - return waitForBooted(serialNumber, fi) ? serialNumber : QString(); - QThread::sleep(2); - } - return QString(); -} - bool AndroidConfig::isBootToQt(const QString &device) const { return isBootToQt(adbToolPath().toString(), device); @@ -961,21 +663,6 @@ QString AndroidConfig::getProductModel(const QString &device) const return model; } -bool AndroidConfig::hasFinishedBooting(const QString &device) const -{ - QStringList arguments = AndroidDeviceInfo::adbSelector(device); - arguments << QLatin1String("shell") << QLatin1String("getprop") - << QLatin1String("init.svc.bootanim"); - - SynchronousProcess adbProc; - adbProc.setTimeoutS(10); - SynchronousProcessResponse response = adbProc.runBlocking(adbToolPath().toString(), arguments); - if (response.result != SynchronousProcessResponse::Finished) - return false; - QString value = response.allOutput().trimmed(); - return value == QLatin1String("stopped"); -} - QStringList AndroidConfig::getAbis(const QString &device) const { return getAbis(adbToolPath().toString(), device); @@ -1053,6 +740,19 @@ void AndroidConfig::setSdkLocation(const FileName &sdkLocation) m_availableSdkPlatformsUpToDate = false; } +QVersionNumber AndroidConfig::sdkToolsVersion() const +{ + QVersionNumber version; + if (m_sdkLocation.exists()) { + Utils::FileName sdkToolsPropertiesPath(m_sdkLocation); + sdkToolsPropertiesPath.appendPath("tools/source.properties"); + QSettings settings(sdkToolsPropertiesPath.toString(), QSettings::IniFormat); + auto versionStr = settings.value(sdkToolsVersionKey).toString(); + version = QVersionNumber::fromString(versionStr); + } + return version; +} + FileName AndroidConfig::ndkLocation() const { return m_ndkLocation; @@ -1126,9 +826,18 @@ void AndroidConfig::setAutomaticKitCreation(bool b) m_automaticKitCreation = b; } +bool AndroidConfig::antScriptsAvailable() const +{ + return sdkToolsVersion() < sdkToolsAntMissingVersion; +} + bool AndroidConfig::useGrandle() const { - return m_useGradle; + if (antScriptsAvailable()) { + return m_useGradle; + } + // Force gradle builds. + return true; } void AndroidConfig::setUseGradle(bool b) @@ -1353,6 +1062,20 @@ QStringList AndroidDeviceInfo::adbSelector(const QString &serialNumber) return QStringList({"-s", serialNumber}); } +bool AndroidDeviceInfo::operator<(const AndroidDeviceInfo &other) const +{ + if (serialNumber.contains("????") != other.serialNumber.contains("????")) + return !serialNumber.contains("????"); + if (type != other.type) + return type == AndroidDeviceInfo::Hardware; + if (sdk != other.sdk) + return sdk < other.sdk; + if (avdname != other.avdname) + return avdname < other.avdname; + + return serialNumber < other.serialNumber; +} + const AndroidConfig &AndroidConfigurations::currentConfig() { return m_instance->m_config; // ensure that m_instance is initialized @@ -1494,4 +1217,13 @@ void AndroidConfigurations::updateAndroidDevice() AndroidConfigurations *AndroidConfigurations::m_instance = 0; +bool SdkPlatform::operator <(const SdkPlatform &other) const +{ + if (apiLevel != other.apiLevel) + return apiLevel > other.apiLevel; + if (name != other.name) + return name < other.name; + return false; +} + } // namespace Android diff --git a/src/plugins/android/androidconfigurations.h b/src/plugins/android/androidconfigurations.h index 751e68d113..9107680810 100644 --- a/src/plugins/android/androidconfigurations.h +++ b/src/plugins/android/androidconfigurations.h @@ -36,6 +36,7 @@ #include <QHash> #include <QMap> #include <QFutureInterface> +#include <QVersionNumber> #include <utils/fileutils.h> @@ -68,19 +69,36 @@ public: static QStringList adbSelector(const QString &serialNumber); - bool isValid() { return !serialNumber.isEmpty() || !avdname.isEmpty(); } + bool isValid() const { return !serialNumber.isEmpty() || !avdname.isEmpty(); } + bool operator<(const AndroidDeviceInfo &other) const; }; +using AndroidDeviceInfoList = QList<AndroidDeviceInfo>; + +//! Defines an Android system image. +class SystemImage +{ +public: + bool isValid() const { return (apiLevel != -1) && !abiName.isEmpty(); } + int apiLevel = -1; + QString abiName; + QString package; + Utils::FileName installedLocation; +}; +using SystemImageList = QList<SystemImage>; + class SdkPlatform { public: - SdkPlatform() - : apiLevel(-1) - {} - int apiLevel; + bool isValid() const { return !name.isEmpty() && apiLevel != -1; } + bool operator <(const SdkPlatform &other) const; + int apiLevel = -1; QString name; - QStringList abis; + QString package; + Utils::FileName installedLocation; + SystemImageList systemImages; }; +using SdkPlatformList = QList<SdkPlatform>; class ANDROID_EXPORT AndroidConfig { @@ -94,6 +112,7 @@ public: Utils::FileName sdkLocation() const; void setSdkLocation(const Utils::FileName &sdkLocation); + QVersionNumber sdkToolsVersion() const; Utils::FileName ndkLocation() const; void setNdkLocation(const Utils::FileName &ndkLocation); @@ -116,18 +135,21 @@ public: bool automaticKitCreation() const; void setAutomaticKitCreation(bool b); + bool antScriptsAvailable() const; + bool useGrandle() const; void setUseGradle(bool b); Utils::FileName adbToolPath() const; Utils::FileName androidToolPath() const; - Utils::Environment androidToolEnvironment() const; Utils::FileName antToolPath() const; Utils::FileName emulatorToolPath() const; - + Utils::FileName sdkManagerToolPath() const; + Utils::FileName avdManagerToolPath() const; Utils::FileName gccPath(const ProjectExplorer::Abi &abi, Core::Id lang, const QString &ndkToolChainVersion) const; + Utils::FileName gdbPath(const ProjectExplorer::Abi &abi, const QString &ndkToolChainVersion) const; Utils::FileName keytoolPath() const; @@ -135,7 +157,8 @@ public: class CreateAvdInfo { public: - QString target; + bool isValid() const { return target.isValid() && !name.isEmpty(); } + SdkPlatform target; QString name; QString abi; int sdcardSize = 0; @@ -143,19 +166,10 @@ public: }; CreateAvdInfo gatherCreateAVDInfo(QWidget *parent, int minApiLevel = 0, QString targetArch = QString()) const; - QFuture<CreateAvdInfo> createAVD(CreateAvdInfo info) const; - bool removeAVD(const QString &name) const; QVector<AndroidDeviceInfo> connectedDevices(QString *error = 0) const; static QVector<AndroidDeviceInfo> connectedDevices(const QString &adbToolPath, QString *error = 0); - QFuture<QVector<AndroidDeviceInfo> > androidVirtualDevicesFuture() const; - static QVector<AndroidDeviceInfo> androidVirtualDevices(const QString &androidTool, const Utils::Environment &environment); - - QString startAVD(const QString &name) const; - bool startAVDAsync(const QString &avdName) const; - QString findAvd(const QString &avdName) const; - QString waitForAvd(const QString &avdName, const QFutureInterface<bool> &fi = QFutureInterface<bool>()) const; QString bestNdkPlatformMatch(int target) const; static ProjectExplorer::Abi abiForToolChainPrefix(const QString &toolchainPrefix); @@ -166,13 +180,10 @@ public: QString getProductModel(const QString &device) const; enum class OpenGl { Enabled, Disabled, Unknown }; OpenGl getOpenGLEnabled(const QString &emulator) const; - bool hasFinishedBooting(const QString &device) const; - bool waitForBooted(const QString &serialNumber, const QFutureInterface<bool> &fi) const; bool isConnected(const QString &serialNumber) const; SdkPlatform highestAndroidSdk() const; private: - static CreateAvdInfo createAVDImpl(CreateAvdInfo info, Utils::FileName androidToolPath, Utils::Environment env); static QString getDeviceProperty(const QString &adbToolPath, const QString &device, const QString &property); Utils::FileName toolPath(const ProjectExplorer::Abi &abi, const QString &ndkToolChainVersion) const; @@ -196,12 +207,11 @@ private: QStringList m_makeExtraSearchDirectories; unsigned m_partitionSize = 1024; bool m_automaticKitCreation = true; - bool m_useGradle = false; + bool m_useGradle = true; // Ant builds are deprecated. //caches mutable bool m_availableSdkPlatformsUpToDate = false; - mutable QVector<SdkPlatform> m_availableSdkPlatforms; - static bool sortSdkPlatformByApiLevel(const SdkPlatform &a, const SdkPlatform &b); + mutable SdkPlatformList m_availableSdkPlatforms; mutable bool m_NdkInformationUpToDate = false; mutable QString m_toolchainHost; @@ -247,3 +257,5 @@ private: }; } // namespace Android +Q_DECLARE_METATYPE(Android::SdkPlatform) + diff --git a/src/plugins/android/androiddebugsupport.cpp b/src/plugins/android/androiddebugsupport.cpp index 76a3493c7e..aaffde81d4 100644 --- a/src/plugins/android/androiddebugsupport.cpp +++ b/src/plugins/android/androiddebugsupport.cpp @@ -180,6 +180,16 @@ AndroidDebugSupport::AndroidDebugSupport(RunControl *runControl) [this](const QString &output) { this->runControl()->showMessage(output, AppOutput); }); + + QTC_ASSERT(runControl, return); + auto formatter = qobject_cast<AndroidOutputFormatter*>(runControl->outputFormatter()); + QTC_ASSERT(formatter, return); + connect(m_runner, &AndroidRunner::pidFound, formatter, &AndroidOutputFormatter::appendPid); + connect(m_runner, &AndroidRunner::pidLost, formatter, &AndroidOutputFormatter::removePid); + connect(m_runner, &AndroidRunner::remoteProcessFinished, formatter, + [formatter] { + formatter->removePid(-1); + }); } void AndroidDebugSupport::handleRemoteProcessStarted(Utils::Port gdbServerPort, Utils::Port qmlPort) diff --git a/src/plugins/android/androiddeployqtstep.cpp b/src/plugins/android/androiddeployqtstep.cpp index 98efc64ffe..b4bfbc7bc1 100644 --- a/src/plugins/android/androiddeployqtstep.cpp +++ b/src/plugins/android/androiddeployqtstep.cpp @@ -33,6 +33,7 @@ #include "androidmanager.h" #include "androidconstants.h" #include "androidglobal.h" +#include "androidavdmanager.h" #include <coreplugin/fileutils.h> #include <coreplugin/icore.h> @@ -262,8 +263,9 @@ bool AndroidDeployQtStep::init(QList<const BuildStep *> &earlierSteps) m_adbPath = AndroidConfigurations::currentConfig().adbToolPath().toString(); - if (AndroidConfigurations::currentConfig().findAvd(m_avdName).isEmpty()) - AndroidConfigurations::currentConfig().startAVDAsync(m_avdName); + AndroidAvdManager avdManager; + if (avdManager.findAvd(m_avdName).isEmpty()) + avdManager.startAvdAsync(m_avdName); return true; } @@ -414,7 +416,7 @@ void AndroidDeployQtStep::slotSetSerialNumber(const QString &serialNumber) void AndroidDeployQtStep::run(QFutureInterface<bool> &fi) { if (!m_avdName.isEmpty()) { - QString serialNumber = AndroidConfigurations::currentConfig().waitForAvd(m_avdName, fi); + QString serialNumber = AndroidAvdManager().waitForAvd(m_avdName, fi); if (serialNumber.isEmpty()) { reportRunResult(fi, false); return; diff --git a/src/plugins/android/androiddevicedialog.cpp b/src/plugins/android/androiddevicedialog.cpp index 519f125f45..451abf2dc5 100644 --- a/src/plugins/android/androiddevicedialog.cpp +++ b/src/plugins/android/androiddevicedialog.cpp @@ -25,6 +25,7 @@ #include "androiddevicedialog.h" #include "androidmanager.h" +#include "androidavdmanager.h" #include "ui_androiddevicedialog.h" #include <utils/environment.h> @@ -423,7 +424,8 @@ AndroidDeviceDialog::AndroidDeviceDialog(int apiLevel, const QString &abi, Andro m_ui(new Ui::AndroidDeviceDialog), m_apiLevel(apiLevel), m_abi(abi), - m_defaultDevice(serialNumber) + m_defaultDevice(serialNumber), + m_avdManager(new AndroidAvdManager) { m_ui->setupUi(this); m_ui->deviceView->setModel(m_model); @@ -515,7 +517,7 @@ void AndroidDeviceDialog::refreshDeviceList() m_ui->refreshDevicesButton->setEnabled(false); m_progressIndicator->show(); m_connectedDevices = AndroidConfig::connectedDevices(AndroidConfigurations::currentConfig().adbToolPath().toString()); - m_futureWatcherRefreshDevices.setFuture(AndroidConfigurations::currentConfig().androidVirtualDevicesFuture()); + m_futureWatcherRefreshDevices.setFuture(m_avdManager->avdList()); } void AndroidDeviceDialog::devicesRefreshed() @@ -530,7 +532,7 @@ void AndroidDeviceDialog::devicesRefreshed() serialNumber = deviceType == AndroidDeviceInfo::Hardware ? info.serialNumber : info.avdname; } - QVector<AndroidDeviceInfo> devices = m_futureWatcherRefreshDevices.result(); + AndroidDeviceInfoList devices = m_futureWatcherRefreshDevices.result(); QSet<QString> startedAvds = Utils::transform<QSet>(m_connectedDevices, [] (const AndroidDeviceInfo &info) { return info.avdname; @@ -583,12 +585,12 @@ void AndroidDeviceDialog::createAvd() m_ui->createAVDButton->setEnabled(false); AndroidConfig::CreateAvdInfo info = AndroidConfigurations::currentConfig().gatherCreateAVDInfo(this, m_apiLevel, m_abi); - if (info.target.isEmpty()) { + if (!info.target.isValid()) { m_ui->createAVDButton->setEnabled(true); return; } - m_futureWatcherAddDevice.setFuture(AndroidConfigurations::currentConfig().createAVD(info)); + m_futureWatcherAddDevice.setFuture(m_avdManager->createAvd(info)); } void AndroidDeviceDialog::avdAdded() diff --git a/src/plugins/android/androiddevicedialog.h b/src/plugins/android/androiddevicedialog.h index b4d7940048..8f957aa0b2 100644 --- a/src/plugins/android/androiddevicedialog.h +++ b/src/plugins/android/androiddevicedialog.h @@ -32,6 +32,8 @@ #include <QFutureWatcher> #include <QTime> +#include <memory> + QT_BEGIN_NAMESPACE class QModelIndex; QT_END_NAMESPACE @@ -41,6 +43,7 @@ namespace Utils { class ProgressIndicator; } namespace Android { namespace Internal { +class AndroidAvdManager; class AndroidDeviceModel; namespace Ui { class AndroidDeviceDialog; } @@ -74,9 +77,10 @@ private: QString m_abi; QString m_avdNameFromAdd; QString m_defaultDevice; + std::unique_ptr<AndroidAvdManager> m_avdManager; QVector<AndroidDeviceInfo> m_connectedDevices; QFutureWatcher<AndroidConfig::CreateAvdInfo> m_futureWatcherAddDevice; - QFutureWatcher<QVector<AndroidDeviceInfo>> m_futureWatcherRefreshDevices; + QFutureWatcher<AndroidDeviceInfoList> m_futureWatcherRefreshDevices; }; } diff --git a/src/plugins/android/androidmanager.cpp b/src/plugins/android/androidmanager.cpp index 0459355b04..ee57399060 100644 --- a/src/plugins/android/androidmanager.cpp +++ b/src/plugins/android/androidmanager.cpp @@ -34,6 +34,7 @@ #include "androidqtsupport.h" #include "androidqtversion.h" #include "androidbuildapkstep.h" +#include "androidavdmanager.h" #include <coreplugin/documentmanager.h> #include <coreplugin/messagemanager.h> @@ -60,11 +61,13 @@ #include <QMessageBox> #include <QApplication> #include <QDomDocument> +#include <QVersionNumber> namespace { const QLatin1String AndroidManifestName("AndroidManifest.xml"); const QLatin1String AndroidDefaultPropertiesName("project.properties"); const QLatin1String AndroidDeviceSn("AndroidDeviceSerialNumber"); + const QLatin1String ApiLevelKey("AndroidVersion.ApiLevel"); } // anonymous namespace @@ -343,7 +346,7 @@ void AndroidManager::cleanLibsOnDevice(ProjectExplorer::Target *target) QString deviceSerialNumber = info.serialNumber; if (info.type == AndroidDeviceInfo::Emulator) { - deviceSerialNumber = AndroidConfigurations::currentConfig().startAVD(info.avdname); + deviceSerialNumber = AndroidAvdManager().startAvd(info.avdname); if (deviceSerialNumber.isEmpty()) Core::MessageManager::write(tr("Starting Android virtual device failed.")); } @@ -372,7 +375,7 @@ void AndroidManager::installQASIPackage(ProjectExplorer::Target *target, const Q QString deviceSerialNumber = info.serialNumber; if (info.type == AndroidDeviceInfo::Emulator) { - deviceSerialNumber = AndroidConfigurations::currentConfig().startAVD(info.avdname); + deviceSerialNumber = AndroidAvdManager().startAvd(info.avdname); if (deviceSerialNumber.isEmpty()) Core::MessageManager::write(tr("Starting Android virtual device failed.")); } @@ -565,18 +568,33 @@ bool AndroidManager::updateGradleProperties(ProjectExplorer::Target *target) gradleProperties["buildDir"] = ".build"; gradleProperties["androidCompileSdkVersion"] = buildTargetSDK(target).split(QLatin1Char('-')).last().toLocal8Bit(); if (gradleProperties["androidBuildToolsVersion"].isEmpty()) { - QString maxVersion; + QVersionNumber maxVersion; QDir buildToolsDir(AndroidConfigurations::currentConfig().sdkLocation().appendPath(QLatin1String("build-tools")).toString()); foreach (const QFileInfo &file, buildToolsDir.entryList(QDir::Dirs|QDir::NoDotAndDotDot)) { - QString ver(file.fileName()); + QVersionNumber ver = QVersionNumber::fromString(file.fileName()); if (maxVersion < ver) maxVersion = ver; } - if (maxVersion.isEmpty()) + if (maxVersion.isNull()) return false; - gradleProperties["androidBuildToolsVersion"] = maxVersion.toLocal8Bit(); + gradleProperties["androidBuildToolsVersion"] = maxVersion.toString().toLocal8Bit(); } return mergeGradleProperties(gradlePropertiesPath, gradleProperties); } +int AndroidManager::findApiLevel(const Utils::FileName &platformPath) +{ + int apiLevel = -1; + Utils::FileName propertiesPath = platformPath; + propertiesPath.appendPath("/source.properties"); + if (propertiesPath.exists()) { + QSettings sdkProperties(propertiesPath.toString(), QSettings::IniFormat); + bool validInt = false; + apiLevel = sdkProperties.value(ApiLevelKey).toInt(&validInt); + if (!validInt) + apiLevel = -1; + } + return apiLevel; +} + } // namespace Android diff --git a/src/plugins/android/androidmanager.h b/src/plugins/android/androidmanager.h index 2b79cb4c6a..ad50f45529 100644 --- a/src/plugins/android/androidmanager.h +++ b/src/plugins/android/androidmanager.h @@ -89,6 +89,7 @@ public: static AndroidQtSupport *androidQtSupport(ProjectExplorer::Target *target); static bool useGradle(ProjectExplorer::Target *target); static bool updateGradleProperties(ProjectExplorer::Target *target); + static int findApiLevel(const Utils::FileName &platformPath); }; } // namespace Android diff --git a/src/plugins/android/androidrunconfiguration.cpp b/src/plugins/android/androidrunconfiguration.cpp index d2d7844e5c..54f6449753 100644 --- a/src/plugins/android/androidrunconfiguration.cpp +++ b/src/plugins/android/androidrunconfiguration.cpp @@ -29,16 +29,205 @@ #include "androidmanager.h" #include <projectexplorer/kitinformation.h> +#include <projectexplorer/projectexplorer.h> +#include <projectexplorer/projectexplorersettings.h> #include <projectexplorer/target.h> #include <qtsupport/qtoutputformatter.h> #include <qtsupport/qtkitinformation.h> +#include <QPlainTextEdit> +#include <QRegularExpression> +#include <QToolButton> #include <utils/qtcassert.h> +#include <utils/utilsicons.h> using namespace ProjectExplorer; namespace Android { +static QRegularExpression logCatRegExp("([0-9\\-]*\\s+[0-9\\-:.]*)" // 1. time + "\\s*" + "([DEIVWF])" // 2. log level + "\\/" + "(.*)" // 3. TAG + "\\(\\s*" + "(\\d+)" // 4. PID + "\\)\\:\\s" + "(.*)"); // 5. Message + +AndroidOutputFormatter::AndroidOutputFormatter(Project *project) + : QtSupport::QtOutputFormatter(project) + , m_filtersButton(new QToolButton) +{ + auto filtersMenu = new QMenu(m_filtersButton.data()); + + m_filtersButton->setToolTip(tr("Filters")); + m_filtersButton->setIcon(Utils::Icons::FILTER.icon()); + m_filtersButton->setProperty("noArrow", true); + m_filtersButton->setAutoRaise(true); + m_filtersButton->setPopupMode(QToolButton::InstantPopup); + m_filtersButton->setMenu(filtersMenu); + + auto logsMenu = filtersMenu->addMenu(tr("Log Level")); + addLogAction(All, logsMenu, tr("All")); + addLogAction(Verbose, logsMenu, tr("Verbose")); + addLogAction(Info, logsMenu, tr("Info")); + addLogAction(Debug, logsMenu, tr("Debug")); + addLogAction(Warning, logsMenu, tr("Warning")); + addLogAction(Error, logsMenu, tr("Error")); + addLogAction(Fatal, logsMenu, tr("Fatal")); + updateLogMenu(); + m_appsMenu = filtersMenu->addMenu(tr("Applications")); + appendPid(-1, tr("All")); +} + +AndroidOutputFormatter::~AndroidOutputFormatter() +{} + +QList<QWidget *> AndroidOutputFormatter::toolbarWidgets() const +{ + return QList<QWidget *>{m_filtersButton.data()}; +} + +void AndroidOutputFormatter::appendMessage(const QString &text, Utils::OutputFormat format) +{ + if (text.isEmpty()) + return; + + CachedLine line; + line.content = text; + + if (format < Utils::StdOutFormat) { + line.level = SkipFiltering; + line.pid = -1; + } else { + QRegularExpressionMatch match = logCatRegExp.match(text); + if (!match.hasMatch()) + return; + line.level = None; + + switch (match.captured(2).toLatin1()[0]) { + case 'D': line.level = Debug; break; + case 'I': line.level = Info; break; + case 'V': line.level = Verbose; break; + case 'W': line.level = Warning; break; + case 'E': line.level = Error; break; + case 'F': line.level = Fatal; break; + default: return; + } + line.pid = match.captured(4).toLongLong(); + } + + m_cachedLines.append(line); + if (m_cachedLines.size() > ProjectExplorerPlugin::projectExplorerSettings().maxAppOutputLines) + m_cachedLines.pop_front(); + + filterMessage(line); +} + +void AndroidOutputFormatter::clear() +{ + m_cachedLines.clear(); +} + +void AndroidOutputFormatter::appendPid(qint64 pid, const QString &name) +{ + if (m_pids.contains(pid)) + return; + + auto action = m_appsMenu->addAction(name); + m_pids[pid] = action; + action->setCheckable(true); + action->setChecked(pid != -1); + connect(action, &QAction::triggered, this, &AndroidOutputFormatter::applyFilter); + applyFilter(); +} + +void AndroidOutputFormatter::removePid(qint64 pid) +{ + if (pid == -1) { + for (auto action : m_pids) + m_appsMenu->removeAction(action); + m_pids.clear(); + } else { + m_appsMenu->removeAction(m_pids[pid]); + m_pids.remove(pid); + } +} + +void AndroidOutputFormatter::updateLogMenu(LogLevel set, LogLevel reset) +{ + m_logLevelFlags |= set; + m_logLevelFlags &= ~reset; + for (const auto & pair : m_logLevels) + pair.second->setChecked((m_logLevelFlags & pair.first) == pair.first); + + applyFilter(); +} + +void AndroidOutputFormatter::filterMessage(const CachedLine &line) +{ + if (line.level == SkipFiltering || m_pids[-1]->isChecked()) { + QtOutputFormatter::appendMessage(line.content, Utils::NormalMessageFormat); + } else { + // Filter Log Level + if (!(m_logLevelFlags & line.level)) + return; + + // Filter PIDs + if (!m_pids[-1]->isChecked()) { + auto it = m_pids.find(line.pid); + if (it == m_pids.end() || !(*it)->isChecked()) + return; + } + + Utils::OutputFormat format = Utils::NormalMessageFormat; + switch (line.level) { + case Debug: + format = Utils::DebugFormat; + break; + case Info: + case Verbose: + format = Utils::StdOutFormat; + break; + + case Warning: + case Error: + case Fatal: + format = Utils::StdErrFormat; + break; + default: + return; + } + + Utils::OutputFormatter::appendMessage(line.content, format); + } +} + +void AndroidOutputFormatter::applyFilter() +{ + if (!plainTextEdit()) + return; + + plainTextEdit()->clear(); + if (!m_pids[-1]->isChecked()) { + bool allOn = true; + for (auto action : m_pids) { + if (!action->isChecked()) { + allOn = false; + break; + } + } + m_pids[-1]->setChecked(allOn); + } else { + for (auto action : m_pids) + action->setChecked(true); + } + + for (const auto &line : m_cachedLines) + filterMessage(line); +} + AndroidRunConfiguration::AndroidRunConfiguration(Target *parent, Core::Id id) : RunConfiguration(parent, id) { @@ -56,7 +245,7 @@ QWidget *AndroidRunConfiguration::createConfigurationWidget() Utils::OutputFormatter *AndroidRunConfiguration::createOutputFormatter() const { - return new QtSupport::QtOutputFormatter(target()->project()); + return new AndroidOutputFormatter(target()->project()); } const QString AndroidRunConfiguration::remoteChannel() const diff --git a/src/plugins/android/androidrunconfiguration.h b/src/plugins/android/androidrunconfiguration.h index cfa54312d9..9ec42e3d78 100644 --- a/src/plugins/android/androidrunconfiguration.h +++ b/src/plugins/android/androidrunconfiguration.h @@ -28,9 +28,75 @@ #include "android_global.h" #include <projectexplorer/runconfiguration.h> +#include <qtsupport/qtoutputformatter.h> + +#include <QMenu> + +QT_BEGIN_NAMESPACE +class QToolButton; +QT_END_NAMESPACE namespace Android { +class AndroidOutputFormatter : public QtSupport::QtOutputFormatter +{ + Q_OBJECT +public: + enum LogLevel { + None = 0, + Verbose = 1, + Info = 1 << 1, + Debug = 1 << 2, + Warning = 1 << 3, + Error = 1 << 4, + Fatal = 1 << 5, + All = Verbose | Info | Debug | Warning | Error | Fatal, + SkipFiltering = ~All + }; + +public: + explicit AndroidOutputFormatter(ProjectExplorer::Project *project); + ~AndroidOutputFormatter(); + + // OutputFormatter interface + QList<QWidget*> toolbarWidgets() const override; + void appendMessage(const QString &text, Utils::OutputFormat format) override; + void clear() override; + +public slots: + void appendPid(qint64 pid, const QString &name); + void removePid(qint64 pid); + +private: + struct CachedLine { + qint64 pid; + LogLevel level; + QString content; + }; + +private: + void updateLogMenu(LogLevel set = None, LogLevel reset = None); + void filterMessage(const CachedLine &line); + + void applyFilter(); + void addLogAction(LogLevel level, QMenu *logsMenu, const QString &name) { + auto action = logsMenu->addAction(name); + m_logLevels.push_back(qMakePair(level, action)); + action->setCheckable(true); + connect(action, &QAction::triggered, this, [level, this](bool checked) { + updateLogMenu(checked ? level : None , checked ? None : level); + }); + } + +private: + int m_logLevelFlags = All; + QVector<QPair<LogLevel, QAction*>> m_logLevels; + QHash<qint64, QAction*> m_pids; + QScopedPointer<QToolButton> m_filtersButton; + QMenu *m_appsMenu; + QList<CachedLine> m_cachedLines; +}; + class ANDROID_EXPORT AndroidRunConfiguration : public ProjectExplorer::RunConfiguration { Q_OBJECT diff --git a/src/plugins/android/androidruncontrol.cpp b/src/plugins/android/androidruncontrol.cpp index 2ac929a0b1..d2a4bb1ea6 100644 --- a/src/plugins/android/androidruncontrol.cpp +++ b/src/plugins/android/androidruncontrol.cpp @@ -62,6 +62,13 @@ void AndroidRunControl::start() this, &AndroidRunControl::handleRemoteOutput); connect(m_runner, &AndroidRunner::remoteProcessFinished, this, &AndroidRunControl::handleRemoteProcessFinished); + + auto formatter = static_cast<AndroidOutputFormatter *>(outputFormatter()); + connect(m_runner, &AndroidRunner::pidFound, + formatter, &AndroidOutputFormatter::appendPid); + connect(m_runner, &AndroidRunner::pidLost, + formatter, &AndroidOutputFormatter::removePid); + appendMessage(tr("Starting remote process."), Utils::NormalMessageFormat); m_runner->setRunnable(runnable().as<AndroidRunnable>()); m_runner->start(); diff --git a/src/plugins/android/androidrunner.cpp b/src/plugins/android/androidrunner.cpp index 778d348b68..9208e69953 100644 --- a/src/plugins/android/androidrunner.cpp +++ b/src/plugins/android/androidrunner.cpp @@ -31,6 +31,7 @@ #include "androidglobal.h" #include "androidrunconfiguration.h" #include "androidmanager.h" +#include "androidavdmanager.h" #include <debugger/debuggerrunconfigurationaspect.h> #include <projectexplorer/projectexplorer.h> @@ -51,6 +52,7 @@ #include <QTime> #include <QTcpServer> #include <QTcpSocket> +#include <QRegularExpression> using namespace std; using namespace std::placeholders; @@ -125,10 +127,10 @@ namespace Internal { const int MIN_SOCKET_HANDSHAKE_PORT = 20001; const int MAX_SOCKET_HANDSHAKE_PORT = 20999; -static const QString pidScript = QStringLiteral("for p in /proc/[0-9]*; " - "do cat <$p/cmdline && echo :${p##*/}; done"); -static const QString pidPollingScript = QStringLiteral("while true; do sleep 1; " - "cat /proc/%1/cmdline > /dev/null; done"); +static const QString pidScript = QStringLiteral("input keyevent KEYCODE_WAKEUP; " + "while true; do sleep 1; echo \"=\"; " + "for p in /proc/[0-9]*; " + "do cat <$p/cmdline && echo :${p##*/}; done; done"); static const QString regExpLogcat = QStringLiteral("[0-9\\-]*" // date "\\s+" @@ -146,55 +148,26 @@ static const QString regExpLogcat = QStringLiteral("[0-9\\-]*" // date ); static int APP_START_TIMEOUT = 45000; -static bool isTimedOut(const chrono::high_resolution_clock::time_point &start, - int msecs = APP_START_TIMEOUT) -{ - bool timedOut = false; - auto end = chrono::high_resolution_clock::now(); - if (chrono::duration_cast<chrono::milliseconds>(end-start).count() > msecs) - timedOut = true; - return timedOut; -} - -static qint64 extractPID(const QByteArray &output, const QString &packageName) -{ - qint64 pid = -1; - foreach (auto tuple, output.split('\n')) { - tuple = tuple.simplified(); - if (!tuple.isEmpty()) { - auto parts = tuple.split(':'); - QString commandName = QString::fromLocal8Bit(parts.first()); - if (parts.length() == 2 && commandName == packageName) { - pid = parts.last().toLongLong(); - break; - } - } - } - return pid; -} +enum class PidStatus { + Found, + Lost +}; -void findProcessPID(QFutureInterface<qint64> &fi, const QString &adbPath, - QStringList selector, const QString &packageName) +struct PidInfo { - if (packageName.isEmpty()) - return; - - qint64 processPID = -1; - chrono::high_resolution_clock::time_point start = chrono::high_resolution_clock::now(); - do { - QThread::msleep(200); - const QByteArray out = Utils::SynchronousProcess() - .runBlocking(adbPath, selector << QStringLiteral("shell") << pidScript) - .allRawOutput(); - processPID = extractPID(out, packageName); - } while (processPID == -1 && !isTimedOut(start) && !fi.isCanceled()); - - if (!fi.isCanceled()) - fi.reportResult(processPID); -} + PidInfo(qint64 pid = -1, PidStatus status = PidStatus::Lost, QString name = {}) + : pid(pid) + , status(status) + , name(name) + {} + qint64 pid; + PidStatus status; + QString name; +}; static void deleter(QProcess *p) { + p->disconnect(); p->kill(); p->waitForFinished(); // Might get deleted from its own signal handler. @@ -228,29 +201,31 @@ signals: void remoteOutput(const QString &output); void remoteErrorOutput(const QString &output); + void pidFound(qint64, const QString &name); + void pidLost(qint64); private: - void onProcessIdChanged(qint64 pid); + void findProcessPids(); + void onProcessIdChanged(PidInfo pidInfo); void logcatReadStandardError(); void logcatReadStandardOutput(); void adbKill(qint64 pid); QStringList selector() const { return m_selector; } void forceStop(); - void findPs(); void logcatProcess(const QByteArray &text, QByteArray &buffer, bool onlyError); bool adbShellAmNeedsQuotes(); bool runAdb(const QStringList &args, QString *exitMessage = nullptr, int timeoutS = 10); + int deviceSdkVersion(); // Create the processes and timer in the worker thread, for correct thread affinity std::unique_ptr<QProcess, decltype(&deleter)> m_adbLogcatProcess; - std::unique_ptr<QProcess, decltype(&deleter)> m_psIsAlive; + std::unique_ptr<QProcess, decltype(&deleter)> m_pidsFinderProcess; QScopedPointer<QTcpSocket> m_socket; QByteArray m_stdoutBuffer; QByteArray m_stderrBuffer; - QFuture<qint64> m_pidFinder; - qint64 m_processPID = -1; + QSet<qint64> m_processPids; bool m_useCppDebugger = false; QmlDebug::QmlDebugServicesPreset m_qmlDebugServices; Utils::Port m_localGdbServerPort; // Local end of forwarded debug socket. @@ -261,20 +236,20 @@ private: QString m_gdbserverSocket; QString m_adb; QStringList m_selector; - QRegExp m_logCatRegExp; DebugHandShakeType m_handShakeMethod = SocketHandShake; bool m_customPort = false; QString m_packageName; int m_socketHandShakePort = MIN_SOCKET_HANDSHAKE_PORT; + QByteArray m_pidsBuffer; + QScopedPointer<QTimer> m_timeoutTimer; }; AndroidRunnerWorker::AndroidRunnerWorker(AndroidRunConfiguration *runConfig, Core::Id runMode, const QString &packageName, const QStringList &selector) : m_adbLogcatProcess(nullptr, deleter) - , m_psIsAlive(nullptr, deleter) + , m_pidsFinderProcess(nullptr, deleter) , m_selector(selector) - , m_logCatRegExp(regExpLogcat) , m_packageName(packageName) { Debugger::DebuggerRunConfigurationAspect *aspect @@ -338,23 +313,18 @@ AndroidRunnerWorker::AndroidRunnerWorker(AndroidRunConfiguration *runConfig, Cor AndroidRunnerWorker::~AndroidRunnerWorker() { - if (!m_pidFinder.isFinished()) - m_pidFinder.cancel(); } void AndroidRunnerWorker::forceStop() { runAdb(selector() << "shell" << "am" << "force-stop" << m_packageName, nullptr, 30); - // try killing it via kill -9 - const QByteArray out = Utils::SynchronousProcess() - .runBlocking(m_adb, selector() << QStringLiteral("shell") << pidScript) - .allRawOutput(); - - qint64 pid = extractPID(out.simplified(), m_packageName); - if (pid != -1) { - adbKill(pid); + for (auto it = m_processPids.constBegin(); it != m_processPids.constEnd(); ++it) { + emit pidLost(*it); + adbKill(*it); } + m_processPids.clear(); + m_pidsBuffer.clear(); } void AndroidRunnerWorker::asyncStart(const QString &intentName, @@ -368,8 +338,12 @@ void AndroidRunnerWorker::asyncStart(const QString &intentName, this, &AndroidRunnerWorker::logcatReadStandardOutput); connect(logcatProcess.get(), &QProcess::readyReadStandardError, this, &AndroidRunnerWorker::logcatReadStandardError); + // Its assumed that the device or avd returned by selector() is online. - logcatProcess->start(m_adb, selector() << "logcat"); + QStringList logcatArgs = selector() << "logcat" << "-v" << "time"; + if (deviceSdkVersion() > 20) + logcatArgs << "-T" << "0"; + logcatProcess->start(m_adb, logcatArgs); QString errorMessage; @@ -507,9 +481,20 @@ void AndroidRunnerWorker::asyncStart(const QString &intentName, QTC_ASSERT(!m_adbLogcatProcess, /**/); m_adbLogcatProcess = std::move(logcatProcess); - m_pidFinder = Utils::onResultReady(Utils::runAsync(&findProcessPID, m_adb, selector(), - m_packageName), - bind(&AndroidRunnerWorker::onProcessIdChanged, this, _1)); + + m_timeoutTimer.reset(new QTimer); + m_timeoutTimer->setSingleShot(true); + connect(m_timeoutTimer.data(), &QTimer::timeout, + this,[this] { onProcessIdChanged(PidInfo{}); }); + m_timeoutTimer->start(APP_START_TIMEOUT); + + m_pidsFinderProcess.reset(new QProcess); + m_pidsFinderProcess->setProcessChannelMode(QProcess::MergedChannels); + connect(m_pidsFinderProcess.get(), &QProcess::readyRead, this, &AndroidRunnerWorker::findProcessPids); + connect(m_pidsFinderProcess.get(), + static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), + this, [this] { onProcessIdChanged(PidInfo{}); }); + m_pidsFinderProcess->start(m_adb, selector() << "shell" << pidScript); } bool AndroidRunnerWorker::adbShellAmNeedsQuotes() @@ -545,6 +530,19 @@ bool AndroidRunnerWorker::runAdb(const QStringList &args, QString *exitMessage, return response.result == Utils::SynchronousProcessResponse::Finished; } +int AndroidRunnerWorker::deviceSdkVersion() +{ + Utils::SynchronousProcess adb; + adb.setTimeoutS(10); + Utils::SynchronousProcessResponse response + = adb.run(m_adb, selector() << "shell" << "getprop" << "ro.build.version.sdk"); + if (response.result == Utils::SynchronousProcessResponse::StartFailed + || response.result != Utils::SynchronousProcessResponse::Finished) + return -1; + + return response.allOutput().trimmed().toInt(); +} + void AndroidRunnerWorker::handleRemoteDebuggerRunning() { if (m_useCppDebugger) { @@ -558,21 +556,79 @@ void AndroidRunnerWorker::handleRemoteDebuggerRunning() runAdb(selector() << "push" << tmp.fileName() << m_pongFile); } - QTC_CHECK(m_processPID != -1); + QTC_CHECK(!m_processPids.isEmpty()); } emit remoteProcessStarted(m_localGdbServerPort, m_qmlPort); } -void AndroidRunnerWorker::asyncStop(const QVector<QStringList> &adbCommands) +void AndroidRunnerWorker::findProcessPids() { - if (!m_pidFinder.isFinished()) - m_pidFinder.cancel(); + static QMap<qint64, QByteArray> extractedPids; + static auto oldPids = m_processPids; - if (m_processPID != -1) { - forceStop(); + m_pidsBuffer += m_pidsFinderProcess->readAll(); + while (!m_pidsBuffer.isEmpty()) { + const int to = m_pidsBuffer.indexOf('\n'); + if (to < 0) + break; + + if (to == 0) { + m_pidsBuffer = m_pidsBuffer.mid(1); + continue; + } + + // = is used to delimit ps outputs + // is needed to know when an existins PID is killed + if (m_pidsBuffer[0] != '=') { + QByteArray tuple = m_pidsBuffer.left(to + 1).simplified(); + QList<QByteArray> parts = tuple.split(':'); + QByteArray commandName = parts.takeFirst(); + if (QString::fromLocal8Bit(commandName) == m_packageName) { + auto pid = parts.last().toLongLong(); + if (!m_processPids.contains(pid)) { + extractedPids[pid] = commandName + (parts.length() == 2 + ? ":" + parts.first() : QByteArray{}); + } else { + oldPids.remove(pid); + } + } + } else { + // Add new PIDs + for (auto it = extractedPids.constBegin(); it != extractedPids.constEnd(); ++it) { + onProcessIdChanged(PidInfo(it.key(), PidStatus::Found, + QString::fromLocal8Bit(it.value()))); + } + extractedPids.clear(); + + // Remove the dead ones + for (auto it = oldPids.constBegin(); it != oldPids.constEnd(); ++it) + onProcessIdChanged(PidInfo(*it, PidStatus::Lost)); + + // Save the current non dead PIDs + oldPids = m_processPids; + if (m_processPids.isEmpty()) { + extractedPids.clear(); + m_pidsBuffer.clear(); + break; + } + } + m_pidsBuffer = m_pidsBuffer.mid(to + 1); } +} + +void AndroidRunnerWorker::asyncStop(const QVector<QStringList> &adbCommands) +{ + m_timeoutTimer.reset(); + m_pidsFinderProcess.reset(); + if (!m_processPids.isEmpty()) + forceStop(); + foreach (const QStringList &entry, adbCommands) runAdb(selector() << entry); + + m_adbLogcatProcess.reset(); + emit remoteProcessFinished(QLatin1String("\n\n") + + tr("\"%1\" terminated.").arg(m_packageName)); } void AndroidRunnerWorker::setAdbParameters(const QString &packageName, const QStringList &selector) @@ -594,58 +650,48 @@ void AndroidRunnerWorker::logcatProcess(const QByteArray &text, QByteArray &buff buffer.clear(); } - QString pidString = QString::number(m_processPID); foreach (const QByteArray &msg, lines) { - const QString line = QString::fromUtf8(msg).trimmed() + QLatin1Char('\n'); - if (!line.contains(pidString)) - continue; - if (m_logCatRegExp.exactMatch(line)) { - // Android M - if (m_logCatRegExp.cap(1) == pidString) { - const QString &messagetype = m_logCatRegExp.cap(2); - QString output = line.mid(m_logCatRegExp.pos(2)); - - if (onlyError - || messagetype == QLatin1String("F") - || messagetype == QLatin1String("E") - || messagetype == QLatin1String("W")) - emit remoteErrorOutput(output); - else - emit remoteOutput(output); - } - } else { - if (onlyError || line.startsWith("F/") - || line.startsWith("E/") - || line.startsWith("W/")) - emit remoteErrorOutput(line); - else - emit remoteOutput(line); - } + const QString line = QString::fromUtf8(msg.trimmed()); + if (onlyError) + emit remoteErrorOutput(line); + else + emit remoteOutput(line); } } -void AndroidRunnerWorker::onProcessIdChanged(qint64 pid) +void AndroidRunnerWorker::onProcessIdChanged(PidInfo pidInfo) { // Don't write to m_psProc from a different thread QTC_ASSERT(QThread::currentThread() == thread(), return); - m_processPID = pid; - if (m_processPID == -1) { + + auto isFirst = m_processPids.isEmpty(); + if (pidInfo.status == PidStatus::Lost) { + m_processPids.remove(pidInfo.pid); + emit pidLost(pidInfo.pid); + } else { + m_processPids.insert(pidInfo.pid); + emit pidFound(pidInfo.pid, pidInfo.name); + } + + if (m_processPids.isEmpty() || pidInfo.pid == -1) { emit remoteProcessFinished(QLatin1String("\n\n") + tr("\"%1\" died.") .arg(m_packageName)); // App died/killed. Reset log and monitor processes. + forceStop(); m_adbLogcatProcess.reset(); - m_psIsAlive.reset(); - } else { + m_timeoutTimer.reset(); + } else if (isFirst) { + m_timeoutTimer.reset(); if (m_useCppDebugger) { // This will be funneled to the engine to actually start and attach // gdb. Afterwards this ends up in handleRemoteDebuggerRunning() below. QByteArray serverChannel = ':' + QByteArray::number(m_localGdbServerPort.number()); - emit remoteServerRunning(serverChannel, m_processPID); + emit remoteServerRunning(serverChannel, pidInfo.pid); } else if (m_qmlDebugServices == QmlDebug::QmlDebuggerServices) { // This will be funneled to the engine to actually start and attach // gdb. Afterwards this ends up in handleRemoteDebuggerRunning() below. QByteArray serverChannel = QByteArray::number(m_qmlPort.number()); - emit remoteServerRunning(serverChannel, m_processPID); + emit remoteServerRunning(serverChannel, pidInfo.pid); } else if (m_qmlDebugServices == QmlDebug::QmlProfilerServices) { emit remoteProcessStarted(Utils::Port(), m_qmlPort); } else { @@ -653,27 +699,18 @@ void AndroidRunnerWorker::onProcessIdChanged(qint64 pid) emit remoteProcessStarted(Utils::Port(), Utils::Port()); } logcatReadStandardOutput(); - QTC_ASSERT(!m_psIsAlive, /**/); - m_psIsAlive.reset(new QProcess); - m_psIsAlive->setProcessChannelMode(QProcess::MergedChannels); - connect(m_psIsAlive.get(), &QProcess::readyRead, [this](){ - if (!m_psIsAlive->readAll().simplified().isEmpty()) - onProcessIdChanged(-1); - }); - m_psIsAlive->start(m_adb, selector() << QStringLiteral("shell") - << pidPollingScript.arg(m_processPID)); } } void AndroidRunnerWorker::logcatReadStandardError() { - if (m_processPID != -1) + if (!m_processPids.isEmpty() && m_adbLogcatProcess) logcatProcess(m_adbLogcatProcess->readAllStandardError(), m_stderrBuffer, true); } void AndroidRunnerWorker::logcatReadStandardOutput() { - if (m_processPID != -1) + if (!m_processPids.isEmpty() && m_adbLogcatProcess) logcatProcess(m_adbLogcatProcess->readAllStandardOutput(), m_stdoutBuffer, false); } @@ -724,6 +761,10 @@ AndroidRunner::AndroidRunner(QObject *parent, RunConfiguration *runConfig, Core: this, &AndroidRunner::remoteOutput); connect(m_worker.data(), &AndroidRunnerWorker::remoteErrorOutput, this, &AndroidRunner::remoteErrorOutput); + connect(m_worker.data(), &AndroidRunnerWorker::pidFound, + this, &AndroidRunner::pidFound); + connect(m_worker.data(), &AndroidRunnerWorker::pidLost, + this, &AndroidRunner::pidLost); m_thread.start(); } @@ -791,8 +832,9 @@ void AndroidRunner::launchAVD() emit adbParametersChanged(m_androidRunnable.packageName, AndroidDeviceInfo::adbSelector(info.serialNumber)); if (info.isValid()) { - if (AndroidConfigurations::currentConfig().findAvd(info.avdname).isEmpty()) { - bool launched = AndroidConfigurations::currentConfig().startAVDAsync(info.avdname); + AndroidAvdManager avdManager; + if (avdManager.findAvd(info.avdname).isEmpty()) { + bool launched = avdManager.startAvdAsync(info.avdname); m_launchedAVDName = launched ? info.avdname:""; } else { m_launchedAVDName.clear(); @@ -803,11 +845,12 @@ void AndroidRunner::launchAVD() void AndroidRunner::checkAVD() { const AndroidConfig &config = AndroidConfigurations::currentConfig(); - QString serialNumber = config.findAvd(m_launchedAVDName); + AndroidAvdManager avdManager(config); + QString serialNumber = avdManager.findAvd(m_launchedAVDName); if (!serialNumber.isEmpty()) return; // try again on next timer hit - if (config.hasFinishedBooting(serialNumber)) { + if (avdManager.isAvdBooted(serialNumber)) { m_checkAVDTimer.stop(); AndroidManager::setDeviceSerialNumber(m_runConfig->target(), serialNumber); emit asyncStart(m_androidRunnable.intentName, m_androidRunnable.beforeStartADBCommands); diff --git a/src/plugins/android/androidrunner.h b/src/plugins/android/androidrunner.h index dd35534bd6..f30dc4754b 100644 --- a/src/plugins/android/androidrunner.h +++ b/src/plugins/android/androidrunner.h @@ -75,6 +75,9 @@ signals: void adbParametersChanged(const QString &packageName, const QStringList &selector); void avdDetected(); + void pidFound(qint64, const QString &name); + void pidLost(qint64); + private: void checkAVD(); void launchAVD(); diff --git a/src/plugins/android/androidsdkmanager.cpp b/src/plugins/android/androidsdkmanager.cpp new file mode 100644 index 0000000000..0ed23660a6 --- /dev/null +++ b/src/plugins/android/androidsdkmanager.cpp @@ -0,0 +1,337 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 "androidsdkmanager.h" + +#include "androidmanager.h" +#include "androidtoolmanager.h" + +#include "utils/algorithm.h" +#include "utils/qtcassert.h" +#include "utils/synchronousprocess.h" +#include "utils/environment.h" + +#include <QLoggingCategory> +#include <QSettings> + +namespace { +Q_LOGGING_CATEGORY(sdkManagerLog, "qtc.android.sdkManager") +} + +namespace Android { +namespace Internal { + +// Though sdk manager is introduced in 25.2.3 but the verbose mode is avaialble in 25.3.0 +// and android tool is supported in 25.2.3 +const QVersionNumber sdkManagerIntroVersion(25, 3 ,0); + +const char installLocationKey[] = "Installed Location:"; +const char apiLevelPropertyKey[] = "AndroidVersion.ApiLevel"; +const char abiPropertyKey[] = "SystemImage.Abi"; + +using namespace Utils; + +/*! + Parses the \a line for a [spaces]key[spaces]value[spaces] pattern and returns + \c true if \a key is found, false otherwise. Result is copied into \a value. + */ +static bool valueForKey(QString key, const QString &line, QString *value = nullptr) +{ + auto trimmedInput = line.trimmed(); + if (trimmedInput.startsWith(key)) { + if (value) + *value = trimmedInput.section(key, 1, 1).trimmed(); + return true; + } + return false; +} + +/*! + Runs the \c sdkmanger tool specific to configuration \a config with arguments \a args. Returns + \c true if the command is successfully executed. Output is copied into \a output. The function + blocks the calling thread. + */ +static bool sdkManagerCommand(const AndroidConfig config, const QStringList &args, QString *output) +{ + QString sdkManagerToolPath = config.sdkManagerToolPath().toString(); + SynchronousProcess proc; + SynchronousProcessResponse response = proc.runBlocking(sdkManagerToolPath, args); + if (response.result == SynchronousProcessResponse::Finished) { + if (output) + *output = response.allOutput(); + return true; + } + return false; +} + +/*! + \class SdkManagerOutputParser + \brief The SdkManagerOutputParser class is a helper class to parse the output of the \c sdkmanager + commands. + */ +class SdkManagerOutputParser +{ +public: + enum MarkerTag + { + None = 0x01, + InstalledPackagesMarker = 0x02, + AvailablePackagesMarkers = 0x04, + AvailableUpdatesMarker = 0x08, + EmptyMarker = 0x10, + PlatformMarker = 0x20, + SystemImageMarker = 0x40, + SectionMarkers = InstalledPackagesMarker | AvailablePackagesMarkers | AvailableUpdatesMarker + }; + + void parsePackageListing(const QString &output); + + SdkPlatformList m_installedPlatforms; + +private: + void compileData(); + void parsePackageData(MarkerTag packageMarker, const QStringList &data); + bool parsePlatform(const QStringList &data, SdkPlatform *platform) const; + bool parseSystemImage(const QStringList &data, SystemImage *image); + MarkerTag parseMarkers(const QString &line); + + MarkerTag m_currentSection = MarkerTag::None; + SystemImageList m_installedSystemImages; +}; + +const std::map<SdkManagerOutputParser::MarkerTag, const char *> markerTags { + {SdkManagerOutputParser::MarkerTag::InstalledPackagesMarker, "Installed packages:"}, + {SdkManagerOutputParser::MarkerTag::AvailablePackagesMarkers, "Available Packages:"}, + {SdkManagerOutputParser::MarkerTag::AvailablePackagesMarkers, "Available Updates:"}, + {SdkManagerOutputParser::MarkerTag::PlatformMarker, "platforms"}, + {SdkManagerOutputParser::MarkerTag::SystemImageMarker, "system-images"} +}; + +AndroidSdkManager::AndroidSdkManager(const AndroidConfig &config): + m_config(config), + m_parser(new SdkManagerOutputParser) +{ + QString packageListing; + if (sdkManagerCommand(config, QStringList({"--list", "--verbose"}), &packageListing)) { + m_parser->parsePackageListing(packageListing); + } +} + +AndroidSdkManager::~AndroidSdkManager() +{ + +} + +SdkPlatformList AndroidSdkManager::availableSdkPlatforms() +{ + if (m_config.sdkToolsVersion() < sdkManagerIntroVersion) { + AndroidToolManager toolManager(m_config); + return toolManager.availableSdkPlatforms(); + } + + return m_parser->m_installedPlatforms; +} + +void SdkManagerOutputParser::parsePackageListing(const QString &output) +{ + QStringList packageData; + bool collectingPackageData = false; + MarkerTag currentPackageMarker = MarkerTag::None; + + auto processCurrentPackage = [&]() { + if (collectingPackageData) { + collectingPackageData = false; + parsePackageData(currentPackageMarker, packageData); + packageData.clear(); + } + }; + + foreach (QString outputLine, output.split('\n')) { + MarkerTag marker = parseMarkers(outputLine); + + if (marker & SectionMarkers) { + // Section marker found. Update the current section being parsed. + m_currentSection = marker; + processCurrentPackage(); + continue; + } + + if (m_currentSection == None + || m_currentSection == AvailablePackagesMarkers // At this point. Not interested in + || m_currentSection == AvailableUpdatesMarker) { // available or update packages. + // Let go of verbose output utill a valid section starts. + continue; + } + + if (marker == EmptyMarker) { + // Empty marker. Occurs at the end of a package details. + // Process the collected package data, if any. + processCurrentPackage(); + continue; + } + + if (marker == None) { + if (collectingPackageData) + packageData << outputLine; // Collect data until next marker. + else + continue; + } else { + // Package marker found. + processCurrentPackage(); // New package starts. Process the collected package data, if any. + currentPackageMarker = marker; + collectingPackageData = true; + packageData << outputLine; + } + } + compileData(); + Utils::sort(m_installedPlatforms); +} + +void SdkManagerOutputParser::compileData() +{ + // Associate the system images with sdk platforms. + for (auto &image : m_installedSystemImages) { + auto findPlatfom = [image](const SdkPlatform &platform) { + return platform.apiLevel == image.apiLevel; + }; + auto itr = std::find_if(m_installedPlatforms.begin(), m_installedPlatforms.end(), findPlatfom); + if (itr != m_installedPlatforms.end()) + itr->systemImages.append(image); + } +} + +void SdkManagerOutputParser::parsePackageData(MarkerTag packageMarker, const QStringList &data) +{ + QTC_ASSERT(!data.isEmpty() && packageMarker != None, return); + + if (m_currentSection != MarkerTag::InstalledPackagesMarker) + return; // For now, only interested in installed packages. + + switch (packageMarker) { + case MarkerTag::PlatformMarker: + { + SdkPlatform platform; + if (parsePlatform(data, &platform)) + m_installedPlatforms.append(platform); + else + qCDebug(sdkManagerLog) << "Platform: Parsing failed: " << data; + } + break; + + case MarkerTag::SystemImageMarker: + { + SystemImage image; + if (parseSystemImage(data, &image)) + m_installedSystemImages.append(image); + else + qCDebug(sdkManagerLog) << "System Image: Parsing failed: " << data; + } + break; + + default: + qCDebug(sdkManagerLog) << "Unhandled package: " << markerTags.at(packageMarker); + break; + } +} + +bool SdkManagerOutputParser::parsePlatform(const QStringList &data, SdkPlatform *platform) const +{ + QTC_ASSERT(platform && !data.isEmpty(), return false); + + QStringList parts = data.at(0).split(';'); + if (parts.count() < 2) { + qCDebug(sdkManagerLog) << "Platform: Unexpected header: "<< data; + return false; + } + platform->name = parts[1]; + platform->package = data.at(0); + + foreach (QString line, data) { + QString value; + if (valueForKey(installLocationKey, line, &value)) + platform->installedLocation = Utils::FileName::fromString(value); + } + + int apiLevel = AndroidManager::findApiLevel(platform->installedLocation); + if (apiLevel != -1) + platform->apiLevel = apiLevel; + else + qCDebug(sdkManagerLog) << "Platform: Can not parse api level: "<< data; + + return apiLevel != -1; +} + +bool SdkManagerOutputParser::parseSystemImage(const QStringList &data, SystemImage *image) +{ + QTC_ASSERT(image && !data.isEmpty(), return false); + + QStringList parts = data.at(0).split(';'); + QTC_ASSERT(!data.isEmpty() && parts.count() >= 4, + qCDebug(sdkManagerLog) << "System Image: Unexpected header: " << data); + + image->package = data.at(0); + foreach (QString line, data) { + QString value; + if (valueForKey(installLocationKey, line, &value)) + image->installedLocation = Utils::FileName::fromString(value); + } + + Utils::FileName propertiesPath = image->installedLocation; + propertiesPath.appendPath("/source.properties"); + if (propertiesPath.exists()) { + // Installed System Image. + QSettings imageProperties(propertiesPath.toString(), QSettings::IniFormat); + bool validApiLevel = false; + image->apiLevel = imageProperties.value(apiLevelPropertyKey).toInt(&validApiLevel); + if (!validApiLevel) { + qCDebug(sdkManagerLog) << "System Image: Can not parse api level: "<< data; + return false; + } + image->abiName = imageProperties.value(abiPropertyKey).toString(); + } else if (parts.count() >= 4){ + image->apiLevel = parts[1].section('-', 1, 1).toInt(); + image->abiName = parts[3]; + } else { + qCDebug(sdkManagerLog) << "System Image: Can not parse: "<< data; + return false; + } + + return true; +} + +SdkManagerOutputParser::MarkerTag SdkManagerOutputParser::parseMarkers(const QString &line) +{ + if (line.isEmpty()) + return EmptyMarker; + + for (auto pair: markerTags) { + if (line.startsWith(QLatin1String(pair.second))) + return pair.first; + } + + return None; +} + +} // namespace Internal +} // namespace Android diff --git a/src/plugins/android/androidsdkmanager.h b/src/plugins/android/androidsdkmanager.h new file mode 100644 index 0000000000..2c11148072 --- /dev/null +++ b/src/plugins/android/androidsdkmanager.h @@ -0,0 +1,51 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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. +** +****************************************************************************/ +#pragma once + +#include "utils/fileutils.h" +#include "androidconfigurations.h" + +#include <memory> + +namespace Android { +namespace Internal { + +class SdkManagerOutputParser; + +class AndroidSdkManager +{ +public: + AndroidSdkManager(const AndroidConfig &config); + ~AndroidSdkManager(); + + SdkPlatformList availableSdkPlatforms(); + +private: + const AndroidConfig &m_config; + std::unique_ptr<SdkManagerOutputParser> m_parser; +}; + +} // namespace Internal +} // namespace Android diff --git a/src/plugins/android/androidsettingswidget.cpp b/src/plugins/android/androidsettingswidget.cpp index 5cdf42cdf3..a0a24ea087 100644 --- a/src/plugins/android/androidsettingswidget.cpp +++ b/src/plugins/android/androidsettingswidget.cpp @@ -30,6 +30,7 @@ #include "androidconfigurations.h" #include "androidconstants.h" #include "androidtoolchain.h" +#include "androidavdmanager.h" #include <utils/environment.h> #include <utils/hostosinfo.h> @@ -58,7 +59,7 @@ namespace Android { namespace Internal { -void AvdModel::setAvdList(const QVector<AndroidDeviceInfo> &list) +void AvdModel::setAvdList(const AndroidDeviceInfoList &list) { beginResetModel(); m_list = list; @@ -128,10 +129,13 @@ AndroidSettingsWidget::AndroidSettingsWidget(QWidget *parent) m_ndkState(NotSet), m_javaState(NotSet), m_ui(new Ui_AndroidSettingsWidget), - m_androidConfig(AndroidConfigurations::currentConfig()) + m_androidConfig(AndroidConfigurations::currentConfig()), + m_avdManager(new AndroidAvdManager(m_androidConfig)) { m_ui->setupUi(this); + m_ui->deprecatedInfoIconLabel->setPixmap(Utils::Icons::INFO.pixmap()); + connect(&m_checkGdbWatcher, &QFutureWatcherBase::finished, this, &AndroidSettingsWidget::checkGdbFinished); @@ -158,7 +162,8 @@ AndroidSettingsWidget::AndroidSettingsWidget(QWidget *parent) m_ui->AntLocationPathChooser->setPromptDialogTitle(tr("Select ant Script")); m_ui->AntLocationPathChooser->setInitialBrowsePathBackup(dir); m_ui->AntLocationPathChooser->setPromptDialogFilter(filter); - m_ui->UseGradleCheckBox->setChecked(m_androidConfig.useGrandle()); + + updateGradleBuildUi(); m_ui->OpenJDKLocationPathChooser->setFileName(m_androidConfig.openJDKLocation()); m_ui->OpenJDKLocationPathChooser->setPromptDialogTitle(tr("Select JDK Path")); @@ -463,7 +468,7 @@ void AndroidSettingsWidget::enableAvdControls() void AndroidSettingsWidget::startUpdateAvd() { disableAvdControls(); - m_virtualDevicesWatcher.setFuture(m_androidConfig.androidVirtualDevicesFuture()); + m_virtualDevicesWatcher.setFuture(m_avdManager->avdList()); } void AndroidSettingsWidget::updateAvds() @@ -476,6 +481,13 @@ void AndroidSettingsWidget::updateAvds() enableAvdControls(); } +void AndroidSettingsWidget::updateGradleBuildUi() +{ + m_ui->UseGradleCheckBox->setEnabled(m_androidConfig.antScriptsAvailable()); + m_ui->UseGradleCheckBox->setChecked(!m_androidConfig.antScriptsAvailable() || + m_androidConfig.useGrandle()); +} + bool AndroidSettingsWidget::sdkLocationIsValid() const { Utils::FileName androidExe = m_androidConfig.sdkLocation(); @@ -505,6 +517,7 @@ void AndroidSettingsWidget::saveSettings() void AndroidSettingsWidget::sdkLocationEditingFinished() { m_androidConfig.setSdkLocation(Utils::FileName::fromUserInput(m_ui->SDKLocationPathChooser->rawPath())); + updateGradleBuildUi(); check(Sdk); @@ -587,12 +600,12 @@ void AndroidSettingsWidget::addAVD() disableAvdControls(); AndroidConfig::CreateAvdInfo info = m_androidConfig.gatherCreateAVDInfo(this); - if (info.target.isEmpty()) { + if (!info.target.isValid()) { enableAvdControls(); return; } - m_futureWatcher.setFuture(m_androidConfig.createAVD(info)); + m_futureWatcher.setFuture(m_avdManager->createAvd(info)); } void AndroidSettingsWidget::avdAdded() @@ -620,13 +633,13 @@ void AndroidSettingsWidget::removeAVD() return; } - m_androidConfig.removeAVD(avdName); + m_avdManager->removeAvd(avdName); startUpdateAvd(); } void AndroidSettingsWidget::startAVD() { - m_androidConfig.startAVDAsync(m_AVDModel.avdName(m_ui->AVDTableView->currentIndex())); + m_avdManager->startAvdAsync(m_AVDModel.avdName(m_ui->AVDTableView->currentIndex())); } void AndroidSettingsWidget::avdActivated(const QModelIndex &index) @@ -671,16 +684,15 @@ void AndroidSettingsWidget::showGdbWarningDialog() void AndroidSettingsWidget::manageAVD() { - QProcess *avdProcess = new QProcess(); - connect(this, &QObject::destroyed, avdProcess, &QObject::deleteLater); - connect(avdProcess, static_cast<void (QProcess::*)(int)>(&QProcess::finished), - avdProcess, &QObject::deleteLater); - - avdProcess->setProcessEnvironment(m_androidConfig.androidToolEnvironment().toProcessEnvironment()); - QString executable = m_androidConfig.androidToolPath().toString(); - QStringList arguments = QStringList("avd"); - - avdProcess->start(executable, arguments); + if (m_avdManager->avdManagerUiToolAvailable()) { + m_avdManager->launchAvdManagerUiTool(); + } else { + QMessageBox::warning(this, tr("AVD Manager Not Available"), + tr("AVD manager UI tool is not available in the installed SDK tools" + "(version %1). Use the command line tool \"avdmanager\" for " + "advanced AVD management.") + .arg(m_androidConfig.sdkToolsVersion().toString())); + } } diff --git a/src/plugins/android/androidsettingswidget.h b/src/plugins/android/androidsettingswidget.h index 3572ffef1a..50be7c1049 100644 --- a/src/plugins/android/androidsettingswidget.h +++ b/src/plugins/android/androidsettingswidget.h @@ -33,6 +33,8 @@ #include <QAbstractTableModel> #include <QFutureWatcher> +#include <memory> + QT_BEGIN_NAMESPACE class Ui_AndroidSettingsWidget; QT_END_NAMESPACE @@ -40,11 +42,13 @@ QT_END_NAMESPACE namespace Android { namespace Internal { +class AndroidAvdManager; + class AvdModel: public QAbstractTableModel { Q_OBJECT public: - void setAvdList(const QVector<AndroidDeviceInfo> &list); + void setAvdList(const AndroidDeviceInfoList &list); QString avdName(const QModelIndex &index) const; QModelIndex indexForAvdName(const QString &avdName) const; @@ -55,7 +59,7 @@ protected: int columnCount(const QModelIndex &parent = QModelIndex()) const; private: - QVector<AndroidDeviceInfo> m_list; + AndroidDeviceInfoList m_list; }; class AndroidSettingsWidget : public QWidget @@ -91,6 +95,7 @@ private: void checkGdbFinished(); void showGdbWarningDialog(); void updateAvds(); + void updateGradleBuildUi(); private: enum Mode { Sdk = 1, Ndk = 2, Java = 4, All = Sdk | Ndk | Java }; @@ -117,8 +122,9 @@ private: QFutureWatcher<QPair<QStringList, bool>> m_checkGdbWatcher; QStringList m_gdbCheckPaths; - QFutureWatcher<QVector<AndroidDeviceInfo>> m_virtualDevicesWatcher; + QFutureWatcher<AndroidDeviceInfoList> m_virtualDevicesWatcher; QString m_lastAddedAvd; + std::unique_ptr<AndroidAvdManager> m_avdManager; }; } // namespace Internal diff --git a/src/plugins/android/androidsettingswidget.ui b/src/plugins/android/androidsettingswidget.ui index 952dc444cc..338c73da04 100644 --- a/src/plugins/android/androidsettingswidget.ui +++ b/src/plugins/android/androidsettingswidget.ui @@ -237,20 +237,30 @@ </layout> </item> <item row="8" column="1"> - <widget class="QCheckBox" name="CreateKitCheckBox"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>Automatically create kits for Android tool chains</string> + <layout class="QHBoxLayout" name="horizontalLayout_8"> + <property name="spacing"> + <number>2</number> </property> - <property name="checked"> - <bool>true</bool> + <property name="leftMargin"> + <number>0</number> </property> - </widget> + <item> + <widget class="QCheckBox" name="CreateKitCheckBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Automatically create kits for Android tool chains</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> </item> <item row="9" column="1"> <layout class="QHBoxLayout" name="horizontalLayout_2"> @@ -289,17 +299,59 @@ </layout> </item> <item row="10" column="1"> - <widget class="QCheckBox" name="UseGradleCheckBox"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> + <layout class="QHBoxLayout" name="horizontalLayout_7"> + <property name="spacing"> + <number>4</number> </property> - <property name="text"> - <string>Use Gradle instead of Ant</string> + <property name="leftMargin"> + <number>0</number> </property> - </widget> + <item> + <widget class="QCheckBox" name="UseGradleCheckBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Use Gradle instead of Ant (Ant builds are deprecated)</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="deprecatedInfoIconLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Gradle builds are forced from Android SDK tools version 25.3.0 onwards as Ant scripts are no longer available.</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> </item> <item row="11" column="0"> <widget class="QLabel" name="AntLocationLabel"> diff --git a/src/plugins/android/androidtoolmanager.cpp b/src/plugins/android/androidtoolmanager.cpp new file mode 100644 index 0000000000..a6e61d96d9 --- /dev/null +++ b/src/plugins/android/androidtoolmanager.cpp @@ -0,0 +1,346 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 "androidtoolmanager.h" + +#include "androidmanager.h" + +#include "utils/algorithm.h" +#include "utils/environment.h" +#include "utils/qtcassert.h" +#include "utils/runextensions.h" +#include "utils/synchronousprocess.h" + +#include <QLoggingCategory> + +namespace { +Q_LOGGING_CATEGORY(androidToolLog, "qtc.android.sdkManager") +} + +namespace Android { +namespace Internal { + +using namespace Utils; + +class AndroidToolOutputParser +{ +public: + void parseTargetListing(const QString &output, const FileName &sdkLocation, + SdkPlatformList *platformList); + + QList<SdkPlatform> m_installedPlatforms; +}; + +/*! + Runs the \c android tool located at \a toolPath with arguments \a args and environment \a + environment. Returns \c true for successful execution. Command's output is copied to \a + output. + */ +static bool androidToolCommand(Utils::FileName toolPath, const QStringList &args, + const Environment &environment, QString *output) +{ + QString androidToolPath = toolPath.toString(); + SynchronousProcess proc; + proc.setProcessEnvironment(environment.toProcessEnvironment()); + SynchronousProcessResponse response = proc.runBlocking(androidToolPath, args); + if (response.result == SynchronousProcessResponse::Finished) { + if (output) + *output = response.allOutput(); + return true; + } + return false; +} + +static QStringList cleanAndroidABIs(const QStringList &abis) +{ + QStringList res; + foreach (const QString &abi, abis) { + int index = abi.lastIndexOf(QLatin1Char('/')); + if (index == -1) + res << abi; + else + res << abi.mid(index + 1); + } + return res; +} + +AndroidToolManager::AndroidToolManager(const AndroidConfig &config) : + m_config(config), + m_parser(new AndroidToolOutputParser) +{ + +} + +AndroidToolManager::~AndroidToolManager() +{ + +} + +SdkPlatformList AndroidToolManager::availableSdkPlatforms() const +{ + SdkPlatformList list; + QString targetListing; + if (androidToolCommand(m_config.androidToolPath(), QStringList({"list", "target"}), + androidToolEnvironment(), &targetListing)) { + m_parser->parseTargetListing(targetListing, m_config.sdkLocation(), &list); + } else { + qCDebug(androidToolLog) << "Android tool target listing failed"; + } + return list; +} + +void AndroidToolManager::launchAvdManager() const +{ + QProcess::startDetached(m_config.androidToolPath().toString(), QStringList("avd")); +} + +QFuture<AndroidConfig::CreateAvdInfo> +AndroidToolManager::createAvd(AndroidConfig::CreateAvdInfo info) const +{ + return Utils::runAsync(&AndroidToolManager::createAvdImpl, info, + m_config.androidToolPath(), androidToolEnvironment()); +} + +bool AndroidToolManager::removeAvd(const QString &name) const +{ + SynchronousProcess proc; + proc.setTimeoutS(5); + proc.setProcessEnvironment(androidToolEnvironment().toProcessEnvironment()); + SynchronousProcessResponse response + = proc.runBlocking(m_config.androidToolPath().toString(), + QStringList({"delete", "avd", "-n", name})); + return response.result == SynchronousProcessResponse::Finished && response.exitCode == 0; +} + +QFuture<AndroidDeviceInfoList> AndroidToolManager::androidVirtualDevicesFuture() const +{ + return Utils::runAsync(&AndroidToolManager::androidVirtualDevices, + m_config.androidToolPath(), m_config.sdkLocation(), + androidToolEnvironment()); +} + +Environment AndroidToolManager::androidToolEnvironment() const +{ + Environment env = Environment::systemEnvironment(); + Utils::FileName jdkLocation = m_config.openJDKLocation(); + if (!jdkLocation.isEmpty()) { + env.set(QLatin1String("JAVA_HOME"), jdkLocation.toUserOutput()); + Utils::FileName binPath = jdkLocation; + binPath.appendPath(QLatin1String("bin")); + env.prependOrSetPath(binPath.toUserOutput()); + } + return env; +} + +AndroidConfig::CreateAvdInfo AndroidToolManager::createAvdImpl(AndroidConfig::CreateAvdInfo info, + FileName androidToolPath, + Environment env) +{ + QProcess proc; + proc.setProcessEnvironment(env.toProcessEnvironment()); + QStringList arguments; + arguments << QLatin1String("create") << QLatin1String("avd") + << QLatin1String("-t") << info.target.name + << QLatin1String("-n") << info.name + << QLatin1String("-b") << info.abi; + if (info.sdcardSize > 0) + arguments << QLatin1String("-c") << QString::fromLatin1("%1M").arg(info.sdcardSize); + proc.start(androidToolPath.toString(), arguments); + if (!proc.waitForStarted()) { + info.error = tr("Could not start process \"%1 %2\"") + .arg(androidToolPath.toString(), arguments.join(QLatin1Char(' '))); + return info; + } + QTC_CHECK(proc.state() == QProcess::Running); + proc.write(QByteArray("yes\n")); // yes to "Do you wish to create a custom hardware profile" + + QByteArray question; + while (true) { + proc.waitForReadyRead(500); + question += proc.readAllStandardOutput(); + if (question.endsWith(QByteArray("]:"))) { + // truncate to last line + int index = question.lastIndexOf(QByteArray("\n")); + if (index != -1) + question = question.mid(index); + if (question.contains("hw.gpu.enabled")) + proc.write(QByteArray("yes\n")); + else + proc.write(QByteArray("\n")); + question.clear(); + } + + if (proc.state() != QProcess::Running) + break; + } + QTC_CHECK(proc.state() == QProcess::NotRunning); + + QString errorOutput = QString::fromLocal8Bit(proc.readAllStandardError()); + // The exit code is always 0, so we need to check stderr + // For now assume that any output at all indicates a error + if (!errorOutput.isEmpty()) { + info.error = errorOutput; + } + + return info; +} + +AndroidDeviceInfoList AndroidToolManager::androidVirtualDevices(const Utils::FileName &androidTool, + const FileName &sdkLocationPath, + const Environment &environment) +{ + AndroidDeviceInfoList devices; + QString output; + if (!androidToolCommand(androidTool, QStringList({"list", "avd"}), environment, &output)) + return devices; + + QStringList avds = output.split('\n'); + if (avds.empty()) + return devices; + + while (avds.first().startsWith(QLatin1String("* daemon"))) + avds.removeFirst(); // remove the daemon logs + avds.removeFirst(); // remove "List of devices attached" header line + + bool nextLineIsTargetLine = false; + + AndroidDeviceInfo dev; + for (int i = 0; i < avds.size(); i++) { + QString line = avds.at(i); + if (!line.contains(QLatin1String("Name:"))) + continue; + + int index = line.indexOf(QLatin1Char(':')) + 2; + if (index >= line.size()) + break; + dev.avdname = line.mid(index).trimmed(); + dev.sdk = -1; + dev.cpuAbi.clear(); + ++i; + for (; i < avds.size(); ++i) { + line = avds.at(i); + if (line.contains(QLatin1String("---------"))) + break; + + if (line.contains(QLatin1String("Target:")) || nextLineIsTargetLine) { + if (line.contains(QLatin1String("Google APIs"))) { + nextLineIsTargetLine = true; + continue; + } + + nextLineIsTargetLine = false; + + int lastIndex = line.lastIndexOf(QLatin1Char(' ')); + if (lastIndex == -1) // skip line + break; + QString tmp = line.mid(lastIndex).remove(QLatin1Char(')')).trimmed(); + Utils::FileName platformPath = sdkLocationPath; + platformPath.appendPath(QString("/platforms/android-%1").arg(tmp)); + dev.sdk = AndroidManager::findApiLevel(platformPath); + } + if (line.contains(QLatin1String("Tag/ABI:"))) { + int lastIndex = line.lastIndexOf(QLatin1Char('/')) + 1; + if (lastIndex >= line.size()) + break; + dev.cpuAbi = QStringList(line.mid(lastIndex)); + } else if (line.contains(QLatin1String("ABI:"))) { + int lastIndex = line.lastIndexOf(QLatin1Char(' ')) + 1; + if (lastIndex >= line.size()) + break; + dev.cpuAbi = QStringList(line.mid(lastIndex).trimmed()); + } + } + // armeabi-v7a devices can also run armeabi code + if (dev.cpuAbi == QStringList("armeabi-v7a")) + dev.cpuAbi << QLatin1String("armeabi"); + dev.state = AndroidDeviceInfo::OkState; + dev.type = AndroidDeviceInfo::Emulator; + if (dev.cpuAbi.isEmpty() || dev.sdk == -1) + continue; + devices.push_back(dev); + } + Utils::sort(devices); + + return devices; +} + +void AndroidToolOutputParser::parseTargetListing(const QString &output, + const Utils::FileName &sdkLocation, + SdkPlatformList *platformList) +{ + if (!platformList) + return; + + auto addSystemImage = [](const QStringList& abiList, SdkPlatform &platform) { + foreach (auto imageAbi, abiList) { + SystemImage image; + image.abiName = imageAbi; + image.apiLevel = platform.apiLevel; + platform.systemImages.append(image); + } + }; + + SdkPlatform platform; + QStringList abiList; + foreach (const QString &l, output.split('\n')) { + const QString line = l.trimmed(); + if (line.startsWith(QLatin1String("id:")) && line.contains(QLatin1String("android-"))) { + int index = line.indexOf(QLatin1String("\"android-")); + if (index == -1) + continue; + QString androidTarget = line.mid(index + 1, line.length() - index - 2); + const QString tmp = androidTarget.mid(androidTarget.lastIndexOf(QLatin1Char('-')) + 1); + Utils::FileName platformPath = sdkLocation; + platformPath.appendPath(QString("/platforms/android-%1").arg(tmp)); + platform.installedLocation = platformPath; + platform.apiLevel = AndroidManager::findApiLevel(platformPath); + } else if (line.startsWith(QLatin1String("Name:"))) { + platform.name = line.mid(6); + } else if (line.startsWith(QLatin1String("Tag/ABIs :"))) { + abiList = cleanAndroidABIs(line.mid(10).trimmed().split(QLatin1String(", "))); + } else if (line.startsWith(QLatin1String("ABIs"))) { + abiList = cleanAndroidABIs(line.mid(6).trimmed().split(QLatin1String(", "))); + } else if (line.startsWith(QLatin1String("---")) || line.startsWith(QLatin1String("==="))) { + if (platform.apiLevel == -1) + continue; + + addSystemImage(abiList, platform); + *platformList << platform; + + platform = SdkPlatform(); + abiList.clear(); + } + } + + // The last parsed Platform. + if (platform.apiLevel != -1) { + addSystemImage(abiList, platform); + *platformList << platform; + } + + Utils::sort(*platformList); +} + +} // namespace Internal +} // namespace Android diff --git a/src/plugins/android/androidtoolmanager.h b/src/plugins/android/androidtoolmanager.h new file mode 100644 index 0000000000..befb095b92 --- /dev/null +++ b/src/plugins/android/androidtoolmanager.h @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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. +** +****************************************************************************/ +#pragma once + +#include "utils/fileutils.h" +#include "androidconfigurations.h" + +#include <QStringList> + +#include <memory> + +namespace Android { +class AndroidConfig; + +namespace Internal { + +class AndroidToolOutputParser; +/*! + Wraps the \c android tool's usage. The tool itself is deprecated since SDK tools version 25.3.0. + */ +class AndroidToolManager +{ + Q_DECLARE_TR_FUNCTIONS(AndroidToolManager) + +public: + AndroidToolManager(const AndroidConfig &config); + ~AndroidToolManager(); + + SdkPlatformList availableSdkPlatforms() const; + void launchAvdManager() const; + + QFuture<AndroidConfig::CreateAvdInfo> createAvd(AndroidConfig::CreateAvdInfo info) const; + bool removeAvd(const QString &name) const; + QFuture<AndroidDeviceInfoList> androidVirtualDevicesFuture() const; + +// Helper methods +private: + Utils::Environment androidToolEnvironment() const; + static AndroidConfig::CreateAvdInfo createAvdImpl(AndroidConfig::CreateAvdInfo info, + Utils::FileName androidToolPath, Utils::Environment env); + static AndroidDeviceInfoList androidVirtualDevices(const Utils::FileName &androidTool, + const Utils::FileName &sdkLlocationPath, + const Utils::Environment &environment); +private: + const AndroidConfig &m_config; + std::unique_ptr<AndroidToolOutputParser> m_parser; +}; + +} // namespace Internal +} // namespace Android diff --git a/src/plugins/android/avddialog.cpp b/src/plugins/android/avddialog.cpp index 9bfea8ef21..eb7da93e90 100644 --- a/src/plugins/android/avddialog.cpp +++ b/src/plugins/android/avddialog.cpp @@ -26,6 +26,7 @@ #include "avddialog.h" #include "androidconfigurations.h" +#include <utils/algorithm.h> #include <utils/tooltip/tooltip.h> #include <utils/utilsicons.h> @@ -67,12 +68,12 @@ AvdDialog::AvdDialog(int minApiLevel, const QString &targetArch, const AndroidCo bool AvdDialog::isValid() const { - return !name().isEmpty() && !target().isEmpty() && !abi().isEmpty(); + return !name().isEmpty() && target().isValid() && !abi().isEmpty(); } -QString AvdDialog::target() const +SdkPlatform AvdDialog::target() const { - return m_avdDialog.targetComboBox->currentText(); + return m_avdDialog.targetComboBox->currentData().value<SdkPlatform>(); } QString AvdDialog::name() const @@ -92,15 +93,23 @@ int AvdDialog::sdcardSize() const void AvdDialog::updateApiLevelComboBox() { - QList<SdkPlatform> filteredList; - QList<SdkPlatform> platforms = m_config->sdkTargets(m_minApiLevel); - foreach (const SdkPlatform &platform, platforms) { - if (platform.abis.contains(abi())) - filteredList << platform; - } + SdkPlatformList filteredList; + SdkPlatformList platforms = m_config->sdkTargets(m_minApiLevel); + + QString selectedAbi = abi(); + auto hasAbi = [selectedAbi](const SystemImage &image) { + return image.isValid() && (image.abiName == selectedAbi); + }; + + filteredList = Utils::filtered(platforms, [hasAbi](const SdkPlatform &platform) { + return Utils::anyOf(platform.systemImages,hasAbi); + }); m_avdDialog.targetComboBox->clear(); - m_avdDialog.targetComboBox->addItems(AndroidConfig::apiLevelNamesFor(filteredList)); + foreach (const SdkPlatform &platform, filteredList) { + m_avdDialog.targetComboBox->addItem(AndroidConfig::apiLevelNameFor(platform), + QVariant::fromValue<SdkPlatform>(platform)); + } if (platforms.isEmpty()) { m_avdDialog.warningIcon->setVisible(true); diff --git a/src/plugins/android/avddialog.h b/src/plugins/android/avddialog.h index 950285aadc..d22e6af64f 100644 --- a/src/plugins/android/avddialog.h +++ b/src/plugins/android/avddialog.h @@ -32,6 +32,7 @@ namespace Android { class AndroidConfig; +class SdkPlatform; namespace Internal { @@ -42,7 +43,7 @@ public: explicit AvdDialog(int minApiLevel, const QString &targetArch, const AndroidConfig *config, QWidget *parent = 0); - QString target() const; + Android::SdkPlatform target() const; QString name() const; QString abi() const; int sdcardSize() const; |