<commit_before>/****************************************************************************
**
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the tools applications of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.html.
**
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QCoreApplication>
#include <QStringList>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QDebug>
#include <QXmlStreamReader>
#include <QXmlStreamAttribute>
#include <QXmlStreamAttributes>
#include <QDateTime>
#include <QStandardPaths>
#include <QUuid>

#include <algorithm>
static const bool mustReadOutputAnyway = true; // pclose seems to return the wrong error code unless we read the output

void deleteRecursively(const QString &dirName)
{
    QDir dir(dirName);
    if (!dir.exists())
        return;

    QFileInfoList entries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
    foreach (QFileInfo entry, entries) {
        if (entry.isDir())
            deleteRecursively(entry.absoluteFilePath());
        else
            QFile::remove(entry.absoluteFilePath());
    }

    QDir().rmdir(dirName);
}

struct Options
{
    Options()
        : helpRequested(false)
        , verbose(false)
        , minimumAndroidVersion(-1)
        , targetAndroidVersion(-1)
        , deploymentMechanism(Bundled)
        , releasePackage(false)
        , digestAlg(QLatin1String("SHA1"))
        , sigAlg(QLatin1String("SHA1withRSA"))
        , internalSf(false)
        , sectionsOnly(false)
        , protectedAuthenticationPath(false)
        , installApk(false)
        , fetchedRemoteModificationDates(false)
    {}

    ~Options()
    {
        if (!temporaryDirectoryName.isEmpty())
            deleteRecursively(temporaryDirectoryName);
    }

    enum DeploymentMechanism
    {
        Bundled,
        Ministro,
        Debug
    };

    bool helpRequested;
    bool verbose;

    // External tools
    QString sdkPath;
    QString ndkPath;
    QString antTool;
    QString jdkPath;

    // Build paths
    QString qtInstallDirectory;
    QString androidSourceDirectory;
    QString outputDirectory;
    QString inputFileName;
    QString applicationBinary;

    // Build information
    QString androidPlatform;
    QString architecture;
    QString toolchainVersion;
    QString toolchainPrefix;
    QString toolPrefix;
    QString ndkHost;

    // Package information
    int minimumAndroidVersion;
    int targetAndroidVersion;
    DeploymentMechanism deploymentMechanism;
    QString appName;
    QString packageName;
    QStringList supportedOrientations;
    QStringList extraLibs;

    // Signing information
    bool releasePackage;
    QString keyStore;
    QString keyStorePassword;
    QString keyStoreAlias;
    QString storeType;
    QString keyPass;
    QString sigFile;
    QString signedJar;
    QString digestAlg;
    QString sigAlg;
    QString tsaUrl;
    QString tsaCert;
    bool internalSf;
    bool sectionsOnly;
    bool protectedAuthenticationPath;

    // Installation information
    bool installApk;
    QString installLocation;

    // Collected information
    typedef QPair<QString, QString> BundledFile;
    QList<BundledFile> bundledFiles;
    QStringList qtDependencies;
    QStringList localLibs;
    QStringList localJars;
    QStringList initClasses;
    QString temporaryDirectoryName;
    bool fetchedRemoteModificationDates;
    QHash<QString, QDateTime> remoteModificationDates;
};

Options parseOptions()
{
    Options options;

    QStringList arguments = QCoreApplication::arguments();
    for (int i=0; i<arguments.size(); ++i) {
        const QString &argument = arguments.at(i);
        if (argument.compare(QLatin1String("--output"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.outputDirectory = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--input"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.inputFileName = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--install"), Qt::CaseInsensitive) == 0) {
            options.installApk = true;
        } else if (argument.compare(QLatin1String("--android-platform"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.androidPlatform = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--help"), Qt::CaseInsensitive) == 0) {
            options.helpRequested = true;
        } else if (argument.compare(QLatin1String("--verbose"), Qt::CaseInsensitive) == 0) {
            options.verbose = true;
        } else if (argument.compare(QLatin1String("--ant"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.antTool = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--deployment"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size()) {
                options.helpRequested = true;
            } else {
                QString deploymentMechanism = arguments.at(++i);
                if (deploymentMechanism.compare(QLatin1String("ministro"), Qt::CaseInsensitive) == 0) {
                    options.deploymentMechanism = Options::Ministro;
                } else if (deploymentMechanism.compare(QLatin1String("debug"), Qt::CaseInsensitive) == 0) {
                    options.deploymentMechanism = Options::Debug;

                    QString temporaryDirectoryName = QStandardPaths::standardLocations(QStandardPaths::TempLocation).at(0)
                            + QLatin1String("/android-build-")
                            + QUuid::createUuid().toString();
                    if (QFileInfo(temporaryDirectoryName).exists()) {
                        fprintf(stderr, "Temporary directory '%s' already exists. Bailing out.\n",
                                qPrintable(temporaryDirectoryName));
                        options.helpRequested = true;
                    } else {
                        options.temporaryDirectoryName = temporaryDirectoryName;
                    }

                } else if (deploymentMechanism.compare(QLatin1String("bundled"), Qt::CaseInsensitive) == 0) {
                    options.deploymentMechanism = Options::Bundled;
                } else {
                    fprintf(stderr, "Unrecognized deployment mechanism: %s\n", qPrintable(deploymentMechanism));
                    options.helpRequested = true;
                }
            }
        } else if (argument.compare(QLatin1String("--device"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.installLocation = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--release"), Qt::CaseInsensitive) == 0) {
            options.releasePackage = true;
        } else if (argument.compare(QLatin1String("--jdk"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.jdkPath = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--sign"), Qt::CaseInsensitive) == 0) {
            if (i + 2 >= arguments.size()) {
                options.helpRequested = true;
            } else {
                options.releasePackage = true;
                options.keyStore = arguments.at(++i);
                options.keyStoreAlias = arguments.at(++i);
            }
        } else if (argument.compare(QLatin1String("--storepass"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.keyStorePassword = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--storetype"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.storeType = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--keypass"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.keyPass = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--sigfile"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.sigFile = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--digestalg"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.digestAlg = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--sigalg"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.sigAlg = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--tsa"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.tsaUrl = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--tsacert"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.tsaCert = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--internalsf"), Qt::CaseInsensitive) == 0) {
            options.internalSf = true;
        } else if (argument.compare(QLatin1String("--sectionsonly"), Qt::CaseInsensitive) == 0) {
            options.sectionsOnly = true;
        } else if (argument.compare(QLatin1String("--protected"), Qt::CaseInsensitive) == 0) {
            options.protectedAuthenticationPath = true;
        }
    }

    if (options.inputFileName.isEmpty())
        options.inputFileName = QString::fromLatin1("android-lib%1.so-deployment-settings.json").arg(QDir::current().dirName());

    return options;
}

void printHelp()
{//                 "012345678901234567890123456789012345678901234567890123456789012345678901"
    fprintf(stderr, "Syntax: %s --output <destination> [options]\n"
                    "\n"
                    "  Creates an Android package in the build directory <destination> and\n"
                    "  builds it into an .apk file.\n\n"
                    "  Optional arguments:\n"
                    "    --input <inputfile>: Reads <inputfile> for options generated by\n"
                    "       qmake. A default file name based on the current working\n"
                    "       directory will be used if nothing else is specified.\n"
                    "    --deployment <mechanism>: Supported deployment mechanisms:\n"
                    "       bundled (default): Include Qt files in stand-alone package.\n"
                    "       ministro: Use the Ministro service to manage Qt files.\n"
                    "       debug: Copy Qt files to device for quick debugging.\n"
                    "    --install: Installs apk to device/emulator. By default this step is\n"
                    "       not taken.\n"
                    "    --device [device ID]: Use specified device for deployment. Default\n"
                    "       is the device selected by default by adb.\n"
                    "    --android-platform <platform>: Builds against the given android\n"
                    "       platform. By default, the highest available version will be\n"
                    "       used.\n"
                    "    --ant <path/to/ant>: If unspecified, ant from the PATH will be\n"
                    "       used."
                    "    --release: Builds a package ready for release. By default, the\n"
                    "       package will be signed with a debug key.\n"
                    "    --sign <url/to/keystore> <alias>: Signs the package with the\n"
                    "       specified keystore, alias and store password. Also implies the\n"
                    "       --release option.\n"
                    "       Optional arguments for use with signing:\n"
                    "         --storepass <password>: Keystore password.\n"
                    "         --storetype <type>: Keystore type.\n"
                    "         --keypass <password>: Password for private key (if different\n"
                    "           from keystore password.)\n"
                    "         --sigfile <file>: Name of .SF/.DSA file.\n"
                    "         --digestalg <name>: Name of digest algorithm. Default is\n"
                    "           \"SHA1\".\n"
                    "         --sigalg <name>: Name of signature algorithm. Default is\n"
                    "           \"SHA1withRSA\".\n"
                    "         --tsa <url>: Location of the Time Stamping Authority.\n"
                    "         --tsacert <alias>: Public key certificate for TSA.\n"
                    "         --internalsf: Include the .SF file inside the signature block.\n"
                    "         --sectionsonly: Don't compute hash of entire manifest.\n"
                    "         --protected: Keystore has protected authentication path.\n"
                    "    --jdk <path/to/jdk>: Used to find the jarsigner tool when used\n"
                    "       in combination with the --release argument. By default,\n"
                    "       an attempt is made to detect the tool using the JAVA_HOME and\n"
                    "       PATH environment variables, in that order.\n"
                    "    --verbose: Prints out information during processing.\n"
                    "    --help: Displays this information.\n\n",
                    qPrintable(QCoreApplication::arguments().at(0))
            );
}

// Since strings compared will all start with the same letters,
// sorting by length and then alphabetically within each length
// gives the natural order.
bool quasiLexicographicalReverseLessThan(const QFileInfo &fi1, const QFileInfo &fi2)
{
    QString s1 = fi1.baseName();
    QString s2 = fi2.baseName();

    if (s1.length() == s2.length())
        return s1 > s2;
    else
        return s1.length() > s2.length();
}

// Files which contain templates that need to be overwritten by build data should be overwritten every
// time.
bool alwaysOverwritableFile(const QString &fileName)
{
    return (fileName.endsWith(QLatin1String("/res/values/libs.xml"))
            || fileName.endsWith(QLatin1String("/AndroidManifest.xml"))
            || fileName.endsWith(QLatin1String("/res/values/strings.xml"))
            || fileName.endsWith(QLatin1String("/src/org/qtproject/qt5/android/bindings/QtActivity.java")));
}

bool copyFileIfNewer(const QString &sourceFileName, const QString &destinationFileName, bool verbose)
{
    if (QFile::exists(destinationFileName)) {
        QFileInfo destinationFileInfo(destinationFileName);
        QFileInfo sourceFileInfo(sourceFileName);

        if (sourceFileInfo.lastModified() <= destinationFileInfo.lastModified() && !alwaysOverwritableFile(destinationFileName)) {
            if (verbose)
                fprintf(stdout, "  -- Skipping file %s. Same or newer file already in place.\n", qPrintable(sourceFileName));
            return true;
        } else {
            if (!QFile(destinationFileName).remove()) {
                fprintf(stderr, "Can't remove old file: %s\n", qPrintable(destinationFileName));
                return false;
            }
        }
    }

    if (!QDir().mkpath(QFileInfo(destinationFileName).path())) {
        fprintf(stderr, "Cannot make output directory for %s.\n", qPrintable(destinationFileName));
        return false;
    }

    if (!QFile::exists(destinationFileName) && !QFile::copy(sourceFileName, destinationFileName)) {
        fprintf(stderr, "Failed to copy %s to %s.\n", qPrintable(sourceFileName), qPrintable(destinationFileName));
        return false;
    } else if (verbose) {
        fprintf(stdout, "  -- Copied %s\n", qPrintable(destinationFileName));
    }

    return true;
}

QString detectLatestAndroidPlatform(const QString &sdkPath)
{
    QDir dir(sdkPath + QLatin1String("/platforms"));
    if (!dir.exists()) {
        fprintf(stderr, "Directory %s does not exist\n", qPrintable(dir.absolutePath()));
        return QString();
    }

    QFileInfoList fileInfos = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
    if (fileInfos.isEmpty()) {
        fprintf(stderr, "No platforms found in %s", qPrintable(dir.absolutePath()));
        return QString();
    }

    std::sort(fileInfos.begin(), fileInfos.end(), quasiLexicographicalReverseLessThan);

    QFileInfo latestPlatform = fileInfos.first();
    return latestPlatform.baseName();
}

bool readInputFile(Options *options)
{
    QFile file(options->inputFileName);
    if (!file.open(QIODevice::ReadOnly)) {
        fprintf(stderr, "Cannot read from input file: %s\n", qPrintable(options->inputFileName));
        return false;
    }

    QJsonDocument jsonDocument = QJsonDocument::fromJson(file.readAll());
    if (jsonDocument.isNull()) {
        fprintf(stderr, "Invalid json file: %s\n", qPrintable(options->inputFileName));
        return false;
    }

    QJsonObject jsonObject = jsonDocument.object();

    {
        QJsonValue sdkPath = jsonObject.value(QLatin1String("sdk"));
        if (sdkPath.isUndefined()) {
            fprintf(stderr, "No SDK path in json file %s\n", qPrintable(options->inputFileName));
            return false;
        }

        options->sdkPath = sdkPath.toString();

        if (options->androidPlatform.isEmpty()) {
            options->androidPlatform = detectLatestAndroidPlatform(options->sdkPath);
            if (options->androidPlatform.isEmpty())
                return false;
        } else {
            if (!QDir(options->sdkPath + QLatin1String("/platforms/") + options->androidPlatform).exists()) {
                fprintf(stderr, "Warning: Android platform '%s' does not exist in NDK.\n",
                        qPrintable(options->androidPlatform));
            }
        }
    }

    {
        QJsonValue qtInstallDirectory = jsonObject.value("qt");
        if (qtInstallDirectory.isUndefined()) {
            fprintf(stderr, "No Qt directory in json file %s\n", qPrintable(options->inputFileName));
            return false;
        }
        options->qtInstallDirectory = qtInstallDirectory.toString();
    }

    {
        QJsonValue androidSourcesDirectory = jsonObject.value("android-package-source-directory");
        if (!androidSourcesDirectory.isUndefined())
            options->androidSourceDirectory = androidSourcesDirectory.toString();
    }

    {
        QJsonValue applicationBinary = jsonObject.value("application-binary");
        if (applicationBinary.isUndefined()) {
            fprintf(stderr, "No application binary defined in json file.\n");
            return false;
        }
        options->applicationBinary = applicationBinary.toString();

        if (!QFile::exists(options->applicationBinary)) {
            fprintf(stderr, "Cannot find application binary %s.\n", qPrintable(options->applicationBinary));
            return false;
        }
    }

    {
        QJsonValue deploymentDependencies = jsonObject.value("deployment-dependencies");
        if (!deploymentDependencies.isUndefined())
            options->qtDependencies = deploymentDependencies.toString().split(QLatin1Char(','));
    }


    {
        QJsonValue targetArchitecture = jsonObject.value("target-architecture");
        if (targetArchitecture.isUndefined()) {
            fprintf(stderr, "No target architecture defined in json file.\n");
            return false;
        }
        options->architecture = targetArchitecture.toString();
    }

    {
        QJsonValue ndk = jsonObject.value("ndk");
        if (ndk.isUndefined()) {
            fprintf(stderr, "No NDK path defined in json file.\n");
            return false;
        }
        options->ndkPath = ndk.toString();
    }

    {
        QJsonValue toolchainPrefix = jsonObject.value("toolchain-prefix");
        if (toolchainPrefix.isUndefined()) {
            fprintf(stderr, "No toolchain prefix defined in json file.\n");
            return false;
        }
        options->toolchainPrefix = toolchainPrefix.toString();
    }

    {
        QJsonValue toolPrefix = jsonObject.value("tool-prefix");
        if (toolPrefix.isUndefined()) {
            fprintf(stderr, "Warning: No tool prefix defined in json file.\n");
            options->toolPrefix = options->toolchainPrefix;
        } else {
            options->toolPrefix = toolPrefix.toString();
        }
    }

    {
        QJsonValue toolchainVersion = jsonObject.value("toolchain-version");
        if (toolchainVersion.isUndefined()) {
            fprintf(stderr, "No toolchain version defined in json file.\n");
            return false;
        }
        options->toolchainVersion = toolchainVersion.toString();
    }

    {
        QJsonValue ndkHost = jsonObject.value("ndk-host");
        if (ndkHost.isUndefined()) {
            fprintf(stderr, "No NDK host defined in json file.\n");
            return false;
        }
        options->ndkHost = ndkHost.toString();
    }

    {
        QJsonValue androidMinimumVersion = jsonObject.value("android-minimum-version");
        if (!androidMinimumVersion.isUndefined()) {
            options->minimumAndroidVersion = androidMinimumVersion.toInt();
            if (options->minimumAndroidVersion < 9)
                fprintf(stderr, "Warning: Unsupported minimum version");
        }
    }

    {
        QJsonValue androidTargetVersion = jsonObject.value("android-target-version");
        if (!androidTargetVersion.isUndefined()) {
            options->targetAndroidVersion = androidTargetVersion.toInt();
            if (options->targetAndroidVersion < 9)
                fprintf(stderr, "Warning: Unsupported target version");
        }
    }

    if (options->minimumAndroidVersion < 0) {
        options->minimumAndroidVersion = 9;
        if (options->targetAndroidVersion < 0)
            options->targetAndroidVersion = 14;
    }

    {
        QJsonValue androidAppName = jsonObject.value("android-app-name");
        if (!androidAppName.isUndefined())
            options->appName = androidAppName.toString();
        else
            options->appName = QFileInfo(options->applicationBinary).baseName().mid(sizeof("lib") - 1);
    }


    {
        QJsonValue androidPackage = jsonObject.value("android-package");
        if (!androidPackage.isUndefined())
            options->packageName = androidPackage.toString();
        else
            options->packageName = QString::fromLatin1("org.qtproject.example.%1").arg(options->appName);
    }

    {
        QJsonValue supportedOrientations = jsonObject.value("supported-orientations");
        if (!supportedOrientations.isUndefined())
            options->supportedOrientations = supportedOrientations.toString().split(QLatin1Char(','));
    }

    {
        QJsonValue extraLibs = jsonObject.value("android-extra-libs");
        if (!extraLibs.isUndefined())
            options->extraLibs = extraLibs.toString().split(QLatin1Char(','));
    }

    return true;
}

bool copyFiles(const QDir &sourceDirectory, const QDir &destinationDirectory, bool verbose)
{
    QFileInfoList entries = sourceDirectory.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
    foreach (QFileInfo entry, entries) {
        if (entry.isDir()) {
            QDir dir(entry.absoluteFilePath());
            if (!destinationDirectory.mkpath(dir.dirName())) {
                fprintf(stderr, "Cannot make directory %s in %s\n", qPrintable(dir.dirName()), qPrintable(destinationDirectory.path()));
                return false;
            }

            if (!copyFiles(dir, QDir(destinationDirectory.path() + QLatin1String("/") + dir.dirName()), verbose))
                return false;
        } else {
            QString destination = destinationDirectory.absoluteFilePath(entry.fileName());
            if (!copyFileIfNewer(entry.absoluteFilePath(), destination, verbose))
                return false;
        }
    }

    return true;
}

bool copyAndroidTemplate(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "Copying Android package template.\n");

    QDir sourceDirectory(options.qtInstallDirectory + QLatin1String("/src/android/java"));
    if (!sourceDirectory.exists()) {
        fprintf(stderr, "Cannot find template directory %s\n", qPrintable(sourceDirectory.absolutePath()));
        return false;
    }

    if (!QDir::current().mkpath(options.outputDirectory)) {
        fprintf(stderr, "Cannot create output directory %s\n", qPrintable(options.outputDirectory));
        return false;
    }

    return copyFiles(sourceDirectory, QDir(options.outputDirectory), options.verbose);
}

bool copyAndroidSources(const Options &options)
{
    if (options.androidSourceDirectory.isEmpty())
        return true;

    if (options.verbose)
        fprintf(stdout, "Copying Android sources from project.\n");

    QDir sourceDirectory(options.androidSourceDirectory);
    if (!sourceDirectory.exists()) {
        fprintf(stderr, "Cannot find android sources in %s", qPrintable(options.androidSourceDirectory));
        return false;
    }

    return copyFiles(sourceDirectory, QDir(options.outputDirectory), options.verbose);
}

bool copyAndroidExtraLibs(const Options &options)
{
    if (options.extraLibs.isEmpty())
        return true;

    if (options.verbose)
        fprintf(stdout, "Copying %d external libraries to package.\n", options.extraLibs.size());

    foreach (QString extraLib, options.extraLibs) {
        QFileInfo extraLibInfo(extraLib);
        if (!extraLibInfo.exists()) {
            fprintf(stderr, "External library %s does not exist!\n", qPrintable(extraLib));
            return false;
        }

        if (!extraLibInfo.fileName().startsWith(QLatin1String("lib")) || extraLibInfo.suffix() != QLatin1String("so")) {
            fprintf(stderr, "The file name of external library %s must begin with \"lib\" and end with the suffix \".so\".\n",
                    qPrintable(extraLib));
            return false;
        }

        QString destinationFile(options.outputDirectory
                                + QLatin1String("/libs/")
                                + options.architecture
                                + QLatin1Char('/')
                                + extraLibInfo.fileName());

        if (!copyFileIfNewer(extraLib, destinationFile, options.verbose))
            return false;
    }

    return true;
}

bool updateFile(const QString &fileName, const QHash<QString, QString> &replacements)
{
    QFile inputFile(fileName);
    if (!inputFile.open(QIODevice::ReadOnly)) {
        fprintf(stderr, "Cannot open %s for reading.\n", qPrintable(fileName));
        return false;
    }

    // All the files we are doing substitutes in are quite small. If this
    // ever changes, this code should be updated to be more conservative.
    QByteArray contents = inputFile.readAll();

    bool hasReplacements = false;
    QHash<QString, QString>::const_iterator it;
    for (it = replacements.constBegin(); it != replacements.constEnd(); ++it) {
        int index = contents.indexOf(it.key().toUtf8());
        if (index >= 0) {
            contents.replace(index, it.key().length(), it.value().toUtf8());
            hasReplacements = true;
        }
    }

    if (hasReplacements) {
        inputFile.close();

        if (!inputFile.open(QIODevice::WriteOnly)) {
            fprintf(stderr, "Cannot open %s for writing.\n", qPrintable(fileName));
            return false;
        }

        inputFile.write(contents);
    }

    return true;

}

bool updateLibsXml(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "  -- res/values/libs.xml\n");

    QString fileName = options.outputDirectory + QLatin1String("/res/values/libs.xml");
    if (!QFile::exists(fileName)) {
        fprintf(stderr, "Cannot find %s in prepared packaged. This file is required.\n", qPrintable(fileName));
        return false;
    }

    QString libsPath = QLatin1String("libs/") + options.architecture + QLatin1Char('/');

    QString qtLibs = QLatin1String("<item>gnustl_shared</item>\n");
    QString bundledInLibs;
    QString bundledInAssets;
    foreach (Options::BundledFile bundledFile, options.bundledFiles) {
        if (bundledFile.second.startsWith(QLatin1String("lib/"))) {
            QString s = bundledFile.second.mid(sizeof("lib/lib") - 1);
            s.chop(sizeof(".so") - 1);
            qtLibs += QString::fromLatin1("<item>%1</item>\n").arg(s);
        } else if (bundledFile.first.startsWith(libsPath)) {
            QString s = bundledFile.first.mid(libsPath.length());
            bundledInLibs += QString::fromLatin1("<item>%1:%2</item>\n")
                    .arg(s).arg(bundledFile.second);
        } else if (bundledFile.first.startsWith(QLatin1String("assets/"))) {
            QString s = bundledFile.first.mid(sizeof("assets/") - 1);
            bundledInAssets += QString::fromLatin1("<item>%1:%2</item>\n")
                    .arg(s).arg(bundledFile.second);
        }
    }

    QHash<QString, QString> replacements;
    replacements[QLatin1String("<!-- %%INSERT_QT_LIBS%% -->")] = qtLibs;
    if (options.deploymentMechanism == Options::Bundled) {
        replacements[QLatin1String("<!-- %%INSERT_BUNDLED_IN_LIB%% -->")] = bundledInLibs;
        replacements[QLatin1String("<!-- %%INSERT_BUNDLED_IN_ASSETS%% -->")] = bundledInAssets;
    }

    QString extraLibs;
    if (!options.extraLibs.isEmpty()) {
        foreach (QString extraLib, options.extraLibs) {
            QFileInfo extraLibInfo(extraLib);
            QString name = extraLibInfo.fileName().mid(sizeof("lib") - 1);
            name.chop(sizeof(".so") - 1);

            extraLibs += QLatin1String("<item>") + name + QLatin1String("</item>\n");
        }
    }
    replacements[QLatin1String("<!-- %%INSERT_EXTRA_LIBS%% -->")] = extraLibs;

    if (!updateFile(fileName, replacements))
        return false;

    return true;
}

bool updateAndroidManifest(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "  -- AndroidManifest.xml \n");

    QString usesSdk = QString("<uses-sdk android:minSdkVersion=\"%1\" %2 />").arg(options.minimumAndroidVersion);
    if (options.targetAndroidVersion >= 0)
        usesSdk = usesSdk.arg(QString::fromLatin1("android:targetSdkVersion=\"%1\"").arg(options.targetAndroidVersion));

    QString androidOrientation;
    if (options.supportedOrientations.size() == 1) {
        const QString &orientation = options.supportedOrientations.at(0);
        if (orientation == QLatin1String("Portrait"))
            androidOrientation = QLatin1String("portrait");
        else if (orientation == QLatin1String("Landscape"))
            androidOrientation = QLatin1String("landscape");
        else if (orientation == QLatin1String("InvertedLandscape"))
            androidOrientation = QLatin1String("reverseLandscape");
        else if (orientation == QLatin1String("InvertedPortrait"))
            androidOrientation = QLatin1String("reversePortrait");
    } else if (options.supportedOrientations.size() > 1) {
        fprintf(stderr, "Warning: Multiple supported orientations specified. Orientation of Android app will be unspecified.");
    }

    QHash<QString, QString> replacements;
    replacements[QLatin1String("<!-- %%INSERT_USES_SDK%% -->")] = usesSdk;
    replacements[QLatin1String("package=\"org.qtproject.example\"")] = QString::fromLatin1("package=\"%1\"").arg(options.packageName);

    if (options.deploymentMechanism == Options::Ministro) {
        replacements[QLatin1String("<meta-data android:value=\"1\" android:name=\"android.app.use_local_qt_libs\"/>")]
                = QString::fromLatin1("<meta-data android:value=\"0\" android:name=\"android.app.use_local_qt_libs\"/>");
    }

    if (options.deploymentMechanism != Options::Bundled) {
        replacements[QLatin1String("<meta-data android:value=\"1\" android:name=\"android.app.bundle_local_qt_libs\"/>")]
                = QString::fromLatin1("<meta-data android:value=\"0\" android:name=\"android.app.bundle_local_qt_libs\"/>");
    }

    if (!androidOrientation.isEmpty())
        replacements[QLatin1String("android:screenOrientation=\"unspecified\"")] = QString::fromLatin1("android:screenOrientation=\"%1\"").arg(androidOrientation);
    if (!updateFile(options.outputDirectory + QLatin1String("/AndroidManifest.xml"), replacements))
        return false;

    return true;
}

bool updateStringsXml(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "  -- res/values/strings.xml\n");

    QStringList localLibs = options.localLibs;

    // If .pro file overrides dependency detection, we need to see which platform plugin they picked
    if (localLibs.isEmpty()) {
        QString plugin;
        foreach (QString qtDependency, options.qtDependencies) {
            if (qtDependency.endsWith(QLatin1String("libqtforandroid.so"))
                    || qtDependency.endsWith(QLatin1String("libqtforandroidGL.so"))) {
                if (!plugin.isEmpty() && plugin != qtDependency) {
                    fprintf(stderr, "Both platform plugins libqtforandroid.so and libqtforandroidGL.so included in package. Please include only one.\n");
                    return false;
                }

                plugin = qtDependency;
            }
        }

        if (plugin.isEmpty()) {
            fprintf(stderr, "No platform plugin, neither libqtforandroid.so or libqtforandroidGL.so, included in package. Please include one.\n");
            return false;
        }

        localLibs.append(plugin);
        if (options.verbose)
            fprintf(stdout, "  -- Using platform plugin %s\n", qPrintable(plugin));
    }

    QHash<QString, QString> replacements;
    replacements[QLatin1String("<!-- %%INSERT_APP_NAME%% -->")] = options.appName;
    replacements[QLatin1String("<!-- %%INSERT_APP_LIB_NAME%% -->")] = QFileInfo(options.applicationBinary).baseName().mid(sizeof("lib") - 1);
    replacements[QLatin1String("<!-- %%INSERT_LOCAL_LIBS%% -->")] = localLibs.join(QLatin1Char(':'));
    replacements[QLatin1String("<!-- %%INSERT_LOCAL_JARS%% -->")] = options.localJars.join(QLatin1Char(':'));
    replacements[QLatin1String("<!-- %%INSERT_INIT_CLASSES%% -->")] = options.initClasses.join(QLatin1Char(':'));

    QString fileName = options.outputDirectory + QLatin1String("/res/values/strings.xml");
    if (!QFile::exists(fileName)) {
        if (options.verbose)
            fprintf(stdout, "  -- Skipping update of strings.xml since it's missing.\n");
        return true;
    }

    if (!updateFile(fileName, replacements))
        return false;

    return true;
}

bool updateJavaFiles(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "  -- /src/org/qtproject/qt5/android/bindings/QtActivity.java\n");

    QString fileName(options.outputDirectory + QLatin1String("/src/org/qtproject/qt5/android/bindings/QtActivity.java"));
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly)) {
        fprintf(stderr, "Cannot open %s.\n", qPrintable(fileName));
        return false;
    }

    QByteArray contents;

    while (!file.atEnd()) {
        QByteArray line = file.readLine();
        if (line.startsWith("//@ANDROID-")) {
            bool ok;
            int requiredSdkVersion = line.mid(sizeof("//@ANDROID-") - 1).trimmed().toInt(&ok);
            if (requiredSdkVersion <= options.minimumAndroidVersion)
                contents += line;

            if (ok) {
                while (!file.atEnd()) {
                    line = file.readLine();
                    if (requiredSdkVersion <= options.minimumAndroidVersion)
                        contents += line;

                    if (line.startsWith(QByteArray("//@ANDROID-") + QByteArray::number(requiredSdkVersion)))
                        break;
                }

                if (!line.startsWith(QByteArray("//@ANDROID-") + QByteArray::number(requiredSdkVersion))) {
                    fprintf(stderr, "Mismatched tag ANDROID-%d in %s\n", requiredSdkVersion, qPrintable(fileName));
                    return false;
                }
            } else {
                fprintf(stderr, "Warning: Misformatted tag in %s: %s\n", qPrintable(fileName), line.constData());
            }
        } else {
            contents += line;
        }
    }

    file.close();

    if (!file.open(QIODevice::WriteOnly)) {
        fprintf(stderr, "Can't open %s for writing.\n", qPrintable(fileName));
        return false;
    }

    if (file.write(contents) < contents.size()) {
        fprintf(stderr, "Failed to write contents to %s.\n", qPrintable(fileName));
        return false;
    }

    return true;
}

bool updateAndroidFiles(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "Updating Android package files with project settings.\n");

    if (!updateLibsXml(options))
        return false;

    if (!updateAndroidManifest(options))
        return false;

    if (!updateStringsXml(options))
        return false;

    if (!updateJavaFiles(options))
        return false;

    return true;
}

QStringList findFilesRecursively(const Options &options, const QString &fileName)
{
    QFileInfo info(options.qtInstallDirectory + QLatin1Char('/') + fileName);
    if (!info.exists()) {
        fprintf(stderr, "Warning: Dependency not found: %s\n", qPrintable(info.filePath()));
        return QStringList();
    }

    if (info.isDir()) {
        QStringList ret;

        QDir dir(info.filePath());
        QStringList entries = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);

        foreach (QString entry, entries) {
            QString s = fileName + QLatin1Char('/') + entry;
            ret += findFilesRecursively(options, s);
        }

        return ret;
    } else {
        return QStringList() << fileName;
    }
}

bool readAndroidDependencyXml(Options *options,
                              const QString &moduleName,
                              QSet<QString> *usedDependencies,
                              QSet<QString> *remainingDependencies)
{
    QString androidDependencyName = options->qtInstallDirectory + QString::fromLatin1("/lib/%1-android-dependencies.xml").arg(moduleName);

    QFile androidDependencyFile(androidDependencyName);
    if (androidDependencyFile.exists()) {
        if (options->verbose)
            fprintf(stdout, "Reading Android dependencies for %s\n", qPrintable(moduleName));

        if (!androidDependencyFile.open(QIODevice::ReadOnly)) {
            fprintf(stderr, "Cannot open %s for reading.\n", qPrintable(androidDependencyName));
            return false;
        }

        QXmlStreamReader reader(&androidDependencyFile);
        while (!reader.atEnd()) {
            reader.readNext();

            if (reader.isStartElement()) {
                if (reader.name() == QLatin1String("bundled")) {
                    if (!reader.attributes().hasAttribute(QLatin1String("file"))) {
                        fprintf(stderr, "Invalid android dependency file: %s\n", qPrintable(androidDependencyName));
                        return false;
                    }

                    QString file = reader.attributes().value(QLatin1String("file")).toString();
                    QStringList fileNames = findFilesRecursively(*options, file);
                    foreach (QString fileName, fileNames) {
                        if (usedDependencies->contains(fileName))
                            continue;

                        usedDependencies->insert(fileName);
                        if (fileName.endsWith(QLatin1String(".so"))) {
                            remainingDependencies->insert(fileName);
                        }

                        options->qtDependencies.append(fileName);
                    }
                } else if (reader.name() == QLatin1String("jar")) {
                    int bundling = reader.attributes().value(QLatin1String("bundling")).toInt();
                    QString fileName = reader.attributes().value(QLatin1String("file")).toString();
                    if (bundling == (options->deploymentMechanism == Options::Bundled)) {
                        if (!usedDependencies->contains(fileName)) {
                            options->qtDependencies.append(fileName);
                            usedDependencies->insert(fileName);
                        }
                    }

                    if (!fileName.isEmpty())
                        options->localJars.append(fileName);

                    if (reader.attributes().hasAttribute(QLatin1String("initClass"))) {
                        options->initClasses.append(reader.attributes().value(QLatin1String("initClass")).toString());
                    }
                } else if (reader.name() == QLatin1String("lib")) {
                    QString fileName = reader.attributes().value(QLatin1String("file")).toString();
                    if (reader.attributes().hasAttribute(QLatin1String("replaces"))) {
                        QString replaces = reader.attributes().value(QLatin1String("replaces")).toString();
                        for (int i=0; i<options->localLibs.size(); ++i) {
                            if (options->localLibs.at(i) == replaces) {
                                options->localLibs[i] = fileName;
                                break;
                            }
                        }
                    } else if (!fileName.isEmpty()) {
                        options->localLibs.append(fileName);
                    }
                }
            }
        }

        if (reader.hasError()) {
            fprintf(stderr, "Error in %s: %s\n", qPrintable(androidDependencyName), qPrintable(reader.errorString()));
            return false;
        }
    } else if (options->verbose) {
        fprintf(stdout, "No android dependencies for %s\n", qPrintable(moduleName));
    }

    return true;
}

QStringList getQtLibsFromElf(const Options &options, const QString &fileName)
{
    QString readElf = options.ndkPath
            + QLatin1String("/toolchains/")
            + options.toolchainPrefix
            + QLatin1Char('-')
            + options.toolchainVersion
            + QLatin1String("/prebuilt/")
            + options.ndkHost
            + QLatin1String("/bin/")
            + options.toolPrefix
            + QLatin1String("-readelf");
#if defined(Q_OS_WIN32)
    readElf += QLatin1String(".exe");
#endif

    if (!QFile::exists(readElf)) {
        fprintf(stderr, "Command does not exist: %s\n", qPrintable(readElf));
        return QStringList();
    }

    readElf = QString::fromLatin1("\"%1\" -d -W %2").arg(readElf).arg(fileName);

    FILE *readElfCommand = popen(readElf.toLocal8Bit().constData(), "r");
    if (readElfCommand == 0) {
        fprintf(stderr, "Cannot execute command %s", qPrintable(readElf));
        return QStringList();
    }

    QStringList ret;

    char buffer[512];
    while (fgets(buffer, sizeof(buffer), readElfCommand) != 0) {
        QByteArray line = QByteArray::fromRawData(buffer, qstrlen(buffer));
        if (line.contains("(NEEDED)") && line.contains("Shared library:") ) {
            const int pos = line.lastIndexOf('[') + 1;
            QString libraryName = QLatin1String("lib/") + QString::fromLatin1(line.mid(pos, line.length() - pos - 2));
            if (QFile::exists(options.qtInstallDirectory + QLatin1Char('/') + libraryName)) {
                ret += libraryName;
            }

        }
    }

    pclose(readElfCommand);

    return ret;
}

bool readDependenciesFromElf(Options *options,
                             const QString &fileName,
                             QSet<QString> *usedDependencies,
                             QSet<QString> *remainingDependencies)
{
    // Get dependencies on libraries in $QTDIR/lib
    QStringList dependencies = getQtLibsFromElf(*options, fileName);

    if (options->verbose) {
        fprintf(stdout, "Reading dependencies from %s\n", qPrintable(fileName));
        foreach (QString dep, dependencies)
            fprintf(stdout, "      %s\n", qPrintable(dep));
    }
    // Recursively add dependencies from ELF and supplementary XML information
    foreach (QString dependency, dependencies) {
        if (usedDependencies->contains(dependency))
            continue;

        usedDependencies->insert(dependency);
        if (!readDependenciesFromElf(options,
                              options->qtInstallDirectory + QLatin1Char('/') + dependency,
                              usedDependencies,
                              remainingDependencies)) {
            return false;
        }

        options->qtDependencies.append(dependency);
        if (options->verbose)
            fprintf(stderr, "Appending dependency: %s\n", qPrintable(dependency));
    }
    // Add the implicit dependencies for later
    foreach (QString dependency, dependencies) {
        if (dependency.startsWith(QLatin1String("lib/lib")) && dependency.endsWith(QLatin1String(".so"))) {
            QString qtBaseName = dependency.mid(sizeof("lib/lib") - 1);
            qtBaseName = qtBaseName.left(qtBaseName.size() - (sizeof(".so") - 1));
            if (!readAndroidDependencyXml(options, qtBaseName, usedDependencies, remainingDependencies)) {
                return false;
            }
        }
    }
    return true;
}


bool readDependencies(Options *options)
{
    if (options->verbose)
        fprintf(stdout, "Detecting dependencies of application.\n");

    // Override set in .pro file<commit_msg>Fix handling android dependency xml.<commit_after>/****************************************************************************
**
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the tools applications of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.html.
**
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QCoreApplication>
#include <QStringList>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QDebug>
#include <QXmlStreamReader>
#include <QXmlStreamAttribute>
#include <QXmlStreamAttributes>
#include <QDateTime>
#include <QStandardPaths>
#include <QUuid>

#include <algorithm>
static const bool mustReadOutputAnyway = true; // pclose seems to return the wrong error code unless we read the output

void deleteRecursively(const QString &dirName)
{
    QDir dir(dirName);
    if (!dir.exists())
        return;

    QFileInfoList entries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
    foreach (QFileInfo entry, entries) {
        if (entry.isDir())
            deleteRecursively(entry.absoluteFilePath());
        else
            QFile::remove(entry.absoluteFilePath());
    }

    QDir().rmdir(dirName);
}

struct Options
{
    Options()
        : helpRequested(false)
        , verbose(false)
        , minimumAndroidVersion(-1)
        , targetAndroidVersion(-1)
        , deploymentMechanism(Bundled)
        , releasePackage(false)
        , digestAlg(QLatin1String("SHA1"))
        , sigAlg(QLatin1String("SHA1withRSA"))
        , internalSf(false)
        , sectionsOnly(false)
        , protectedAuthenticationPath(false)
        , installApk(false)
        , fetchedRemoteModificationDates(false)
    {}

    ~Options()
    {
        if (!temporaryDirectoryName.isEmpty())
            deleteRecursively(temporaryDirectoryName);
    }

    enum DeploymentMechanism
    {
        Bundled,
        Ministro,
        Debug
    };

    bool helpRequested;
    bool verbose;

    // External tools
    QString sdkPath;
    QString ndkPath;
    QString antTool;
    QString jdkPath;

    // Build paths
    QString qtInstallDirectory;
    QString androidSourceDirectory;
    QString outputDirectory;
    QString inputFileName;
    QString applicationBinary;

    // Build information
    QString androidPlatform;
    QString architecture;
    QString toolchainVersion;
    QString toolchainPrefix;
    QString toolPrefix;
    QString ndkHost;

    // Package information
    int minimumAndroidVersion;
    int targetAndroidVersion;
    DeploymentMechanism deploymentMechanism;
    QString appName;
    QString packageName;
    QStringList supportedOrientations;
    QStringList extraLibs;

    // Signing information
    bool releasePackage;
    QString keyStore;
    QString keyStorePassword;
    QString keyStoreAlias;
    QString storeType;
    QString keyPass;
    QString sigFile;
    QString signedJar;
    QString digestAlg;
    QString sigAlg;
    QString tsaUrl;
    QString tsaCert;
    bool internalSf;
    bool sectionsOnly;
    bool protectedAuthenticationPath;

    // Installation information
    bool installApk;
    QString installLocation;

    // Collected information
    typedef QPair<QString, QString> BundledFile;
    QList<BundledFile> bundledFiles;
    QStringList qtDependencies;
    QStringList localLibs;
    QStringList localJars;
    QStringList initClasses;
    QString temporaryDirectoryName;
    bool fetchedRemoteModificationDates;
    QHash<QString, QDateTime> remoteModificationDates;
};

Options parseOptions()
{
    Options options;

    QStringList arguments = QCoreApplication::arguments();
    for (int i=0; i<arguments.size(); ++i) {
        const QString &argument = arguments.at(i);
        if (argument.compare(QLatin1String("--output"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.outputDirectory = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--input"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.inputFileName = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--install"), Qt::CaseInsensitive) == 0) {
            options.installApk = true;
        } else if (argument.compare(QLatin1String("--android-platform"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.androidPlatform = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--help"), Qt::CaseInsensitive) == 0) {
            options.helpRequested = true;
        } else if (argument.compare(QLatin1String("--verbose"), Qt::CaseInsensitive) == 0) {
            options.verbose = true;
        } else if (argument.compare(QLatin1String("--ant"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.antTool = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--deployment"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size()) {
                options.helpRequested = true;
            } else {
                QString deploymentMechanism = arguments.at(++i);
                if (deploymentMechanism.compare(QLatin1String("ministro"), Qt::CaseInsensitive) == 0) {
                    options.deploymentMechanism = Options::Ministro;
                } else if (deploymentMechanism.compare(QLatin1String("debug"), Qt::CaseInsensitive) == 0) {
                    options.deploymentMechanism = Options::Debug;

                    QString temporaryDirectoryName = QStandardPaths::standardLocations(QStandardPaths::TempLocation).at(0)
                            + QLatin1String("/android-build-")
                            + QUuid::createUuid().toString();
                    if (QFileInfo(temporaryDirectoryName).exists()) {
                        fprintf(stderr, "Temporary directory '%s' already exists. Bailing out.\n",
                                qPrintable(temporaryDirectoryName));
                        options.helpRequested = true;
                    } else {
                        options.temporaryDirectoryName = temporaryDirectoryName;
                    }

                } else if (deploymentMechanism.compare(QLatin1String("bundled"), Qt::CaseInsensitive) == 0) {
                    options.deploymentMechanism = Options::Bundled;
                } else {
                    fprintf(stderr, "Unrecognized deployment mechanism: %s\n", qPrintable(deploymentMechanism));
                    options.helpRequested = true;
                }
            }
        } else if (argument.compare(QLatin1String("--device"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.installLocation = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--release"), Qt::CaseInsensitive) == 0) {
            options.releasePackage = true;
        } else if (argument.compare(QLatin1String("--jdk"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.jdkPath = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--sign"), Qt::CaseInsensitive) == 0) {
            if (i + 2 >= arguments.size()) {
                options.helpRequested = true;
            } else {
                options.releasePackage = true;
                options.keyStore = arguments.at(++i);
                options.keyStoreAlias = arguments.at(++i);
            }
        } else if (argument.compare(QLatin1String("--storepass"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.keyStorePassword = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--storetype"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.storeType = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--keypass"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.keyPass = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--sigfile"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.sigFile = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--digestalg"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.digestAlg = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--sigalg"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.sigAlg = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--tsa"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.tsaUrl = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--tsacert"), Qt::CaseInsensitive) == 0) {
            if (i + 1 == arguments.size())
                options.helpRequested = true;
            else
                options.tsaCert = arguments.at(++i);
        } else if (argument.compare(QLatin1String("--internalsf"), Qt::CaseInsensitive) == 0) {
            options.internalSf = true;
        } else if (argument.compare(QLatin1String("--sectionsonly"), Qt::CaseInsensitive) == 0) {
            options.sectionsOnly = true;
        } else if (argument.compare(QLatin1String("--protected"), Qt::CaseInsensitive) == 0) {
            options.protectedAuthenticationPath = true;
        }
    }

    if (options.inputFileName.isEmpty())
        options.inputFileName = QString::fromLatin1("android-lib%1.so-deployment-settings.json").arg(QDir::current().dirName());

    return options;
}

void printHelp()
{//                 "012345678901234567890123456789012345678901234567890123456789012345678901"
    fprintf(stderr, "Syntax: %s --output <destination> [options]\n"
                    "\n"
                    "  Creates an Android package in the build directory <destination> and\n"
                    "  builds it into an .apk file.\n\n"
                    "  Optional arguments:\n"
                    "    --input <inputfile>: Reads <inputfile> for options generated by\n"
                    "       qmake. A default file name based on the current working\n"
                    "       directory will be used if nothing else is specified.\n"
                    "    --deployment <mechanism>: Supported deployment mechanisms:\n"
                    "       bundled (default): Include Qt files in stand-alone package.\n"
                    "       ministro: Use the Ministro service to manage Qt files.\n"
                    "       debug: Copy Qt files to device for quick debugging.\n"
                    "    --install: Installs apk to device/emulator. By default this step is\n"
                    "       not taken.\n"
                    "    --device [device ID]: Use specified device for deployment. Default\n"
                    "       is the device selected by default by adb.\n"
                    "    --android-platform <platform>: Builds against the given android\n"
                    "       platform. By default, the highest available version will be\n"
                    "       used.\n"
                    "    --ant <path/to/ant>: If unspecified, ant from the PATH will be\n"
                    "       used."
                    "    --release: Builds a package ready for release. By default, the\n"
                    "       package will be signed with a debug key.\n"
                    "    --sign <url/to/keystore> <alias>: Signs the package with the\n"
                    "       specified keystore, alias and store password. Also implies the\n"
                    "       --release option.\n"
                    "       Optional arguments for use with signing:\n"
                    "         --storepass <password>: Keystore password.\n"
                    "         --storetype <type>: Keystore type.\n"
                    "         --keypass <password>: Password for private key (if different\n"
                    "           from keystore password.)\n"
                    "         --sigfile <file>: Name of .SF/.DSA file.\n"
                    "         --digestalg <name>: Name of digest algorithm. Default is\n"
                    "           \"SHA1\".\n"
                    "         --sigalg <name>: Name of signature algorithm. Default is\n"
                    "           \"SHA1withRSA\".\n"
                    "         --tsa <url>: Location of the Time Stamping Authority.\n"
                    "         --tsacert <alias>: Public key certificate for TSA.\n"
                    "         --internalsf: Include the .SF file inside the signature block.\n"
                    "         --sectionsonly: Don't compute hash of entire manifest.\n"
                    "         --protected: Keystore has protected authentication path.\n"
                    "    --jdk <path/to/jdk>: Used to find the jarsigner tool when used\n"
                    "       in combination with the --release argument. By default,\n"
                    "       an attempt is made to detect the tool using the JAVA_HOME and\n"
                    "       PATH environment variables, in that order.\n"
                    "    --verbose: Prints out information during processing.\n"
                    "    --help: Displays this information.\n\n",
                    qPrintable(QCoreApplication::arguments().at(0))
            );
}

// Since strings compared will all start with the same letters,
// sorting by length and then alphabetically within each length
// gives the natural order.
bool quasiLexicographicalReverseLessThan(const QFileInfo &fi1, const QFileInfo &fi2)
{
    QString s1 = fi1.baseName();
    QString s2 = fi2.baseName();

    if (s1.length() == s2.length())
        return s1 > s2;
    else
        return s1.length() > s2.length();
}

// Files which contain templates that need to be overwritten by build data should be overwritten every
// time.
bool alwaysOverwritableFile(const QString &fileName)
{
    return (fileName.endsWith(QLatin1String("/res/values/libs.xml"))
            || fileName.endsWith(QLatin1String("/AndroidManifest.xml"))
            || fileName.endsWith(QLatin1String("/res/values/strings.xml"))
            || fileName.endsWith(QLatin1String("/src/org/qtproject/qt5/android/bindings/QtActivity.java")));
}

bool copyFileIfNewer(const QString &sourceFileName, const QString &destinationFileName, bool verbose)
{
    if (QFile::exists(destinationFileName)) {
        QFileInfo destinationFileInfo(destinationFileName);
        QFileInfo sourceFileInfo(sourceFileName);

        if (sourceFileInfo.lastModified() <= destinationFileInfo.lastModified() && !alwaysOverwritableFile(destinationFileName)) {
            if (verbose)
                fprintf(stdout, "  -- Skipping file %s. Same or newer file already in place.\n", qPrintable(sourceFileName));
            return true;
        } else {
            if (!QFile(destinationFileName).remove()) {
                fprintf(stderr, "Can't remove old file: %s\n", qPrintable(destinationFileName));
                return false;
            }
        }
    }

    if (!QDir().mkpath(QFileInfo(destinationFileName).path())) {
        fprintf(stderr, "Cannot make output directory for %s.\n", qPrintable(destinationFileName));
        return false;
    }

    if (!QFile::exists(destinationFileName) && !QFile::copy(sourceFileName, destinationFileName)) {
        fprintf(stderr, "Failed to copy %s to %s.\n", qPrintable(sourceFileName), qPrintable(destinationFileName));
        return false;
    } else if (verbose) {
        fprintf(stdout, "  -- Copied %s\n", qPrintable(destinationFileName));
    }

    return true;
}

QString detectLatestAndroidPlatform(const QString &sdkPath)
{
    QDir dir(sdkPath + QLatin1String("/platforms"));
    if (!dir.exists()) {
        fprintf(stderr, "Directory %s does not exist\n", qPrintable(dir.absolutePath()));
        return QString();
    }

    QFileInfoList fileInfos = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
    if (fileInfos.isEmpty()) {
        fprintf(stderr, "No platforms found in %s", qPrintable(dir.absolutePath()));
        return QString();
    }

    std::sort(fileInfos.begin(), fileInfos.end(), quasiLexicographicalReverseLessThan);

    QFileInfo latestPlatform = fileInfos.first();
    return latestPlatform.baseName();
}

bool readInputFile(Options *options)
{
    QFile file(options->inputFileName);
    if (!file.open(QIODevice::ReadOnly)) {
        fprintf(stderr, "Cannot read from input file: %s\n", qPrintable(options->inputFileName));
        return false;
    }

    QJsonDocument jsonDocument = QJsonDocument::fromJson(file.readAll());
    if (jsonDocument.isNull()) {
        fprintf(stderr, "Invalid json file: %s\n", qPrintable(options->inputFileName));
        return false;
    }

    QJsonObject jsonObject = jsonDocument.object();

    {
        QJsonValue sdkPath = jsonObject.value(QLatin1String("sdk"));
        if (sdkPath.isUndefined()) {
            fprintf(stderr, "No SDK path in json file %s\n", qPrintable(options->inputFileName));
            return false;
        }

        options->sdkPath = sdkPath.toString();

        if (options->androidPlatform.isEmpty()) {
            options->androidPlatform = detectLatestAndroidPlatform(options->sdkPath);
            if (options->androidPlatform.isEmpty())
                return false;
        } else {
            if (!QDir(options->sdkPath + QLatin1String("/platforms/") + options->androidPlatform).exists()) {
                fprintf(stderr, "Warning: Android platform '%s' does not exist in NDK.\n",
                        qPrintable(options->androidPlatform));
            }
        }
    }

    {
        QJsonValue qtInstallDirectory = jsonObject.value("qt");
        if (qtInstallDirectory.isUndefined()) {
            fprintf(stderr, "No Qt directory in json file %s\n", qPrintable(options->inputFileName));
            return false;
        }
        options->qtInstallDirectory = qtInstallDirectory.toString();
    }

    {
        QJsonValue androidSourcesDirectory = jsonObject.value("android-package-source-directory");
        if (!androidSourcesDirectory.isUndefined())
            options->androidSourceDirectory = androidSourcesDirectory.toString();
    }

    {
        QJsonValue applicationBinary = jsonObject.value("application-binary");
        if (applicationBinary.isUndefined()) {
            fprintf(stderr, "No application binary defined in json file.\n");
            return false;
        }
        options->applicationBinary = applicationBinary.toString();

        if (!QFile::exists(options->applicationBinary)) {
            fprintf(stderr, "Cannot find application binary %s.\n", qPrintable(options->applicationBinary));
            return false;
        }
    }

    {
        QJsonValue deploymentDependencies = jsonObject.value("deployment-dependencies");
        if (!deploymentDependencies.isUndefined())
            options->qtDependencies = deploymentDependencies.toString().split(QLatin1Char(','));
    }


    {
        QJsonValue targetArchitecture = jsonObject.value("target-architecture");
        if (targetArchitecture.isUndefined()) {
            fprintf(stderr, "No target architecture defined in json file.\n");
            return false;
        }
        options->architecture = targetArchitecture.toString();
    }

    {
        QJsonValue ndk = jsonObject.value("ndk");
        if (ndk.isUndefined()) {
            fprintf(stderr, "No NDK path defined in json file.\n");
            return false;
        }
        options->ndkPath = ndk.toString();
    }

    {
        QJsonValue toolchainPrefix = jsonObject.value("toolchain-prefix");
        if (toolchainPrefix.isUndefined()) {
            fprintf(stderr, "No toolchain prefix defined in json file.\n");
            return false;
        }
        options->toolchainPrefix = toolchainPrefix.toString();
    }

    {
        QJsonValue toolPrefix = jsonObject.value("tool-prefix");
        if (toolPrefix.isUndefined()) {
            fprintf(stderr, "Warning: No tool prefix defined in json file.\n");
            options->toolPrefix = options->toolchainPrefix;
        } else {
            options->toolPrefix = toolPrefix.toString();
        }
    }

    {
        QJsonValue toolchainVersion = jsonObject.value("toolchain-version");
        if (toolchainVersion.isUndefined()) {
            fprintf(stderr, "No toolchain version defined in json file.\n");
            return false;
        }
        options->toolchainVersion = toolchainVersion.toString();
    }

    {
        QJsonValue ndkHost = jsonObject.value("ndk-host");
        if (ndkHost.isUndefined()) {
            fprintf(stderr, "No NDK host defined in json file.\n");
            return false;
        }
        options->ndkHost = ndkHost.toString();
    }

    {
        QJsonValue androidMinimumVersion = jsonObject.value("android-minimum-version");
        if (!androidMinimumVersion.isUndefined()) {
            options->minimumAndroidVersion = androidMinimumVersion.toInt();
            if (options->minimumAndroidVersion < 9)
                fprintf(stderr, "Warning: Unsupported minimum version");
        }
    }

    {
        QJsonValue androidTargetVersion = jsonObject.value("android-target-version");
        if (!androidTargetVersion.isUndefined()) {
            options->targetAndroidVersion = androidTargetVersion.toInt();
            if (options->targetAndroidVersion < 9)
                fprintf(stderr, "Warning: Unsupported target version");
        }
    }

    if (options->minimumAndroidVersion < 0) {
        options->minimumAndroidVersion = 9;
        if (options->targetAndroidVersion < 0)
            options->targetAndroidVersion = 14;
    }

    {
        QJsonValue androidAppName = jsonObject.value("android-app-name");
        if (!androidAppName.isUndefined())
            options->appName = androidAppName.toString();
        else
            options->appName = QFileInfo(options->applicationBinary).baseName().mid(sizeof("lib") - 1);
    }


    {
        QJsonValue androidPackage = jsonObject.value("android-package");
        if (!androidPackage.isUndefined())
            options->packageName = androidPackage.toString();
        else
            options->packageName = QString::fromLatin1("org.qtproject.example.%1").arg(options->appName);
    }

    {
        QJsonValue supportedOrientations = jsonObject.value("supported-orientations");
        if (!supportedOrientations.isUndefined())
            options->supportedOrientations = supportedOrientations.toString().split(QLatin1Char(','));
    }

    {
        QJsonValue extraLibs = jsonObject.value("android-extra-libs");
        if (!extraLibs.isUndefined())
            options->extraLibs = extraLibs.toString().split(QLatin1Char(','));
    }

    return true;
}

bool copyFiles(const QDir &sourceDirectory, const QDir &destinationDirectory, bool verbose)
{
    QFileInfoList entries = sourceDirectory.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
    foreach (QFileInfo entry, entries) {
        if (entry.isDir()) {
            QDir dir(entry.absoluteFilePath());
            if (!destinationDirectory.mkpath(dir.dirName())) {
                fprintf(stderr, "Cannot make directory %s in %s\n", qPrintable(dir.dirName()), qPrintable(destinationDirectory.path()));
                return false;
            }

            if (!copyFiles(dir, QDir(destinationDirectory.path() + QLatin1String("/") + dir.dirName()), verbose))
                return false;
        } else {
            QString destination = destinationDirectory.absoluteFilePath(entry.fileName());
            if (!copyFileIfNewer(entry.absoluteFilePath(), destination, verbose))
                return false;
        }
    }

    return true;
}

bool copyAndroidTemplate(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "Copying Android package template.\n");

    QDir sourceDirectory(options.qtInstallDirectory + QLatin1String("/src/android/java"));
    if (!sourceDirectory.exists()) {
        fprintf(stderr, "Cannot find template directory %s\n", qPrintable(sourceDirectory.absolutePath()));
        return false;
    }

    if (!QDir::current().mkpath(options.outputDirectory)) {
        fprintf(stderr, "Cannot create output directory %s\n", qPrintable(options.outputDirectory));
        return false;
    }

    return copyFiles(sourceDirectory, QDir(options.outputDirectory), options.verbose);
}

bool copyAndroidSources(const Options &options)
{
    if (options.androidSourceDirectory.isEmpty())
        return true;

    if (options.verbose)
        fprintf(stdout, "Copying Android sources from project.\n");

    QDir sourceDirectory(options.androidSourceDirectory);
    if (!sourceDirectory.exists()) {
        fprintf(stderr, "Cannot find android sources in %s", qPrintable(options.androidSourceDirectory));
        return false;
    }

    return copyFiles(sourceDirectory, QDir(options.outputDirectory), options.verbose);
}

bool copyAndroidExtraLibs(const Options &options)
{
    if (options.extraLibs.isEmpty())
        return true;

    if (options.verbose)
        fprintf(stdout, "Copying %d external libraries to package.\n", options.extraLibs.size());

    foreach (QString extraLib, options.extraLibs) {
        QFileInfo extraLibInfo(extraLib);
        if (!extraLibInfo.exists()) {
            fprintf(stderr, "External library %s does not exist!\n", qPrintable(extraLib));
            return false;
        }

        if (!extraLibInfo.fileName().startsWith(QLatin1String("lib")) || extraLibInfo.suffix() != QLatin1String("so")) {
            fprintf(stderr, "The file name of external library %s must begin with \"lib\" and end with the suffix \".so\".\n",
                    qPrintable(extraLib));
            return false;
        }

        QString destinationFile(options.outputDirectory
                                + QLatin1String("/libs/")
                                + options.architecture
                                + QLatin1Char('/')
                                + extraLibInfo.fileName());

        if (!copyFileIfNewer(extraLib, destinationFile, options.verbose))
            return false;
    }

    return true;
}

bool updateFile(const QString &fileName, const QHash<QString, QString> &replacements)
{
    QFile inputFile(fileName);
    if (!inputFile.open(QIODevice::ReadOnly)) {
        fprintf(stderr, "Cannot open %s for reading.\n", qPrintable(fileName));
        return false;
    }

    // All the files we are doing substitutes in are quite small. If this
    // ever changes, this code should be updated to be more conservative.
    QByteArray contents = inputFile.readAll();

    bool hasReplacements = false;
    QHash<QString, QString>::const_iterator it;
    for (it = replacements.constBegin(); it != replacements.constEnd(); ++it) {
        int index = contents.indexOf(it.key().toUtf8());
        if (index >= 0) {
            contents.replace(index, it.key().length(), it.value().toUtf8());
            hasReplacements = true;
        }
    }

    if (hasReplacements) {
        inputFile.close();

        if (!inputFile.open(QIODevice::WriteOnly)) {
            fprintf(stderr, "Cannot open %s for writing.\n", qPrintable(fileName));
            return false;
        }

        inputFile.write(contents);
    }

    return true;

}

bool updateLibsXml(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "  -- res/values/libs.xml\n");

    QString fileName = options.outputDirectory + QLatin1String("/res/values/libs.xml");
    if (!QFile::exists(fileName)) {
        fprintf(stderr, "Cannot find %s in prepared packaged. This file is required.\n", qPrintable(fileName));
        return false;
    }

    QString libsPath = QLatin1String("libs/") + options.architecture + QLatin1Char('/');

    QString qtLibs = QLatin1String("<item>gnustl_shared</item>\n");
    QString bundledInLibs;
    QString bundledInAssets;
    foreach (Options::BundledFile bundledFile, options.bundledFiles) {
        if (bundledFile.second.startsWith(QLatin1String("lib/"))) {
            QString s = bundledFile.second.mid(sizeof("lib/lib") - 1);
            s.chop(sizeof(".so") - 1);
            qtLibs += QString::fromLatin1("<item>%1</item>\n").arg(s);
        } else if (bundledFile.first.startsWith(libsPath)) {
            QString s = bundledFile.first.mid(libsPath.length());
            bundledInLibs += QString::fromLatin1("<item>%1:%2</item>\n")
                    .arg(s).arg(bundledFile.second);
        } else if (bundledFile.first.startsWith(QLatin1String("assets/"))) {
            QString s = bundledFile.first.mid(sizeof("assets/") - 1);
            bundledInAssets += QString::fromLatin1("<item>%1:%2</item>\n")
                    .arg(s).arg(bundledFile.second);
        }
    }

    QHash<QString, QString> replacements;
    replacements[QLatin1String("<!-- %%INSERT_QT_LIBS%% -->")] = qtLibs;
    if (options.deploymentMechanism == Options::Bundled) {
        replacements[QLatin1String("<!-- %%INSERT_BUNDLED_IN_LIB%% -->")] = bundledInLibs;
        replacements[QLatin1String("<!-- %%INSERT_BUNDLED_IN_ASSETS%% -->")] = bundledInAssets;
    }

    QString extraLibs;
    if (!options.extraLibs.isEmpty()) {
        foreach (QString extraLib, options.extraLibs) {
            QFileInfo extraLibInfo(extraLib);
            QString name = extraLibInfo.fileName().mid(sizeof("lib") - 1);
            name.chop(sizeof(".so") - 1);

            extraLibs += QLatin1String("<item>") + name + QLatin1String("</item>\n");
        }
    }
    replacements[QLatin1String("<!-- %%INSERT_EXTRA_LIBS%% -->")] = extraLibs;

    if (!updateFile(fileName, replacements))
        return false;

    return true;
}

bool updateAndroidManifest(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "  -- AndroidManifest.xml \n");

    QString usesSdk = QString("<uses-sdk android:minSdkVersion=\"%1\" %2 />").arg(options.minimumAndroidVersion);
    if (options.targetAndroidVersion >= 0)
        usesSdk = usesSdk.arg(QString::fromLatin1("android:targetSdkVersion=\"%1\"").arg(options.targetAndroidVersion));

    QString androidOrientation;
    if (options.supportedOrientations.size() == 1) {
        const QString &orientation = options.supportedOrientations.at(0);
        if (orientation == QLatin1String("Portrait"))
            androidOrientation = QLatin1String("portrait");
        else if (orientation == QLatin1String("Landscape"))
            androidOrientation = QLatin1String("landscape");
        else if (orientation == QLatin1String("InvertedLandscape"))
            androidOrientation = QLatin1String("reverseLandscape");
        else if (orientation == QLatin1String("InvertedPortrait"))
            androidOrientation = QLatin1String("reversePortrait");
    } else if (options.supportedOrientations.size() > 1) {
        fprintf(stderr, "Warning: Multiple supported orientations specified. Orientation of Android app will be unspecified.");
    }

    QHash<QString, QString> replacements;
    replacements[QLatin1String("<!-- %%INSERT_USES_SDK%% -->")] = usesSdk;
    replacements[QLatin1String("package=\"org.qtproject.example\"")] = QString::fromLatin1("package=\"%1\"").arg(options.packageName);

    if (options.deploymentMechanism == Options::Ministro) {
        replacements[QLatin1String("<meta-data android:value=\"1\" android:name=\"android.app.use_local_qt_libs\"/>")]
                = QString::fromLatin1("<meta-data android:value=\"0\" android:name=\"android.app.use_local_qt_libs\"/>");
    }

    if (options.deploymentMechanism != Options::Bundled) {
        replacements[QLatin1String("<meta-data android:value=\"1\" android:name=\"android.app.bundle_local_qt_libs\"/>")]
                = QString::fromLatin1("<meta-data android:value=\"0\" android:name=\"android.app.bundle_local_qt_libs\"/>");
    }

    if (!androidOrientation.isEmpty())
        replacements[QLatin1String("android:screenOrientation=\"unspecified\"")] = QString::fromLatin1("android:screenOrientation=\"%1\"").arg(androidOrientation);
    if (!updateFile(options.outputDirectory + QLatin1String("/AndroidManifest.xml"), replacements))
        return false;

    return true;
}

bool updateStringsXml(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "  -- res/values/strings.xml\n");

    QStringList localLibs = options.localLibs;

    // If .pro file overrides dependency detection, we need to see which platform plugin they picked
    if (localLibs.isEmpty()) {
        QString plugin;
        foreach (QString qtDependency, options.qtDependencies) {
            if (qtDependency.endsWith(QLatin1String("libqtforandroid.so"))
                    || qtDependency.endsWith(QLatin1String("libqtforandroidGL.so"))) {
                if (!plugin.isEmpty() && plugin != qtDependency) {
                    fprintf(stderr, "Both platform plugins libqtforandroid.so and libqtforandroidGL.so included in package. Please include only one.\n");
                    return false;
                }

                plugin = qtDependency;
            }
        }

        if (plugin.isEmpty()) {
            fprintf(stderr, "No platform plugin, neither libqtforandroid.so or libqtforandroidGL.so, included in package. Please include one.\n");
            return false;
        }

        localLibs.append(plugin);
        if (options.verbose)
            fprintf(stdout, "  -- Using platform plugin %s\n", qPrintable(plugin));
    }

    QHash<QString, QString> replacements;
    replacements[QLatin1String("<!-- %%INSERT_APP_NAME%% -->")] = options.appName;
    replacements[QLatin1String("<!-- %%INSERT_APP_LIB_NAME%% -->")] = QFileInfo(options.applicationBinary).baseName().mid(sizeof("lib") - 1);
    replacements[QLatin1String("<!-- %%INSERT_LOCAL_LIBS%% -->")] = localLibs.join(QLatin1Char(':'));
    replacements[QLatin1String("<!-- %%INSERT_LOCAL_JARS%% -->")] = options.localJars.join(QLatin1Char(':'));
    replacements[QLatin1String("<!-- %%INSERT_INIT_CLASSES%% -->")] = options.initClasses.join(QLatin1Char(':'));

    QString fileName = options.outputDirectory + QLatin1String("/res/values/strings.xml");
    if (!QFile::exists(fileName)) {
        if (options.verbose)
            fprintf(stdout, "  -- Skipping update of strings.xml since it's missing.\n");
        return true;
    }

    if (!updateFile(fileName, replacements))
        return false;

    return true;
}

bool updateJavaFiles(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "  -- /src/org/qtproject/qt5/android/bindings/QtActivity.java\n");

    QString fileName(options.outputDirectory + QLatin1String("/src/org/qtproject/qt5/android/bindings/QtActivity.java"));
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly)) {
        fprintf(stderr, "Cannot open %s.\n", qPrintable(fileName));
        return false;
    }

    QByteArray contents;

    while (!file.atEnd()) {
        QByteArray line = file.readLine();
        if (line.startsWith("//@ANDROID-")) {
            bool ok;
            int requiredSdkVersion = line.mid(sizeof("//@ANDROID-") - 1).trimmed().toInt(&ok);
            if (requiredSdkVersion <= options.minimumAndroidVersion)
                contents += line;

            if (ok) {
                while (!file.atEnd()) {
                    line = file.readLine();
                    if (requiredSdkVersion <= options.minimumAndroidVersion)
                        contents += line;

                    if (line.startsWith(QByteArray("//@ANDROID-") + QByteArray::number(requiredSdkVersion)))
                        break;
                }

                if (!line.startsWith(QByteArray("//@ANDROID-") + QByteArray::number(requiredSdkVersion))) {
                    fprintf(stderr, "Mismatched tag ANDROID-%d in %s\n", requiredSdkVersion, qPrintable(fileName));
                    return false;
                }
            } else {
                fprintf(stderr, "Warning: Misformatted tag in %s: %s\n", qPrintable(fileName), line.constData());
            }
        } else {
            contents += line;
        }
    }

    file.close();

    if (!file.open(QIODevice::WriteOnly)) {
        fprintf(stderr, "Can't open %s for writing.\n", qPrintable(fileName));
        return false;
    }

    if (file.write(contents) < contents.size()) {
        fprintf(stderr, "Failed to write contents to %s.\n", qPrintable(fileName));
        return false;
    }

    return true;
}

bool updateAndroidFiles(const Options &options)
{
    if (options.verbose)
        fprintf(stdout, "Updating Android package files with project settings.\n");

    if (!updateLibsXml(options))
        return false;

    if (!updateAndroidManifest(options))
        return false;

    if (!updateStringsXml(options))
        return false;

    if (!updateJavaFiles(options))
        return false;

    return true;
}

QStringList findFilesRecursively(const Options &options, const QString &fileName)
{
    QFileInfo info(options.qtInstallDirectory + QLatin1Char('/') + fileName);
    if (!info.exists()) {
        fprintf(stderr, "Warning: Dependency not found: %s\n", qPrintable(info.filePath()));
        return QStringList();
    }

    if (info.isDir()) {
        QStringList ret;

        QDir dir(info.filePath());
        QStringList entries = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);

        foreach (QString entry, entries) {
            QString s = fileName + QLatin1Char('/') + entry;
            ret += findFilesRecursively(options, s);
        }

        return ret;
    } else {
        return QStringList() << fileName;
    }
}

bool readAndroidDependencyXml(Options *options,
                              const QString &moduleName,
                              QSet<QString> *usedDependencies,
                              QSet<QString> *remainingDependencies)
{
    QString androidDependencyName = options->qtInstallDirectory + QString::fromLatin1("/lib/%1-android-dependencies.xml").arg(moduleName);

    QFile androidDependencyFile(androidDependencyName);
    if (androidDependencyFile.exists()) {
        if (options->verbose)
            fprintf(stdout, "Reading Android dependencies for %s\n", qPrintable(moduleName));

        if (!androidDependencyFile.open(QIODevice::ReadOnly)) {
            fprintf(stderr, "Cannot open %s for reading.\n", qPrintable(androidDependencyName));
            return false;
        }

        QXmlStreamReader reader(&androidDependencyFile);
        while (!reader.atEnd()) {
            reader.readNext();

            if (reader.isStartElement()) {
                if (reader.name() == QLatin1String("bundled")) {
                    if (!reader.attributes().hasAttribute(QLatin1String("file"))) {
                        fprintf(stderr, "Invalid android dependency file: %s\n", qPrintable(androidDependencyName));
                        return false;
                    }

                    QString file = reader.attributes().value(QLatin1String("file")).toString();
                    QStringList fileNames = findFilesRecursively(*options, file);
                    foreach (QString fileName, fileNames) {
                        if (usedDependencies->contains(fileName))
                            continue;

                        usedDependencies->insert(fileName);
                        if (fileName.endsWith(QLatin1String(".so"))) {
                            remainingDependencies->insert(fileName);
                        }

                        options->qtDependencies.append(fileName);
                    }
                } else if (reader.name() == QLatin1String("jar")) {
                    int bundling = reader.attributes().value(QLatin1String("bundling")).toInt();
                    QString fileName = reader.attributes().value(QLatin1String("file")).toString();
                    if (bundling == (options->deploymentMechanism == Options::Bundled)) {
                        if (!usedDependencies->contains(fileName)) {
                            options->qtDependencies.append(fileName);
                            usedDependencies->insert(fileName);
                        }
                    }

                    if (!fileName.isEmpty())
                        options->localJars.append(fileName);

                    if (reader.attributes().hasAttribute(QLatin1String("initClass"))) {
                        options->initClasses.append(reader.attributes().value(QLatin1String("initClass")).toString());
                    }
                } else if (reader.name() == QLatin1String("lib")) {
                    QString fileName = reader.attributes().value(QLatin1String("file")).toString();
                    if (reader.attributes().hasAttribute(QLatin1String("replaces"))) {
                        QString replaces = reader.attributes().value(QLatin1String("replaces")).toString();
                        for (int i=0; i<options->localLibs.size(); ++i) {
                            if (options->localLibs.at(i) == replaces) {
                                options->localLibs[i] = fileName;
                                break;
                            }
                        }
                    } else if (!fileName.isEmpty()) {
                        options->localLibs.append(fileName);
                    }
                }
            }
        }

        if (reader.hasError()) {
            fprintf(stderr, "Error in %s: %s\n", qPrintable(androidDependencyName), qPrintable(reader.errorString()));
            return false;
        }
    } else if (options->verbose) {
        fprintf(stdout, "No android dependencies for %s\n", qPrintable(moduleName));
    }

    return true;
}

QStringList getQtLibsFromElf(const Options &options, const QString &fileName)
{
    QString readElf = options.ndkPath
            + QLatin1String("/toolchains/")
            + options.toolchainPrefix
            + QLatin1Char('-')
            + options.toolchainVersion
            + QLatin1String("/prebuilt/")
            + options.ndkHost
            + QLatin1String("/bin/")
            + options.toolPrefix
            + QLatin1String("-readelf");
#if defined(Q_OS_WIN32)
    readElf += QLatin1String(".exe");
#endif

    if (!QFile::exists(readElf)) {
        fprintf(stderr, "Command does not exist: %s\n", qPrintable(readElf));
        return QStringList();
    }

    readElf = QString::fromLatin1("\"%1\" -d -W %2").arg(readElf).arg(fileName);

    FILE *readElfCommand = popen(readElf.toLocal8Bit().constData(), "r");
    if (readElfCommand == 0) {
        fprintf(stderr, "Cannot execute command %s", qPrintable(readElf));
        return QStringList();
    }

    QStringList ret;

    char buffer[512];
    while (fgets(buffer, sizeof(buffer), readElfCommand) != 0) {
        QByteArray line = QByteArray::fromRawData(buffer, qstrlen(buffer));
        if (line.contains("(NEEDED)") && line.contains("Shared library:") ) {
            const int pos = line.lastIndexOf('[') + 1;
            QString libraryName = QLatin1String("lib/") + QString::fromLatin1(line.mid(pos, line.length() - pos - 2));
            if (QFile::exists(options.qtInstallDirectory + QLatin1Char('/') + libraryName)) {
                ret += libraryName;
            }

        }
    }

    pclose(readElfCommand);

    return ret;
}

bool readDependenciesFromElf(Options *options,
                             const QString &fileName,
                             QSet<QString> *usedDependencies,
                             QSet<QString> *remainingDependencies)
{
    // Get dependencies on libraries in $QTDIR/lib
    QStringList dependencies = getQtLibsFromElf(*options, fileName);

    if (options->verbose) {
        fprintf(stdout, "Reading dependencies from %s\n", qPrintable(fileName));
        foreach (QString dep, dependencies)
            fprintf(stdout, "      %s\n", qPrintable(dep));
    }
    // Recursively add dependencies from ELF and supplementary XML information
    foreach (QString dependency, dependencies) {
        if (usedDependencies->contains(dependency))
            continue;

        usedDependencies->insert(dependency);
        if (!readDependenciesFromElf(options,
                              options->qtInstallDirectory + QLatin1Char('/') + dependency,
                              usedDependencies,
                              remainingDependencies)) {
            return false;
        }

        options->qtDependencies.append(dependency);
        if (options->verbose)
            fprintf(stderr, "Appending dependency: %s\n", qPrintable(dependency));
        QString qtBaseName = dependency.mid(sizeof("lib/lib") - 1);
        qtBaseName = qtBaseName.left(qtBaseName.size() - (sizeof(".so") - 1));
        if (!readAndroidDependencyXml(options, qtBaseName, usedDependencies, remainingDependencies)) {
            return false;
        }
    }
    return true;
}

bool readDependencies(Options *options)
{
    if (options->verbose)
        fprintf(stdout, "Detecting dependencies of application.\n");

    // Override set in .pro file
{'fraction_non_alphanumeric': 0.12504693954187007, 'fraction_numerical': 0.006863604122334877, 'mean_word_length': 2.342246548598522, 'pattern_counts': {'":': 0, '<': 167, '<?xml version=': 0, '>': 289, 'https://': 0, 'lorem ipsum': 0, 'www.': 6, 'xml': 23}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3282117', 'n_tokens_mistral': 26449, 'n_tokens_neox': 25460, 'n_words': 5666}