From f46099d21e518c15979dfeb4c3a0d1fee1880095 Mon Sep 17 00:00:00 2001 From: Assam Boudjelthia Date: Mon, 23 Dec 2019 16:13:23 +0200 Subject: [PATCH] Android: Automatically download SDK tools and essential packages Automatically download Android SDK Tools to default path used by Android Studio, then essential packages will be installed using the sdkmanager tool. Automatic installation can also be triggered by an added button in the settings page. Essentials packages include NDK Bundle and other NDK versions required by previous Qt versions. An sdk_definitions.json file holds download paths for SDK Tools, and other (Qt version <-> essential packages) combinations. [ChangeLog][Android] Automatically download SDK Tools, NDKs and all essential packages for Android builds. Task-number: QTCREATORBUG-23285 Change-Id: I90e7aafecd017d2bdc959e403711d9d440a6bbb2 Reviewed-by: Alessandro Portale --- share/qtcreator/CMakeLists.txt | 2 +- share/qtcreator/android/sdk_definitions.json | 25 ++ share/qtcreator/static.pro | 3 +- src/plugins/android/CMakeLists.txt | 1 + src/plugins/android/android.pro | 6 +- src/plugins/android/androidconfigurations.cpp | 141 +++++++++++ src/plugins/android/androidconfigurations.h | 31 +++ src/plugins/android/androidplugin.cpp | 30 +++ src/plugins/android/androidplugin.h | 1 + src/plugins/android/androidsdkdownloader.cpp | 231 ++++++++++++++++++ src/plugins/android/androidsdkdownloader.h | 78 ++++++ src/plugins/android/androidsdkmanager.cpp | 34 ++- src/plugins/android/androidsdkmanager.h | 1 + src/plugins/android/androidsdkmodel.cpp | 49 ++-- src/plugins/android/androidsdkpackage.cpp | 25 ++ src/plugins/android/androidsdkpackage.h | 23 +- src/plugins/android/androidsettingswidget.cpp | 183 +++++++++++--- src/plugins/android/androidsettingswidget.ui | 77 +++--- 18 files changed, 844 insertions(+), 97 deletions(-) create mode 100644 share/qtcreator/android/sdk_definitions.json create mode 100644 src/plugins/android/androidsdkdownloader.cpp create mode 100644 src/plugins/android/androidsdkdownloader.h diff --git a/share/qtcreator/CMakeLists.txt b/share/qtcreator/CMakeLists.txt index 63393293c1a..05f64cc2f15 100644 --- a/share/qtcreator/CMakeLists.txt +++ b/share/qtcreator/CMakeLists.txt @@ -1,4 +1,4 @@ -set(template_directories cplusplus debugger glsl modeleditor qml qmldesigner +set(template_directories android cplusplus debugger glsl modeleditor qml qmldesigner qmlicons qml-type-descriptions schemes scripts snippets styles templates themes welcomescreen) add_custom_target(copy_share_to_builddir ALL diff --git a/share/qtcreator/android/sdk_definitions.json b/share/qtcreator/android/sdk_definitions.json new file mode 100644 index 00000000000..106bb3e353a --- /dev/null +++ b/share/qtcreator/android/sdk_definitions.json @@ -0,0 +1,25 @@ +{ + "common": { + "sdk_tools_url": { + "linux": "https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip", + "linux_sha256": "92ffee5a1d98d856634e8b71132e8a95d96c83a63fde1099be3d86df3106def9", + "windows": "https://dl.google.com/android/repository/sdk-tools-windows-4333796.zip", + "windows_sha256": "7e81d69c303e47a4f0e748a6352d85cd0c8fd90a5a95ae4e076b5e5f960d3c7a", + "mac": "https://dl.google.com/android/repository/sdk-tools-darwin-4333796.zip", + "mac_sha256": "ecb29358bc0f13d7c2fa0f9290135a5b608e38434aad9bf7067d0252c160853e" + }, + "sdk_essential_packages": ["platform-tools", "platforms;android-29"] + }, + "specific_qt_versions": [ + { + "versions": ["default"], + "sdk_essential_packages": ["build-tools;29.0.2", "ndk-bundle"], + "ndk_path": "ndk-bundle" + }, + { + "versions": ["5.12.[0-5]", "5.13.[0-1]"], + "sdk_essential_packages": ["build-tools;28.0.2", "ndk;19.2.5345600"], + "ndk_path": "ndk/19.2.5345600" + } + ] +} diff --git a/share/qtcreator/static.pro b/share/qtcreator/static.pro index 7f39ae2faed..d3fdfdd2816 100644 --- a/share/qtcreator/static.pro +++ b/share/qtcreator/static.pro @@ -23,7 +23,8 @@ DATA_DIRS = \ modeleditor \ glsl \ cplusplus \ - indexer_preincludes + indexer_preincludes \ + android macx: DATA_DIRS += scripts for(data_dir, DATA_DIRS) { diff --git a/src/plugins/android/CMakeLists.txt b/src/plugins/android/CMakeLists.txt index 9d97455dccf..0f2eb17d7ba 100644 --- a/src/plugins/android/CMakeLists.txt +++ b/src/plugins/android/CMakeLists.txt @@ -33,6 +33,7 @@ add_qtc_plugin(Android androidruncontrol.cpp androidruncontrol.h androidrunner.cpp androidrunner.h androidrunnerworker.cpp androidrunnerworker.h + androidsdkdownloader.cpp androidsdkdownloader.h androidsdkmanager.cpp androidsdkmanager.h androidsdkmanagerwidget.cpp androidsdkmanagerwidget.h androidsdkmanagerwidget.ui androidsdkmodel.cpp androidsdkmodel.h diff --git a/src/plugins/android/android.pro b/src/plugins/android/android.pro index 101ed63fa6f..f3745d18c07 100644 --- a/src/plugins/android/android.pro +++ b/src/plugins/android/android.pro @@ -48,7 +48,8 @@ HEADERS += \ androidsdkmanagerwidget.h \ androidpackageinstallationstep.h \ androidextralibrarylistmodel.h \ - createandroidmanifestwizard.h + createandroidmanifestwizard.h \ + androidsdkdownloader.h SOURCES += \ androidconfigurations.cpp \ @@ -90,7 +91,8 @@ SOURCES += \ androidsdkmanagerwidget.cpp \ androidpackageinstallationstep.cpp \ androidextralibrarylistmodel.cpp \ - createandroidmanifestwizard.cpp + createandroidmanifestwizard.cpp \ + androidsdkdownloader.cpp FORMS += \ androidsettingswidget.ui \ diff --git a/src/plugins/android/androidconfigurations.cpp b/src/plugins/android/androidconfigurations.cpp index 2826bcb8a24..d8e9b92f5f8 100644 --- a/src/plugins/android/androidconfigurations.cpp +++ b/src/plugins/android/androidconfigurations.cpp @@ -61,6 +61,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -72,6 +75,7 @@ #include #include +using namespace QtSupport; using namespace ProjectExplorer; using namespace Utils; @@ -82,11 +86,21 @@ static Q_LOGGING_CATEGORY(avdConfigLog, "qtc.android.androidconfig", QtWarningMs namespace Android { using namespace Internal; +const char JsonFilePath[] = "/android/sdk_definitions.json"; +const char SdkToolsUrlKey[] = "sdk_tools_url"; +const char CommonKey[] = "common"; +const char SdkEssentialPkgsKey[] = "sdk_essential_packages"; +const char VersionsKey[] = "versions"; +const char NdkPathKey[] = "ndk_path"; +const char SpecificQtVersionsKey[] = "specific_qt_versions"; +const char DefaultVersionKey[] = "default"; + namespace { const char jdkSettingsPath[] = "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit"; const QLatin1String SettingsGroup("AndroidConfigurations"); const QLatin1String SDKLocationKey("SDKLocation"); + const QLatin1String SdkFullyConfiguredKey("AllEssentialsInstalled"); const QLatin1String SDKManagerToolArgsKey("SDKManagerToolArgs"); const QLatin1String NDKLocationKey("NDKLocation"); const QLatin1String OpenJDKLocationKey("OpenJDKLocation"); @@ -228,6 +242,7 @@ void AndroidConfig::load(const QSettings &settings) m_keystoreLocation = FilePath::fromString(settings.value(KeystoreLocationKey).toString()); m_toolchainHost = settings.value(ToolchainHostKey).toString(); m_automaticKitCreation = settings.value(AutomaticKitCreationKey, true).toBool(); + m_sdkFullyConfigured = settings.value(SdkFullyConfiguredKey, false).toBool(); PersistentSettingsReader reader; if (reader.load(FilePath::fromString(sdkSettingsFileName())) @@ -240,8 +255,10 @@ void AndroidConfig::load(const QSettings &settings) m_keystoreLocation = FilePath::fromString(reader.restoreValue(KeystoreLocationKey, m_keystoreLocation.toString()).toString()); m_toolchainHost = reader.restoreValue(ToolchainHostKey, m_toolchainHost).toString(); m_automaticKitCreation = reader.restoreValue(AutomaticKitCreationKey, m_automaticKitCreation).toBool(); + m_sdkFullyConfigured = reader.restoreValue(SdkFullyConfiguredKey, m_sdkFullyConfigured).toBool(); // persistent settings } + parseDependenciesJson(); m_NdkInformationUpToDate = false; } @@ -260,6 +277,7 @@ void AndroidConfig::save(QSettings &settings) const settings.setValue(PartitionSizeKey, m_partitionSize); settings.setValue(AutomaticKitCreationKey, m_automaticKitCreation); settings.setValue(ToolchainHostKey, m_toolchainHost); + settings.setValue(SdkFullyConfiguredKey, m_sdkFullyConfigured); } void AndroidConfig::updateNdkInformation() const @@ -298,6 +316,72 @@ void AndroidConfig::updateNdkInformation() const m_NdkInformationUpToDate = true; } +void AndroidConfig::parseDependenciesJson() +{ + QString sdkConfigFile(Core::ICore::resourcePath() + JsonFilePath); + QFile jsonFile(sdkConfigFile); + if (!jsonFile.open(QIODevice::ReadOnly)) + qCDebug(avdConfigLog, "Couldn't open JSON config file %s.", qPrintable(sdkConfigFile)); + + QJsonDocument loadDoc(QJsonDocument::fromJson(jsonFile.readAll())); + QJsonObject jsonObject = loadDoc.object(); + + if (jsonObject.contains(CommonKey) && jsonObject[CommonKey].isObject()) { + QJsonObject commonObject = jsonObject[CommonKey].toObject(); + // Parse SDK Tools URL + if (commonObject.contains(SdkToolsUrlKey) && commonObject[SdkToolsUrlKey].isObject()) { + QJsonObject sdkToolsObj(commonObject[SdkToolsUrlKey].toObject()); + if (Utils::HostOsInfo::isMacHost()) { + m_sdkToolsUrl = sdkToolsObj["mac"].toString(); + m_sdkToolsSha256 = QByteArray::fromHex(sdkToolsObj["mac_sha256"].toString().toUtf8()); + } else if (Utils::HostOsInfo::isWindowsHost()) { + m_sdkToolsUrl = sdkToolsObj["windows"].toString(); + m_sdkToolsSha256 = QByteArray::fromHex(sdkToolsObj["windows_sha256"].toString().toUtf8()); + } else { + m_sdkToolsUrl = sdkToolsObj["linux"].toString(); + m_sdkToolsSha256 = QByteArray::fromHex(sdkToolsObj["linux_sha256"].toString().toUtf8()); + } + } + + // Parse common essential packages + QJsonArray commonEssentials = commonObject[SdkEssentialPkgsKey].toArray(); + for (const QJsonValueRef &pkg : commonEssentials) + m_commonEssentialPkgs.append(pkg.toString()); + } + + auto fillQtVersionsRange = [](const QString &shortVersion) { + QList versions; + QRegularExpression re("([0-9]\\.[0-9]*\\.)\\[([0-9])\\-([0-9])\\]"); + QRegularExpressionMatch match = re.match(shortVersion); + if (match.hasMatch() && match.lastCapturedIndex() == 3) + for (int i = match.captured(2).toInt(); i <= match.captured(3).toInt(); ++i) + versions.append(QtVersionNumber(match.captured(1) + QString::number(i))); + else + versions.append(QtVersionNumber(shortVersion)); + + return versions; + }; + + if (jsonObject.contains(SpecificQtVersionsKey) && jsonObject[SpecificQtVersionsKey].isArray()) { + QJsonArray versionsArray = jsonObject[SpecificQtVersionsKey].toArray(); + for (const QJsonValueRef &item : versionsArray) { + QJsonObject itemObj = item.toObject(); + SdkForQtVersions specificVersion; + + specificVersion.ndkPath = itemObj[NdkPathKey].toString(); + for (const QJsonValueRef &pkg : itemObj[SdkEssentialPkgsKey].toArray()) + specificVersion.essentialPackages.append(pkg.toString()); + for (const QJsonValueRef &pkg : itemObj[VersionsKey].toArray()) + specificVersion.versions.append(fillQtVersionsRange(pkg.toString())); + + if (itemObj[VersionsKey].toArray().first().toString() == DefaultVersionKey) + m_defaultSdkDepends = specificVersion; + else + m_specificQtVersions.append(specificVersion); + } + } +} + QStringList AndroidConfig::apiLevelNamesFor(const SdkPlatformList &platforms) { return Utils::transform(platforms, AndroidConfig::apiLevelNameFor); @@ -709,6 +793,7 @@ QVersionNumber AndroidConfig::sdkToolsVersion() const QVersionNumber AndroidConfig::buildToolsVersion() const { + //TODO: return version according to qt version QVersionNumber maxVersion; QDir buildToolsDir(m_sdkLocation.pathAppended("build-tools").toString()); for (const QFileInfo &file: buildToolsDir.entryList(QDir::Dirs|QDir::NoDotAndDotDot)) @@ -732,6 +817,11 @@ FilePath AndroidConfig::ndkLocation() const return m_ndkLocation; } +FilePath AndroidConfig::defaultNdkLocation() const +{ + return sdkLocation().pathAppended(m_defaultSdkDepends.ndkPath); +} + static inline QString gdbServerArch(const QString &androidAbi) { if (androidAbi == "arm64-v8a") { @@ -808,6 +898,57 @@ void AndroidConfig::setNdkLocation(const FilePath &ndkLocation) m_NdkInformationUpToDate = false; } +QStringList AndroidConfig::allEssentials() const +{ + QList installedVersions = QtVersionManager::versions( + [](const BaseQtVersion *v) { + return v->targetDeviceTypes().contains(Android::Constants::ANDROID_DEVICE_TYPE); + }); + + QStringList allPackages(defaultEssentials()); + for (const BaseQtVersion *version : installedVersions) + allPackages.append(essentialsFromQtVersion(*version)); + allPackages.removeDuplicates(); + + return allPackages; +} + +QStringList AndroidConfig::essentialsFromQtVersion(const BaseQtVersion &version) const +{ + QtVersionNumber qtVersion = version.qtVersion(); + for (const SdkForQtVersions &item : m_specificQtVersions) + if (item.containsVersion(qtVersion)) + return item.essentialPackages; + + return m_defaultSdkDepends.essentialPackages; +} + +QString AndroidConfig::ndkPathFromQtVersion(const BaseQtVersion &version) const +{ + QtVersionNumber qtVersion = version.qtVersion(); + for (const SdkForQtVersions &item : m_specificQtVersions) + if (item.containsVersion(qtVersion)) + return item.ndkPath; + + return m_defaultSdkDepends.ndkPath; +} + +QStringList AndroidConfig::defaultEssentials() const +{ + return m_defaultSdkDepends.essentialPackages + m_commonEssentialPkgs; +} + +void AndroidConfig::updateDependenciesConfig() +{ + parseDependenciesJson(); +} + +bool SdkForQtVersions::containsVersion(const QtVersionNumber &qtVersion) const +{ + return versions.contains(qtVersion) + || versions.contains(QtVersionNumber(qtVersion.majorVersion, qtVersion.minorVersion)); +} + FilePath AndroidConfig::openJDKLocation() const { return m_openJDKLocation; diff --git a/src/plugins/android/androidconfigurations.h b/src/plugins/android/androidconfigurations.h index 1756056b25d..93250713aa7 100644 --- a/src/plugins/android/androidconfigurations.h +++ b/src/plugins/android/androidconfigurations.h @@ -28,6 +28,7 @@ #include "android_global.h" #include "androidsdkpackage.h" #include +#include #include #include @@ -95,6 +96,16 @@ public: bool overwrite = false; }; +struct SdkForQtVersions +{ + QList versions; + QStringList essentialPackages; + QString ndkPath; + +public: + bool containsVersion(const QtSupport::QtVersionNumber &qtVersion) const; +}; + class ANDROID_EXPORT AndroidConfig { public: @@ -112,10 +123,20 @@ public: void setSdkManagerToolArgs(const QStringList &args); Utils::FilePath ndkLocation() const; + Utils::FilePath defaultNdkLocation() const; Utils::FilePath gdbServer(const QString &androidAbi) const; QVersionNumber ndkVersion() const; void setNdkLocation(const Utils::FilePath &ndkLocation); + QUrl sdkToolsUrl() const { return m_sdkToolsUrl; }; + QByteArray getSdkToolsSha256() const { return m_sdkToolsSha256; }; + QString ndkPathFromQtVersion(const QtSupport::BaseQtVersion &version) const; + + QStringList defaultEssentials() const; + QStringList essentialsFromQtVersion(const QtSupport::BaseQtVersion &version) const; + QStringList allEssentials() const; + void updateDependenciesConfig(); + Utils::FilePath openJDKLocation() const; void setOpenJDKLocation(const Utils::FilePath &openJDKLocation); @@ -162,6 +183,9 @@ public: bool useNativeUiTools() const; + bool sdkFullyConfigured() const { return m_sdkFullyConfigured; }; + void setSdkFullyConfigured(bool allEssentialsInstalled) { m_sdkFullyConfigured = allEssentialsInstalled; }; + private: static QString getDeviceProperty(const Utils::FilePath &adbToolPath, const QString &device, const QString &property); @@ -176,6 +200,7 @@ private: static QString getAvdName(const QString &serialnumber); void updateNdkInformation() const; + void parseDependenciesJson(); Utils::FilePath m_sdkLocation; QStringList m_sdkManagerToolArgs; @@ -184,6 +209,12 @@ private: Utils::FilePath m_keystoreLocation; unsigned m_partitionSize = 1024; bool m_automaticKitCreation = true; + QUrl m_sdkToolsUrl; + QByteArray m_sdkToolsSha256; + QStringList m_commonEssentialPkgs; + SdkForQtVersions m_defaultSdkDepends; + QList m_specificQtVersions; + bool m_sdkFullyConfigured = false; //caches mutable bool m_NdkInformationUpToDate = false; diff --git a/src/plugins/android/androidplugin.cpp b/src/plugins/android/androidplugin.cpp index 30e4c9d55af..93850bb4831 100644 --- a/src/plugins/android/androidplugin.cpp +++ b/src/plugins/android/androidplugin.cpp @@ -46,6 +46,10 @@ # include "androidqbspropertyprovider.h" #endif +#include +#include +#include + #include #include #include @@ -60,6 +64,8 @@ using namespace ProjectExplorer; using namespace ProjectExplorer::Constants; +const char kSetupAndroidSetting[] = "ConfigureAndroid"; + namespace Android { namespace Internal { @@ -153,6 +159,10 @@ bool AndroidPlugin::initialize(const QStringList &arguments, QString *errorMessa d = new AndroidPluginPrivate; + if (!AndroidConfigurations::currentConfig().sdkFullyConfigured()) { + connect(Core::ICore::instance(), &Core::ICore::coreOpened, this, + &AndroidPlugin::askUserAboutAndroidSetup, Qt::QueuedConnection); + } connect(KitManager::instance(), &KitManager::kitsLoaded, this, &AndroidPlugin::kitsRestored); @@ -168,5 +178,25 @@ void AndroidPlugin::kitsRestored() this, &AndroidPlugin::kitsRestored); } +void AndroidPlugin::askUserAboutAndroidSetup() +{ + if (!Utils::CheckableMessageBox::shouldAskAgain(Core::ICore::settings(), kSetupAndroidSetting) + || !Core::ICore::infoBar()->canInfoBeAdded(kSetupAndroidSetting)) + return; + + Core::InfoBarEntry info( + kSetupAndroidSetting, + tr("Would you like to configure Android options? This will ensure " + "Android kits can be usable and all essential packages are installed. " + "To do it later, select Options > Devices > Android."), + Core::InfoBarEntry::GlobalSuppression::Enabled); + info.setCustomButtonInfo(tr("Configure Android"), [this] { + Core::ICore::infoBar()->removeInfo(kSetupAndroidSetting); + Core::ICore::infoBar()->globallySuppressInfo(kSetupAndroidSetting); + QTimer::singleShot(0, this, [this]() { d->potentialKit.executeFromMenu(); }); + }); + Core::ICore::infoBar()->addInfo(info); +} + } // namespace Internal } // namespace Android diff --git a/src/plugins/android/androidplugin.h b/src/plugins/android/androidplugin.h index ea2d7461c4a..45db6ed2372 100644 --- a/src/plugins/android/androidplugin.h +++ b/src/plugins/android/androidplugin.h @@ -40,6 +40,7 @@ class AndroidPlugin final : public ExtensionSystem::IPlugin bool initialize(const QStringList &arguments, QString *errorMessage) final; void kitsRestored(); + void askUserAboutAndroidSetup(); class AndroidPluginPrivate *d = nullptr; }; diff --git a/src/plugins/android/androidsdkdownloader.cpp b/src/plugins/android/androidsdkdownloader.cpp new file mode 100644 index 00000000000..da3fb672b26 --- /dev/null +++ b/src/plugins/android/androidsdkdownloader.cpp @@ -0,0 +1,231 @@ +/**************************************************************************** +** +** Copyright (C) 2020 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 "androidsdkdownloader.h" + +#include +#include +#include +#include +#include +#include + +namespace { +Q_LOGGING_CATEGORY(sdkDownloaderLog, "qtc.android.sdkDownloader", QtWarningMsg) +} + +namespace Android { +namespace Internal { +/** + * @class SdkDownloader + * @brief Download Android SDK tools package from within Qt Creator. + */ +AndroidSdkDownloader::AndroidSdkDownloader(const QUrl &sdkUrl, const QByteArray &sha256) : + m_sdkUrl(sdkUrl), m_sha256(sha256) +{ + connect(&m_manager, &QNetworkAccessManager::finished, this, &AndroidSdkDownloader::downloadFinished); +} + +#if QT_CONFIG(ssl) +void AndroidSdkDownloader::sslErrors(const QList &sslErrors) +{ + for (const QSslError &error : sslErrors) + qCDebug(sdkDownloaderLog, "SSL error: %s\n", qPrintable(error.errorString())); + cancelWithError(tr("Encountered SSL errors, download is aborted.")); +} +#endif + +static void setSdkFilesExecPermission( const QString &sdkExtractPath) +{ + QDirIterator it(sdkExtractPath + "/tools", QStringList() << "*", + QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + QFile file(it.next()); + if (!file.fileName().contains('.')) { + QFlags currentPermissions + = file.permissions(); + file.setPermissions(currentPermissions | QFileDevice::ExeOwner); + } + } +} + +void AndroidSdkDownloader::downloadAndExtractSdk(const QString &jdkPath, const QString &sdkExtractPath) +{ + if (m_sdkUrl.isEmpty()) { + logError(tr("The SDK Tools download URL is empty.")); + return; + } + + QNetworkRequest request(m_sdkUrl); + m_reply = m_manager.get(request); + +#if QT_CONFIG(ssl) + connect(m_reply, &QNetworkReply::sslErrors, this, &AndroidSdkDownloader::sslErrors); +#endif + + m_progressDialog = new QProgressDialog(tr("Downloading SDK Tools package..."), tr("Cancel"), 0, 100); + m_progressDialog->setWindowModality(Qt::WindowModal); + m_progressDialog->setWindowTitle(dialogTitle()); + m_progressDialog->setFixedSize(m_progressDialog->sizeHint()); + + connect(m_reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64 max) { + m_progressDialog->setRange(0, max); + m_progressDialog->setValue(received); + }); + + connect(m_progressDialog, &QProgressDialog::canceled, this, &AndroidSdkDownloader::cancel); + + connect(this, &AndroidSdkDownloader::sdkPackageWriteFinished, this, [this, jdkPath, sdkExtractPath]() { + if (extractSdk(jdkPath, sdkExtractPath)) { + setSdkFilesExecPermission(sdkExtractPath); + emit sdkExtracted(); + } + }); +} + +bool AndroidSdkDownloader::extractSdk(const QString &jdkPath, const QString &sdkExtractPath) +{ + if (!QDir(sdkExtractPath).exists()) { + if (!QDir().mkdir(sdkExtractPath)) { + logError(QString(tr("Could not create the SDK folder %1.")).arg(sdkExtractPath)); + return false; + } + } + + QProcess *jarExtractProc = new QProcess(); + jarExtractProc->setWorkingDirectory(sdkExtractPath); + QString jarCmdPath(jdkPath + "/bin/jar"); + jarExtractProc->start(jarCmdPath, {"xf", m_sdkFilename}); + jarExtractProc->waitForFinished(); + jarExtractProc->close(); + + return jarExtractProc->exitCode() ? false : true; +} + +bool AndroidSdkDownloader::verifyFileIntegrity() +{ + QFile f(m_sdkFilename); + if (f.open(QFile::ReadOnly)) { + QCryptographicHash hash(QCryptographicHash::Sha256); + if (hash.addData(&f)) { + return hash.result() == m_sha256; + } + } + return false; +} + +QString AndroidSdkDownloader::dialogTitle() +{ + return tr("Download SDK Tools"); +} + +void AndroidSdkDownloader::cancel() +{ + if (m_reply) { + m_reply->abort(); + m_reply->deleteLater(); + } + if (m_progressDialog) + m_progressDialog->hide(); +} + +void AndroidSdkDownloader::cancelWithError(const QString &error) +{ + cancel(); + logError(error); +} + +void AndroidSdkDownloader::logError(const QString &error) +{ + qCDebug(sdkDownloaderLog, "%s", error.toUtf8().data()); + emit sdkDownloaderError(error); +} + +QString AndroidSdkDownloader::getSaveFilename(const QUrl &url) +{ + QString path = url.path(); + QString basename = QFileInfo(path).fileName(); + + if (basename.isEmpty()) + basename = "sdk-tools.zip"; + + if (QFile::exists(basename)) { + int i = 0; + basename += '.'; + while (QFile::exists(basename + QString::number(i))) + ++i; + basename += QString::number(i); + } + + QString fullPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + + QDir::separator() + basename; + return fullPath; +} + +bool AndroidSdkDownloader::saveToDisk(const QString &filename, QIODevice *data) +{ + QFile file(filename); + if (!file.open(QIODevice::WriteOnly)) { + logError(QString(tr("Could not open %1 for writing: %2.")).arg(filename, file.errorString())); + return false; + } + + file.write(data->readAll()); + file.close(); + + return true; +} + +bool AndroidSdkDownloader::isHttpRedirect(QNetworkReply *reply) +{ + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + return statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 + || statusCode == 307 || statusCode == 308; +} + +void AndroidSdkDownloader::downloadFinished(QNetworkReply *reply) +{ + QUrl url = reply->url(); + if (reply->error()) { + cancelWithError(QString(tr("Downloading Android SDK Tools from URL %1 has failed: %2.")) + .arg(url.toString(), reply->errorString())); + } else { + if (isHttpRedirect(reply)) { + cancelWithError(QString(tr("Download from %1 was redirected.")).arg(url.toString())); + } else { + m_sdkFilename = getSaveFilename(url); + if (saveToDisk(m_sdkFilename, reply) && verifyFileIntegrity()) + emit sdkPackageWriteFinished(); + else + cancelWithError( + tr("Writing and verifying the integrity of the downloaded file has failed.")); + } + } + + reply->deleteLater(); +} + +} // Internal +} // Android diff --git a/src/plugins/android/androidsdkdownloader.h b/src/plugins/android/androidsdkdownloader.h new file mode 100644 index 00000000000..0801f763add --- /dev/null +++ b/src/plugins/android/androidsdkdownloader.h @@ -0,0 +1,78 @@ +/**************************************************************************** +** +** Copyright (C) 2020 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. +** +****************************************************************************/ + +#ifndef ANDROIDSDKDOWNLOADER_H +#define ANDROIDSDKDOWNLOADER_H + +#include +#include +#include + +namespace Android { +namespace Internal { + +class AndroidSdkDownloader : public QObject +{ + Q_OBJECT + +public: + AndroidSdkDownloader(const QUrl &sdkUrl, const QByteArray &sha256); + void downloadAndExtractSdk(const QString &jdkPath, const QString &sdkExtractPath); + static QString dialogTitle(); + + void cancel(); + +signals: + void sdkPackageWriteFinished(); + void sdkExtracted(); + void sdkDownloaderError(const QString &error); + +private: + static QString getSaveFilename(const QUrl &url); + bool saveToDisk(const QString &filename, QIODevice *data); + static bool isHttpRedirect(QNetworkReply *m_reply); + + bool extractSdk(const QString &jdkPath, const QString &sdkExtractPath); + bool verifyFileIntegrity(); + void cancelWithError(const QString &error); + void logError(const QString &error); + + void downloadFinished(QNetworkReply *m_reply); +#if QT_CONFIG(ssl) + void sslErrors(const QList &errors); +#endif + + QNetworkAccessManager m_manager; + QNetworkReply *m_reply; + QString m_sdkFilename; + QProgressDialog *m_progressDialog; + QUrl m_sdkUrl; + QByteArray m_sha256; +}; + +} // Internal +} // Android + +#endif // ANDROIDSDKDOWNLOADER_H diff --git a/src/plugins/android/androidsdkmanager.cpp b/src/plugins/android/androidsdkmanager.cpp index 494fa8cf256..cc5959a369f 100644 --- a/src/plugins/android/androidsdkmanager.cpp +++ b/src/plugins/android/androidsdkmanager.cpp @@ -271,7 +271,8 @@ public: SdkToolsMarker = 0x100, PlatformToolsMarker = 0x200, EmulatorToolsMarker = 0x400, - ExtrasMarker = 0x800, + NdkMarker = 0x800, + ExtrasMarker = 0x1000, SectionMarkers = InstalledPackagesMarker | AvailablePackagesMarkers | AvailableUpdatesMarker }; @@ -292,6 +293,7 @@ private: SdkTools *parseSdkToolsPackage(const QStringList &data) const; PlatformTools *parsePlatformToolsPackage(const QStringList &data) const; EmulatorTools *parseEmulatorToolsPackage(const QStringList &data) const; + Ndk *parseNdkPackage(const QStringList &data) const; ExtraTools *parseExtraToolsPackage(const QStringList &data) const; MarkerTag parseMarkers(const QString &line); @@ -309,6 +311,7 @@ const std::map markerTags { {SdkManagerOutputParser::MarkerTag::SdkToolsMarker, "tools"}, {SdkManagerOutputParser::MarkerTag::PlatformToolsMarker, "platform-tools"}, {SdkManagerOutputParser::MarkerTag::EmulatorToolsMarker, "emulator"}, + {SdkManagerOutputParser::MarkerTag::NdkMarker, "ndk"}, {SdkManagerOutputParser::MarkerTag::ExtrasMarker, "extras"} }; @@ -360,6 +363,13 @@ SystemImageList AndroidSdkManager::installedSystemImages() return result; } +NdkList AndroidSdkManager::installedNdkPackages() +{ + AndroidSdkPackageList list = m_d->filteredPackages(AndroidSdkPackage::Installed, + AndroidSdkPackage::NDKPackage); + return Utils::static_container_cast(list); +} + SdkPlatform *AndroidSdkManager::latestAndroidSdkPlatform(AndroidSdkPackage::PackageState state) { SdkPlatform *result = nullptr; @@ -601,6 +611,10 @@ void SdkManagerOutputParser::parsePackageData(MarkerTag packageMarker, const QSt } break; + case MarkerTag::NdkMarker: + createPackage(&SdkManagerOutputParser::parseNdkPackage); + break; + case MarkerTag::ExtrasMarker: createPackage(&SdkManagerOutputParser::parseExtraToolsPackage); break; @@ -771,6 +785,24 @@ EmulatorTools *SdkManagerOutputParser::parseEmulatorToolsPackage(const QStringLi return emulatorTools; } +Ndk *SdkManagerOutputParser::parseNdkPackage(const QStringList &data) const +{ + Ndk *ndk = nullptr; + GenericPackageData packageData; + if (parseAbstractData(packageData, data, 1, "NDK")) { + ndk = new Ndk(packageData.revision, data.at(0)); + ndk->setDescriptionText(packageData.description); + ndk->setDisplayText(packageData.description); + ndk->setInstalledLocation(packageData.installedLocation); + if (packageData.description == "NDK") + ndk->setAsNdkBundle(true); + } else { + qCDebug(sdkManagerLog) << "NDK: Parsing failed. Minimum required data unavailable:" + << data; + } + return ndk; +} + ExtraTools *SdkManagerOutputParser::parseExtraToolsPackage(const QStringList &data) const { ExtraTools *extraTools = nullptr; diff --git a/src/plugins/android/androidsdkmanager.h b/src/plugins/android/androidsdkmanager.h index fa0d52e1acc..ce5cf4e299e 100644 --- a/src/plugins/android/androidsdkmanager.h +++ b/src/plugins/android/androidsdkmanager.h @@ -70,6 +70,7 @@ public: AndroidSdkPackageList availableSdkPackages(); AndroidSdkPackageList installedSdkPackages(); SystemImageList installedSystemImages(); + NdkList installedNdkPackages(); SdkPlatform *latestAndroidSdkPlatform(AndroidSdkPackage::PackageState state = AndroidSdkPackage::Installed); diff --git a/src/plugins/android/androidsdkmodel.cpp b/src/plugins/android/androidsdkmodel.cpp index 5506ad23787..7e18b19ff4a 100644 --- a/src/plugins/android/androidsdkmodel.cpp +++ b/src/plugins/android/androidsdkmodel.cpp @@ -273,37 +273,44 @@ bool AndroidSdkModel::setData(const QModelIndex &index, const QVariant &value, i void AndroidSdkModel::selectMissingEssentials() { resetSelection(); - bool selectPlatformTool = !m_config.adbToolPath().exists(); - bool selectBuildTools = m_config.buildToolsVersion().isNull(); + QStringList pendingPkgs(m_config.allEssentials()); auto addTool = [this](QList::const_iterator itr) { - m_changeState << *itr; - auto i = index(std::distance(m_tools.cbegin(), itr), 0, index(0, 0)); - emit dataChanged(i, i, {Qt::CheckStateRole}); + if ((*itr)->installedLocation().isEmpty()) { + m_changeState << *itr; + auto i = index(std::distance(m_tools.cbegin(), itr), 0, index(0, 0)); + emit dataChanged(i, i, {Qt::CheckStateRole}); + } }; for (auto tool = m_tools.cbegin(); tool != m_tools.cend(); ++tool) { - if (selectPlatformTool && (*tool)->type() == AndroidSdkPackage::PlatformToolsPackage) { - // Select Platform tools - addTool(tool); - selectPlatformTool = false; - } - if (selectBuildTools && (*tool)->type() == AndroidSdkPackage::BuildToolsPackage) { - // Select build tools - addTool(tool); - selectBuildTools = false; - } - if (!selectPlatformTool && !selectBuildTools) + if (!pendingPkgs.contains((*tool)->sdkStylePath())) + continue; + + if ((*tool)->type() == AndroidSdkPackage::PlatformToolsPackage) + addTool(tool); // Select Platform tools + else if ((*tool)->type() == AndroidSdkPackage::BuildToolsPackage) + addTool(tool); // Select build tools + else if ((*tool)->type() == AndroidSdkPackage::NDKPackage) + addTool(tool); // Select NDK Bundle + + pendingPkgs.removeOne((*tool)->sdkStylePath()); + if (pendingPkgs.isEmpty()) break; } // Select SDK platform - if (m_sdkManager->installedSdkPlatforms().isEmpty() && !m_sdkPlatforms.isEmpty()) { - auto i = index(0, 0, index(1,0)); - m_changeState << m_sdkPlatforms.at(0); - emit dataChanged(i , i, {Qt::CheckStateRole}); + for (const SdkPlatform *platform : m_sdkPlatforms) { + if (pendingPkgs.contains(platform->sdkStylePath()) && + platform->installedLocation().isEmpty()) { + auto i = index(0, 0, index(1, 0)); + m_changeState << platform; + emit dataChanged(i, i, {Qt::CheckStateRole}); + pendingPkgs.removeOne(platform->sdkStylePath()); + } + if (pendingPkgs.isEmpty()) + break; } } - QList AndroidSdkModel::userSelection() const { return Utils::toList(m_changeState); diff --git a/src/plugins/android/androidsdkpackage.cpp b/src/plugins/android/androidsdkpackage.cpp index 8c6e0d79114..a3da9625bc6 100644 --- a/src/plugins/android/androidsdkpackage.cpp +++ b/src/plugins/android/androidsdkpackage.cpp @@ -294,4 +294,29 @@ AndroidSdkPackage::PackageType ExtraTools::type() const return AndroidSdkPackage::ExtraToolsPackage; } +Ndk::Ndk(QVersionNumber revision, QString sdkStylePathStr, QObject *parent) : + AndroidSdkPackage(revision, sdkStylePathStr, parent) +{ +} + +bool Ndk::isValid() const +{ + return installedLocation().exists(); +} + +AndroidSdkPackage::PackageType Ndk::type() const +{ + return AndroidSdkPackage::NDKPackage; +} + +bool Ndk::isNdkBundle() const +{ + return m_isBundle; +} + +void Ndk::setAsNdkBundle(const bool isBundle) +{ + m_isBundle = isBundle; +} + } // namespace Android diff --git a/src/plugins/android/androidsdkpackage.h b/src/plugins/android/androidsdkpackage.h index 035e146f97b..eed0fd43959 100644 --- a/src/plugins/android/androidsdkpackage.h +++ b/src/plugins/android/androidsdkpackage.h @@ -53,9 +53,11 @@ public: SdkPlatformPackage = 1 << 4, SystemImagePackage = 1 << 5, EmulatorToolsPackage = 1 << 6, - ExtraToolsPackage = 1 << 7, + NDKPackage = 1 << 7, + ExtraToolsPackage = 1 << 8, AnyValidType = SdkToolsPackage | BuildToolsPackage | PlatformToolsPackage | - SdkPlatformPackage | SystemImagePackage | EmulatorToolsPackage | ExtraToolsPackage + SdkPlatformPackage | SystemImagePackage | EmulatorToolsPackage | NDKPackage | + ExtraToolsPackage }; enum PackageState { @@ -196,6 +198,23 @@ public: PackageType type() const override; }; +class Ndk : public AndroidSdkPackage +{ +public: + Ndk(QVersionNumber revision, QString sdkStylePathStr, QObject *parent = nullptr); + + // AndroidSdkPackage Overrides + bool isValid() const override; + PackageType type() const override; + + bool isNdkBundle() const; + void setAsNdkBundle(const bool isBundle); + +private: + bool m_isBundle = false; +}; +using NdkList = QList; + class ExtraTools : public AndroidSdkPackage { public: diff --git a/src/plugins/android/androidsettingswidget.cpp b/src/plugins/android/androidsettingswidget.cpp index 1991f63681d..d34f3c4ab9c 100644 --- a/src/plugins/android/androidsettingswidget.cpp +++ b/src/plugins/android/androidsettingswidget.cpp @@ -33,6 +33,7 @@ #include "androidavdmanager.h" #include "androidsdkmanager.h" #include "avddialog.h" +#include "androidsdkdownloader.h" #include "androidsdkmanagerwidget.h" #include @@ -51,6 +52,7 @@ #include #include +#include #include #include #include @@ -104,6 +106,7 @@ private: void validateJdk(); Utils::FilePath findJdkInCommonPaths(); void validateNdk(); + void updateNdkList(); void onSdkPathChanged(); void validateSdk(); void openSDKDownloadUrl(); @@ -125,6 +128,11 @@ private: void enableAvdControls(); void disableAvdControls(); + void downloadSdk(); + bool allEssentialsInstalled(); + bool sdkToolsOk() const; + Utils::FilePath getDefaultSdkPath(); + Ui_AndroidSettingsWidget *m_ui; AndroidSdkManagerWidget *m_sdkManagerWidget = nullptr; AndroidConfig m_androidConfig; @@ -150,6 +158,7 @@ enum AndroidValidation { BuildToolsInstalledRow, SdkManagerSuccessfulRow, PlatformSdkInstalledRow, + AllEssentialsInstalledRow, NdkPathExistsRow, NdkDirStructureRow, NdkinstallDirOkRow @@ -302,6 +311,37 @@ int AvdModel::columnCount(const QModelIndex &/*parent*/) const return 6; } +Utils::FilePath AndroidSettingsWidget::getDefaultSdkPath() +{ + QString sdkFromEnvVar = QString::fromLocal8Bit(getenv("ANDROID_SDK_ROOT")); + if (!sdkFromEnvVar.isEmpty()) + return Utils::FilePath::fromString(sdkFromEnvVar); + + // Set default path of SDK as used by Android Studio + if (Utils::HostOsInfo::isMacHost()) { + return Utils::FilePath::fromString( + QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + + "/../Android/sdk"); + } + + if (Utils::HostOsInfo::isWindowsHost()) { + return Utils::FilePath::fromString( + QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/Android/sdk"); + } + + return Utils::FilePath::fromString( + QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/Android/Sdk"); +} + +void AndroidSettingsWidget::updateNdkList() +{ + m_ui->ndkListComboBox->clear(); + QString currentNdk = m_androidConfig.ndkLocation().toString(); + for (const Ndk *ndk : m_sdkManager->installedNdkPackages()) + m_ui->ndkListComboBox->addItem(ndk->installedLocation().toString()); + m_ui->ndkListComboBox->setCurrentText(currentNdk); +} + AndroidSettingsWidget::AndroidSettingsWidget() : m_ui(new Ui_AndroidSettingsWidget), m_androidConfig(AndroidConfigurations::currentConfig()), @@ -339,28 +379,36 @@ AndroidSettingsWidget::AndroidSettingsWidget() androidValidationPoints[PlatformToolsInstalledRow] = tr("Platform tools installed."); androidValidationPoints[SdkManagerSuccessfulRow] = tr( "SDK manager runs (requires exactly Java 1.8)."); + androidValidationPoints[AllEssentialsInstalledRow] = tr( + "All essential packages installed for all installed Qt versions."); androidValidationPoints[BuildToolsInstalledRow] = tr("Build tools installed."); androidValidationPoints[PlatformSdkInstalledRow] = tr("Platform SDK installed."); - androidValidationPoints[NdkPathExistsRow] = tr("Android NDK path exists."); - androidValidationPoints[NdkDirStructureRow] = tr("Android NDK directory structure is correct."); - androidValidationPoints[NdkinstallDirOkRow] = tr("Android NDK installed into a path without " + androidValidationPoints[NdkPathExistsRow] = tr("Default Android NDK path exists."); + androidValidationPoints[NdkDirStructureRow] = tr("Default Android NDK directory structure is correct."); + androidValidationPoints[NdkinstallDirOkRow] = tr("Default Android NDK installed into a path without " "spaces."); auto androidSummary = new SummaryWidget(androidValidationPoints, tr("Android settings are OK."), tr("Android settings have errors."), m_ui->androidDetailsWidget); m_ui->androidDetailsWidget->setWidget(androidSummary); - m_ui->SDKLocationPathChooser->setFileName(m_androidConfig.sdkLocation()); - m_ui->SDKLocationPathChooser->setPromptDialogTitle(tr("Select Android SDK folder")); - m_ui->NDKLocationPathChooser->setFileName(m_androidConfig.ndkLocation()); - m_ui->NDKLocationPathChooser->setPromptDialogTitle(tr("Select Android NDK folder")); - + connect(m_ui->OpenJDKLocationPathChooser, &Utils::PathChooser::rawPathChanged, + this, &AndroidSettingsWidget::validateJdk); Utils::FilePath currentJdkPath = m_androidConfig.openJDKLocation(); if (currentJdkPath.isEmpty()) currentJdkPath = findJdkInCommonPaths(); - m_ui->OpenJDKLocationPathChooser->setFileName(currentJdkPath); m_ui->OpenJDKLocationPathChooser->setPromptDialogTitle(tr("Select JDK Path")); + + connect(m_ui->SDKLocationPathChooser, &Utils::PathChooser::rawPathChanged, + this, &AndroidSettingsWidget::onSdkPathChanged); + Utils::FilePath currentSDKPath = m_androidConfig.sdkLocation(); + if (currentSDKPath.isEmpty()) + currentSDKPath = getDefaultSdkPath(); + + m_ui->SDKLocationPathChooser->setFileName(currentSDKPath); + m_ui->SDKLocationPathChooser->setPromptDialogTitle(tr("Select Android SDK folder")); + m_ui->DataPartitionSizeSpinBox->setValue(m_androidConfig.partitionSize()); m_ui->CreateKitCheckBox->setChecked(m_androidConfig.automaticKitCreation()); m_ui->AVDTableView->setModel(&m_AVDModel); @@ -373,19 +421,15 @@ AndroidSettingsWidget::AndroidSettingsWidget() m_ui->downloadSDKToolButton->setIcon(downloadIcon); m_ui->downloadNDKToolButton->setIcon(downloadIcon); m_ui->downloadOpenJDKToolButton->setIcon(downloadIcon); + m_ui->sdkToolsAutoDownloadButton->setIcon(Utils::Icons::RELOAD.icon()); + connect(m_ui->ndkListComboBox, QOverload::of(&QComboBox::currentIndexChanged), + [this](const QString) { validateNdk(); }); connect(&m_virtualDevicesWatcher, &QFutureWatcherBase::finished, this, &AndroidSettingsWidget::updateAvds); connect(m_ui->AVDRefreshPushButton, &QAbstractButton::clicked, this, &AndroidSettingsWidget::startUpdateAvd); - connect(&m_futureWatcher, &QFutureWatcherBase::finished, - this, &AndroidSettingsWidget::avdAdded); - connect(m_ui->NDKLocationPathChooser, &Utils::PathChooser::rawPathChanged, - this, &AndroidSettingsWidget::validateNdk); - connect(m_ui->SDKLocationPathChooser, &Utils::PathChooser::rawPathChanged, - this, &AndroidSettingsWidget::onSdkPathChanged); - connect(m_ui->OpenJDKLocationPathChooser, &Utils::PathChooser::rawPathChanged, - this, &AndroidSettingsWidget::validateJdk); + connect(&m_futureWatcher, &QFutureWatcherBase::finished, this, &AndroidSettingsWidget::avdAdded); connect(m_ui->AVDAddPushButton, &QAbstractButton::clicked, this, &AndroidSettingsWidget::addAVD); connect(m_ui->AVDRemovePushButton, &QAbstractButton::clicked, @@ -402,20 +446,38 @@ AndroidSettingsWidget::AndroidSettingsWidget() this, &AndroidSettingsWidget::manageAVD); connect(m_ui->CreateKitCheckBox, &QAbstractButton::toggled, this, &AndroidSettingsWidget::createKitToggled); - connect(m_ui->downloadSDKToolButton, &QAbstractButton::clicked, - this, &AndroidSettingsWidget::openSDKDownloadUrl); connect(m_ui->downloadNDKToolButton, &QAbstractButton::clicked, this, &AndroidSettingsWidget::openNDKDownloadUrl); + connect(m_ui->downloadSDKToolButton, &QAbstractButton::clicked, + this, &AndroidSettingsWidget::openSDKDownloadUrl); connect(m_ui->downloadOpenJDKToolButton, &QAbstractButton::clicked, this, &AndroidSettingsWidget::openOpenJDKDownloadUrl); // Validate SDK again after any change in SDK packages. - connect(m_sdkManager.get(), &AndroidSdkManager::packageReloadFinished, - this, &AndroidSettingsWidget::validateSdk); - validateJdk(); - validateNdk(); - // Reloading SDK packages is still synchronous. Use zero timer to let settings dialog open + connect(m_sdkManager.get(), + &AndroidSdkManager::packageReloadFinished, + this, + &AndroidSettingsWidget::validateSdk); + connect(m_ui->sdkToolsAutoDownloadButton, &QAbstractButton::clicked, this, [this]() { + if (sdkToolsOk()) { + QMessageBox::warning(this, AndroidSdkDownloader::dialogTitle(), + tr("The selected path already has a valid SDK Tools package.")); + validateSdk(); + return; + } + downloadSdk(); + }); + + auto startOneShot = QSharedPointer::create(); + *startOneShot = connect(m_sdkManager.get(), + &AndroidSdkManager::packageReloadFinished, [this, startOneShot] { + QObject::disconnect(*startOneShot); + if (!sdkToolsOk()) + downloadSdk(); + }); + + // Reloading SDK packages (force) is still synchronous. Use zero timer to let settings dialog open // first. - QTimer::singleShot(0, std::bind(&AndroidSdkManager::reloadPackages, m_sdkManager.get(), false)); + QTimer::singleShot(0, std::bind(&AndroidSdkManager::reloadPackages, m_sdkManager.get(), true)); startUpdateAvd(); } @@ -521,11 +583,11 @@ Utils::FilePath AndroidSettingsWidget::findJdkInCommonPaths() void AndroidSettingsWidget::validateNdk() { - auto ndkPath = Utils::FilePath::fromUserInput(m_ui->NDKLocationPathChooser->rawPath()); + auto ndkPath = Utils::FilePath::fromString(m_ui->ndkListComboBox->currentText()); m_androidConfig.setNdkLocation(ndkPath); auto summaryWidget = static_cast(m_ui->androidDetailsWidget->widget()); - summaryWidget->setPointValid(NdkPathExistsRow, m_androidConfig.ndkLocation().exists()); + summaryWidget->setPointValid(NdkPathExistsRow, ndkPath.exists()); const Utils::FilePath ndkPlatformsDir = ndkPath.pathAppended("platforms"); const Utils::FilePath ndkToolChainsDir = ndkPath.pathAppended("toolchains"); @@ -550,6 +612,9 @@ void AndroidSettingsWidget::onSdkPathChanged() void AndroidSettingsWidget::validateSdk() { + auto sdkPath = Utils::FilePath::fromString(m_ui->SDKLocationPathChooser->rawPath()); + m_androidConfig.setSdkLocation(sdkPath); + auto summaryWidget = static_cast(m_ui->androidDetailsWidget->widget()); summaryWidget->setPointValid(SdkPathExistsRow, m_androidConfig.sdkLocation().exists()); summaryWidget->setPointValid(SdkPathWritableRow, m_androidConfig.sdkLocation().isWritablePath()); @@ -559,19 +624,21 @@ void AndroidSettingsWidget::validateSdk() m_androidConfig.adbToolPath().exists()); summaryWidget->setPointValid(BuildToolsInstalledRow, !m_androidConfig.buildToolsVersion().isNull()); - summaryWidget->setPointValid(SdkManagerSuccessfulRow, m_sdkManager->packageListingSuccessful()); // installedSdkPlatforms should not trigger a package reload as validate SDK is only called // after AndroidSdkManager::packageReloadFinished. summaryWidget->setPointValid(PlatformSdkInstalledRow, !m_sdkManager->installedSdkPlatforms().isEmpty()); + + summaryWidget->setPointValid(AllEssentialsInstalledRow, allEssentialsInstalled()); updateUI(); bool sdkToolsOk = summaryWidget->rowsOk( {SdkPathExistsRow, SdkPathWritableRow, SdkToolsInstalledRow, SdkManagerSuccessfulRow}); bool componentsOk = summaryWidget->rowsOk({PlatformToolsInstalledRow, BuildToolsInstalledRow, - PlatformSdkInstalledRow}); - + PlatformSdkInstalledRow, + AllEssentialsInstalledRow}); + m_androidConfig.setSdkFullyConfigured(sdkToolsOk && componentsOk); if (sdkToolsOk && !componentsOk && !m_androidConfig.useNativeUiTools()) { // Ask user to install essential SDK components. Works only for sdk tools version >= 26.0.0 QString message = tr("Android SDK installation is missing necessary packages. Do you " @@ -583,6 +650,9 @@ void AndroidSettingsWidget::validateSdk() m_sdkManagerWidget->installEssentials(); } } + + updateNdkList(); + validateNdk(); } void AndroidSettingsWidget::openSDKDownloadUrl() @@ -675,7 +745,7 @@ void AndroidSettingsWidget::updateUI() m_ui->sdkManagerTab->setEnabled(sdkToolsOk); m_sdkManagerWidget->setSdkManagerControlsEnabled(!m_androidConfig.useNativeUiTools()); - auto infoText = tr("(SDK Version: %1, NDK Version: %2)") + auto infoText = tr("(SDK Version: %1, NDK Bundle Version: %2)") .arg(m_androidConfig.sdkToolsVersion().toString()) .arg(m_androidConfig.ndkVersion().toString()); androidSummaryWidget->setInfoText(androidSetupOk ? infoText : ""); @@ -699,6 +769,57 @@ void AndroidSettingsWidget::manageAVD() } } +void AndroidSettingsWidget::downloadSdk() +{ + QString message(tr("Android SDK Tools package is not installed. Do you want to download it?\n" + "The final location: ") + + QDir::toNativeSeparators(m_ui->SDKLocationPathChooser->rawPath())); + auto userInput = QMessageBox::information(this, AndroidSdkDownloader::dialogTitle(), + message, QMessageBox::Yes | QMessageBox::No); + if (userInput == QMessageBox::Yes) { + auto javaSummaryWidget = static_cast(m_ui->javaDetailsWidget->widget()); + if (javaSummaryWidget->allRowsOk()) { + auto javaPath = Utils::FilePath::fromUserInput(m_ui->OpenJDKLocationPathChooser->rawPath()); + AndroidSdkDownloader *sdkDownloader = new AndroidSdkDownloader( + m_androidConfig.sdkToolsUrl(), + m_androidConfig.getSdkToolsSha256()); + sdkDownloader->downloadAndExtractSdk(javaPath.toString(), + m_ui->SDKLocationPathChooser->path()); + + connect(sdkDownloader, &AndroidSdkDownloader::sdkExtracted, this, [this]() { + m_sdkManager->reloadPackages(true); + apply(); + }); + + auto showErrorDialog = [this](const QString &error) { + QMessageBox::warning(this, AndroidSdkDownloader::dialogTitle(), error); + }; + + connect(sdkDownloader, &AndroidSdkDownloader::sdkDownloaderError, this, showErrorDialog); + } + } +} + +bool AndroidSettingsWidget::allEssentialsInstalled() +{ + QStringList essentialPkgs(m_androidConfig.allEssentials()); + for (const AndroidSdkPackage *pkg : m_sdkManager->installedSdkPackages()) { + if (essentialPkgs.contains(pkg->sdkStylePath())) + essentialPkgs.removeOne(pkg->sdkStylePath()); + if (essentialPkgs.isEmpty()) + break; + } + return essentialPkgs.isEmpty() ? true : false; +} + +bool AndroidSettingsWidget::sdkToolsOk() const +{ + bool exists = m_androidConfig.sdkLocation().exists(); + bool writable = m_androidConfig.sdkLocation().isWritablePath(); + bool sdkToolsExist = !m_androidConfig.sdkToolsVersion().isNull(); + return exists && writable && sdkToolsExist; +} + // AndroidSettingsPage AndroidSettingsPage::AndroidSettingsPage() diff --git a/src/plugins/android/androidsettingswidget.ui b/src/plugins/android/androidsettingswidget.ui index cc548753573..5d149dddcf5 100644 --- a/src/plugins/android/androidsettingswidget.ui +++ b/src/plugins/android/androidsettingswidget.ui @@ -90,42 +90,6 @@ Android Settings - - - - - - - - 0 - 0 - - - - - - - - Download Android NDK - - - - - - - - 0 - 0 - - - - Android NDK location: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - @@ -142,16 +106,53 @@ - + Download Android SDK - + + + + Android NDK list: + + + + + + + Automatically download Android SDK Tools to selected location. + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + + +