From 93c443abc87dc2e25a8b42da95579aab05575413 Mon Sep 17 00:00:00 2001
From: Ryan Huffman <ryanhuffman@gmail.com>
Date: Fri, 13 Sep 2019 13:57:26 -0700
Subject: [PATCH] Add qt launcher states, download, and install

---
 launchers/qt/resources/qml/Download.qml       |  20 +-
 .../resources/qml/HFControls/HFTextField.qml  |   1 +
 launchers/qt/resources/qml/root.qml           |  11 +
 launchers/qt/src/Launcher.cpp                 |   2 +-
 launchers/qt/src/LauncherState.cpp            | 368 ++++++++++++++++--
 launchers/qt/src/LauncherState.h              |  87 +++--
 launchers/qt/src/main.cpp                     |   2 +-
 7 files changed, 428 insertions(+), 63 deletions(-)

diff --git a/launchers/qt/resources/qml/Download.qml b/launchers/qt/resources/qml/Download.qml
index 8020889e8a..c323d3b505 100644
--- a/launchers/qt/resources/qml/Download.qml
+++ b/launchers/qt/resources/qml/Download.qml
@@ -62,6 +62,8 @@ Item {
         width: 394
         height: 8
 
+        value: LauncherState.downloadProgress;
+
         anchors {
             top: secondText.bottom
             topMargin: 30
@@ -88,14 +90,14 @@ Item {
         }
 
 
-        PropertyAnimation {
-            target: progressBar;
-            loops: Animation.Infinite
-            property: "value"
-            from: 0;
-            to: 1;
-            duration: 5000
-            running: true
-        }
+        //PropertyAnimation {
+            //target: progressBar;
+            //loops: Animation.Infinite
+            //property: "value"
+            //from: 0;
+            //to: 1;
+            //duration: 5000
+            //running: true
+        //}
     }
 }
diff --git a/launchers/qt/resources/qml/HFControls/HFTextField.qml b/launchers/qt/resources/qml/HFControls/HFTextField.qml
index 77a6601fdf..547cd94843 100644
--- a/launchers/qt/resources/qml/HFControls/HFTextField.qml
+++ b/launchers/qt/resources/qml/HFControls/HFTextField.qml
@@ -10,6 +10,7 @@ TextField {
     horizontalAlignment: TextInput.AlignLeft
     placeholderText: "PlaceHolder"
     property string seperatorColor: "#FFFFFF"
+    selectByMouse: true
     background: Item {
         anchors.fill: parent
         Rectangle {
diff --git a/launchers/qt/resources/qml/root.qml b/launchers/qt/resources/qml/root.qml
index 0ceda5b189..e80466a6eb 100644
--- a/launchers/qt/resources/qml/root.qml
+++ b/launchers/qt/resources/qml/root.qml
@@ -4,6 +4,7 @@ import QtQuick 2.3
 import QtQuick.Controls 2.1
 import HQLauncher 1.0
 import "HFControls"
+
 Image {
     id: root
     width: 515
@@ -21,4 +22,14 @@ Image {
             loader.source = url;
         });
     }
+
+    Text {
+        font.pixelSize: 12
+
+        anchors.right: root.right
+        anchors.bottom: root.bottom
+
+        color: "#FFFFFF"
+        text: LauncherState.uiState.toString() + " - " + LauncherState.applicationState
+    }
 }
diff --git a/launchers/qt/src/Launcher.cpp b/launchers/qt/src/Launcher.cpp
index 20405d5a39..23467624f8 100644
--- a/launchers/qt/src/Launcher.cpp
+++ b/launchers/qt/src/Launcher.cpp
@@ -11,7 +11,7 @@ Launcher::Launcher(int& argc, char**argv) : QGuiApplication(argc, argv) {
     QString resourceBinaryLocation =  QGuiApplication::applicationDirPath() + "/resources.rcc";
     QResource::registerResource(resourceBinaryLocation);
     _launcherState = std::make_shared<LauncherState>();
-    _launcherState->setUIState(LauncherState::SPLASH_SCREEN);
+    //_launcherState->setUIState(LauncherState::SPLASH_SCREEN);
     _launcherWindow = std::make_unique<LauncherWindow>();
     _launcherWindow->rootContext()->setContextProperty("LauncherState", _launcherState.get());
     _launcherWindow->setFlags(Qt::FramelessWindowHint);
diff --git a/launchers/qt/src/LauncherState.cpp b/launchers/qt/src/LauncherState.cpp
index 9c5f66f9cf..d1635c277c 100644
--- a/launchers/qt/src/LauncherState.cpp
+++ b/launchers/qt/src/LauncherState.cpp
@@ -1,5 +1,9 @@
 #include "LauncherState.h"
 
+#include "Unzipper.h"
+
+#include <Windows.h>
+
 #include <array>
 
 #include <QNetworkRequest>
@@ -13,18 +17,56 @@
 #include <QDebug>
 #include <QQmlEngine>
 
+#include <QThreadPool>
+
+#include <QStandardPaths>
+
+
+bool LatestBuilds::getBuild(QString tag, Build* outBuild) {
+    if (tag.isNull()) {
+        tag = defaultTag;
+    }
+
+    for (auto& build : builds) {
+        if (build.tag == tag) {
+            *outBuild = build;
+            return true;
+        }
+    }
+
+    return false;
+}
+
 static const std::array<QString, LauncherState::UIState::UI_STATE_NUM> QML_FILE_FOR_UI_STATE =
     { { "qrc:/qml/SplashScreen.qml", "qrc:/qml/Login.qml", "qrc:/qml/DisplayName.qml",
         "qrc:/qml/Download.qml", "qrc:/qml/DownloadFinshed.qml", "qrc:/qml/Error.qml" } };
 
-void LauncherState::ASSERT_STATE(LauncherState::ApplicationState state) const {
-    if (_appState != state) {
+void LauncherState::ASSERT_STATE(LauncherState::ApplicationState state) {
+    if (_applicationState != state) {
+#ifdef Q_OS_WIN
         __debugbreak();
-        exit(0);
+#endif
+        setApplicationState(ApplicationState::UnexpectedError);
     }
 }
 
+void LauncherState::ASSERT_STATE(std::vector<LauncherState::ApplicationState> states) {
+    for (auto state : states) {
+        if (_applicationState == state) {
+            return;
+        }
+    }
+#ifdef Q_OS_WIN
+    __debugbreak();
+#endif
+    setApplicationState(ApplicationState::UnexpectedError);
+}
+
 LauncherState::LauncherState() {
+    _launcherDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
+    // TODO Fix launcher directory
+    qDebug() << "Launcher directory: " << _launcherDirectory.absolutePath();
+    _launcherDirectory.mkpath(_launcherDirectory.absolutePath());
     requestBuilds();
 }
 
@@ -36,13 +78,29 @@ void LauncherState::declareQML() {
     qmlRegisterType<LauncherState>("HQLauncher", 1, 0, "LauncherStateEnums");
 }
 
-void LauncherState::setUIState(UIState state) {
-    _uiState = state;
-    emit updateSourceUrl(getCurrentUISource());
-}
-
 LauncherState::UIState LauncherState::getUIState() const {
-    return _uiState;
+    switch (_applicationState) {
+        case ApplicationState::Init:
+        case ApplicationState::RequestingBuilds:
+            return SPLASH_SCREEN;
+        case ApplicationState::WaitingForLogin:
+        case ApplicationState::RequestingLogin:
+            return LOGIN_SCREEN;
+        case ApplicationState::DownloadingClient:
+        case ApplicationState::InstallingClient:
+        case ApplicationState::DownloadingContentCache:
+        case ApplicationState::InstallingContentCache:
+            return DOWNLOAD_SCREEN;
+        case ApplicationState::LaunchingHighFidelity:
+            return DOWNLOAD_FINSISHED;
+        case ApplicationState::UnexpectedError:
+            __debugbreak();
+            return ERROR_SCREEN;
+        default:
+            qDebug() << "FATAL: No UI for" << _applicationState;
+            __debugbreak();
+            return ERROR_SCREEN;
+    }
 }
 
 void LauncherState::setLastLoginError(LastLoginError lastLoginError) {
@@ -54,8 +112,8 @@ LauncherState::LastLoginError LauncherState::getLastLoginError() const {
 }
 
 void LauncherState::requestBuilds() {
-    ASSERT_STATE(ApplicationState::INIT);
-    _appState = ApplicationState::REQUESTING_BUILDS;
+    ASSERT_STATE(ApplicationState::Init);
+    setApplicationState(ApplicationState::RequestingBuilds);
 
     // TODO Show splash screen until this request is complete
     auto request = new QNetworkRequest(QUrl("https://thunder.highfidelity.com/builds/api/tags/latest/?format=json"));
@@ -67,7 +125,7 @@ void LauncherState::requestBuilds() {
 void LauncherState::receivedBuildsReply() {
     auto reply = static_cast<QNetworkReply*>(sender());
 
-    ASSERT_STATE(ApplicationState::REQUESTING_BUILDS);
+    ASSERT_STATE(ApplicationState::RequestingBuilds);
 
     if (reply->error()) {
         qDebug() << "Error getting builds from thunder: " << reply->errorString();
@@ -81,8 +139,7 @@ void LauncherState::receivedBuildsReply() {
         } else {
             auto root = doc.object();
             if (!root.contains("default_tag")) {
-                _appState = ApplicationState::REQUESTING_BUILDS_FAILED;
-                setUIState(LauncherState::ERROR_SCREEN);
+                setApplicationState(ApplicationState::UnexpectedError);
                 return;
             }
 
@@ -90,8 +147,7 @@ void LauncherState::receivedBuildsReply() {
 
             auto results = root["results"];
             if (!results.isArray()) {
-                _appState = ApplicationState::REQUESTING_BUILDS_FAILED;
-                setUIState(LauncherState::ERROR_SCREEN);
+                setApplicationState(ApplicationState::UnexpectedError);
                 return;
             }
 
@@ -112,14 +168,13 @@ void LauncherState::receivedBuildsReply() {
             }
         }
     }
-    _appState = ApplicationState::WAITING_FOR_LOGIN;
-    setUIState(LauncherState::LOGIN_SCREEN);
+    setApplicationState(ApplicationState::WaitingForLogin);
 }
 
 void LauncherState::login(QString username, QString password) {
-    ASSERT_STATE(ApplicationState::WAITING_FOR_LOGIN);
+    ASSERT_STATE(ApplicationState::WaitingForLogin);
 
-    _appState = ApplicationState::REQUESTING_LOGIN;
+    setApplicationState(ApplicationState::RequestingLogin);
 
     qDebug() << "Got login: " << username << password;
 
@@ -133,32 +188,287 @@ void LauncherState::login(QString username, QString password) {
     query.addQueryItem("scope", "owner");
 
     auto reply = _networkAccessManager.post(*request, query.toString().toUtf8());
-
     QObject::connect(reply, &QNetworkReply::finished, this, &LauncherState::receivedLoginReply);
 }
 
 Q_INVOKABLE void LauncherState::receivedLoginReply() {
+    ASSERT_STATE(ApplicationState::RequestingLogin);
+
+    // TODO Check for errors
     auto reply = static_cast<QNetworkReply*>(sender());
 
-    ASSERT_STATE(ApplicationState::REQUESTING_LOGIN);
+    if (reply->error()) {
+        setApplicationState(ApplicationState::UnexpectedError);
+        return;
+    }
 
-    qDebug() << "Got response for login: " << reply->readAll();
+    auto data = reply->readAll();
+    QJsonParseError parseError;
+    auto doc = QJsonDocument::fromJson(data, &parseError);
+    auto root = doc.object();
 
+    if (!root.contains("access_token")
+        || !root.contains("token_type")
+        || !root.contains("expires_in")
+        || !root.contains("refresh_token")
+        || !root.contains("scope")
+        || !root.contains("created_at")) {
 
-    download();
+        setApplicationState(ApplicationState::UnexpectedError);
+        return;
+    }
+
+    _loginResponse.accessToken = root["access_token"].toString();
+    _loginResponse.refreshToken = root["refresh_token"].toString();
+    _loginResponse.tokenType = root["token_type"].toString();
+
+    qDebug() << "Got response for login: " << data;
+
+    downloadClient();
 }
 
-void LauncherState::download() {
-    _appState = ApplicationState::DOWNLOADING_CONTENT;
-    setUIState(LauncherState::DOWNLOAD_SCREEN);
+QString LauncherState::getContentCachePath() const {
+    return _launcherDirectory.filePath("cache");
 }
 
-void LauncherState::contentDownloadComplete() {
+bool LauncherState::shouldDownloadContentCache() const {
+    return !_contentCacheURL.isNull() && !QFile::exists(getContentCachePath());
+}
+
+void LauncherState::downloadClient() {
+    ASSERT_STATE(ApplicationState::RequestingLogin);
+
+    Build build;
+    if (!_latestBuilds.getBuild(_buildTag, &build)) {
+        qDebug() << "Cannot determine latest build";
+        setApplicationState(ApplicationState::UnexpectedError);
+        return;
+    }
+
+    _downloadProgress = 0;
+    setApplicationState(ApplicationState::DownloadingClient);
+
+    // Start client download
+    {
+        qDebug() << "Latest build: " << build.tag << build.buildNumber << build.latestVersion << build.installerZipURL;
+        auto request = new QNetworkRequest(QUrl(build.installerZipURL));
+        auto reply = _networkAccessManager.get(*request);
+
+        _clientZipFile.setFileName(_launcherDirectory.absoluteFilePath("client.zip"));
+
+        qDebug() << "Opening " << _clientZipFile.fileName();
+        if (!_clientZipFile.open(QIODevice::WriteOnly)) {
+            setApplicationState(ApplicationState::UnexpectedError);
+            return;
+        }
+
+        connect(reply, &QNetworkReply::finished, this, &LauncherState::clientDownloadComplete);
+        connect(reply, &QNetworkReply::readyRead, this, [this, reply]() {
+            char buf[4096];
+            while (reply->bytesAvailable() > 0) {
+                qint64 size;
+                size = reply->read(buf, (qint64)sizeof(buf));
+                if (size == 0) {
+                    break;
+                }
+                _clientZipFile.write(buf, size);
+            }
+        });
+        connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64 total) {
+            _downloadProgress = (float)received / (float)total;
+            emit downloadProgressChanged();
+        });
+    }
 }
 
 void LauncherState::clientDownloadComplete() {
+    ASSERT_STATE(ApplicationState::DownloadingClient);
+
+    _clientZipFile.close();
+
+    installClient();
+}
+
+void LauncherState::installClient() {
+    ASSERT_STATE(ApplicationState::DownloadingClient);
+    setApplicationState(ApplicationState::InstallingClient);
+
+    auto installDir = _launcherDirectory.absoluteFilePath("interface_install");
+    _launcherDirectory.mkpath("interface_install");
+
+    _downloadProgress = 0;
+
+    qDebug() << "Unzipping " << _clientZipFile.fileName() << " to " << installDir;
+
+    auto unzipper = new Unzipper(_clientZipFile.fileName(), QDir(installDir));
+    unzipper->setAutoDelete(true);
+    connect(unzipper, &Unzipper::progress, this, [this](float progress) {
+        qDebug() << "Unzipper progress: " << progress;
+        _downloadProgress = progress;
+        emit downloadProgressChanged();
+    });
+    connect(unzipper, &Unzipper::finished, this, [this](bool error, QString errorMessage) {
+        if (error) {
+            qDebug() << "Unzipper finished with error: " << errorMessage;
+            setApplicationState(ApplicationState::UnexpectedError);
+        } else {
+            qDebug() << "Unzipper finished without error";
+            downloadContentCache();
+        }
+    });
+    QThreadPool::globalInstance()->start(unzipper);
+
+    //launchClient();
+}
+
+void LauncherState::downloadContentCache() {
+    ASSERT_STATE(ApplicationState::InstallingClient);
+
+    // Start content set cache download
+    if (shouldDownloadContentCache()) {
+        setApplicationState(ApplicationState::DownloadingContentCache);
+
+        _downloadProgress = 0;
+
+        auto request = new QNetworkRequest(QUrl(_contentCacheURL));
+        auto reply = _networkAccessManager.get(*request);
+
+        _contentZipFile.setFileName(_launcherDirectory.absoluteFilePath("content_cache.zip"));
+
+        qDebug() << "Opening " << _contentZipFile.fileName();
+        if (!_contentZipFile.open(QIODevice::WriteOnly)) {
+            setApplicationState(ApplicationState::UnexpectedError);
+            return;
+        }
+
+        connect(reply, &QNetworkReply::finished, this, &LauncherState::contentCacheDownloadComplete);
+        connect(reply, &QNetworkReply::readyRead, this, [this, reply]() {
+            char buf[4096];
+            while (reply->bytesAvailable() > 0) {
+                qint64 size;
+                size = reply->read(buf, (qint64)sizeof(buf));
+                if (size == 0) {
+                    break;
+                }
+                _contentZipFile.write(buf, size);
+            }
+        });
+        connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64 total) {
+            _downloadProgress = (float)received / (float)total;
+            emit downloadProgressChanged();
+        });
+    } else {
+        launchClient();
+    }
+}
+
+void LauncherState::contentCacheDownloadComplete() {
+    ASSERT_STATE(ApplicationState::DownloadingContentCache);
+
+    _contentZipFile.close();
+
+    installContentCache();
+}
+
+
+void LauncherState::installContentCache() {
+    ASSERT_STATE(ApplicationState::DownloadingContentCache);
+    setApplicationState(ApplicationState::InstallingContentCache);
+
+    auto installDir = getContentCachePath();
+
+    qDebug() << "Unzipping " << _contentZipFile.fileName() << " to " << installDir;
+
+    _downloadProgress = 0;
+
+    auto unzipper = new Unzipper(_contentZipFile.fileName(), QDir(installDir));
+    unzipper->setAutoDelete(true);
+    connect(unzipper, &Unzipper::progress, this, [this](float progress) {
+        qDebug() << "Unzipper progress (content cache): " << progress;
+        _downloadProgress = progress;
+        emit downloadProgressChanged();
+    });
+    connect(unzipper, &Unzipper::finished, this, [this](bool error, QString errorMessage) {
+        if (error) {
+            qDebug() << "Unzipper finished with error: " << errorMessage;
+            setApplicationState(ApplicationState::UnexpectedError);
+        } else {
+            qDebug() << "Unzipper finished without error";
+            launchClient();
+        }
+    });
+    QThreadPool::globalInstance()->start(unzipper);
+
 }
 
 void LauncherState::launchClient() {
-    _appState = ApplicationState::LAUNCHING_HIGH_FIDELITY;
+    ASSERT_STATE({ ApplicationState::InstallingClient, ApplicationState::InstallingContentCache });
+
+    setApplicationState(ApplicationState::LaunchingHighFidelity);
+
+    QDir installDirectory = _launcherDirectory.filePath("interface_install");
+    auto clientPath = installDirectory.absoluteFilePath("interface.exe");
+
+    QString homePath = "hifi://hq";
+    QString defaultScriptsPath = installDirectory.filePath("scripts/simplifiedUIBootstrapper");
+    QString displayName = "fixMe";
+    QString contentCachePath = _launcherDirectory.filePath("cache");
+
+    // TODO Fix parameters
+    QString params = "--url " + homePath
+        + " --setBookmark hqhome=\"" + homePath + "\""
+        + " --defaultScriptsOverride " + QDir::toNativeSeparators(defaultScriptsPath)
+        + " --displayName " + displayName
+        + " --cache " + contentCachePath;
+
+#if defined(Q_OS_WIN)
+    STARTUPINFO si;
+    PROCESS_INFORMATION pi;
+
+    // set the size of the structures
+    ZeroMemory(&si, sizeof(si));
+    si.cb = sizeof(si);
+    ZeroMemory(&pi, sizeof(pi));
+
+    // start the program up
+    BOOL success = CreateProcess(
+        clientPath.toUtf8().data(),
+        params.toUtf8().data(),
+        nullptr,                   // Process handle not inheritable
+        nullptr,                   // Thread handle not inheritable
+        FALSE,                  // Set handle inheritance to FALSE
+        CREATE_NEW_CONSOLE,     // Opens file in a separate console
+        nullptr,           // Use parent's environment block
+        nullptr,           // Use parent's starting directory 
+        &si,            // Pointer to STARTUPINFO structure
+        &pi           // Pointer to PROCESS_INFORMATION structure
+    );
+    // Close process and thread handles. 
+    CloseHandle(pi.hProcess);
+    CloseHandle(pi.hThread);
+    exit(0);
+#elif defined(Q_OS_MACOS)
+    // TODO Implement launching of client
+#else
+#error UNSUPPORTED PLATFORM
+#endif
+}
+
+void LauncherState::setApplicationState(ApplicationState state) {
+    qDebug() << "Changing application state: " << _applicationState << " -> " << state;
+
+    if (state == ApplicationState::UnexpectedError) {
+        __debugbreak();
+    }
+
+    _applicationState = state;
+
+    emit uiStateChanged();
+    emit updateSourceUrl(getCurrentUISource());
+
+    emit applicationStateChanged();
+}
+
+LauncherState::ApplicationState LauncherState::getApplicationState() const {
+    return _applicationState;
 }
diff --git a/launchers/qt/src/LauncherState.h b/launchers/qt/src/LauncherState.h
index 62a58c9778..156daaa165 100644
--- a/launchers/qt/src/LauncherState.h
+++ b/launchers/qt/src/LauncherState.h
@@ -1,6 +1,10 @@
+#pragma once
+
+#include <QDir>
 #include <QObject>
 #include <QString>
 #include <QNetworkAccessManager>
+#include <QFile>
 
 struct Build {
     QString tag;
@@ -10,12 +14,24 @@ struct Build {
 };
 
 struct LatestBuilds {
+    bool getBuild(QString tag, Build* outBuild);
+
     QString defaultTag;
     std::vector<Build> builds;
 };
 
+struct LoginResponse {
+    QString accessToken;
+    QString tokenType;
+    QString refreshToken;
+};
+
 class LauncherState : public QObject {
     Q_OBJECT
+    Q_PROPERTY(UIState uiState READ getUIState NOTIFY uiStateChanged);
+    Q_PROPERTY(ApplicationState applicationState READ getApplicationState NOTIFY applicationStateChanged);
+
+    Q_PROPERTY(float downloadProgress READ getDownloadProgress NOTIFY downloadProgressChanged);
 
 public:
     LauncherState();
@@ -30,29 +46,27 @@ public:
         ERROR_SCREEN,
         UI_STATE_NUM
     };
-    Q_ENUMS(UIState);
+    Q_ENUM(UIState);
 
     enum class ApplicationState {
-        INIT,
+        Init,
 
-        REQUESTING_BUILDS,
-        REQUESTING_BUILDS_FAILED,
+        UnexpectedError,
 
-        WAITING_FOR_LOGIN,
-        REQUESTING_LOGIN,
+        RequestingBuilds,
 
-        WAITING_FOR_SIGNUP,
-        REQUESTING_SIGNUP,
+        WaitingForLogin,
+        RequestingLogin,
 
-        DOWNLOADING_CONTENT,
-        DOWNLOADING_HIGH_FIDELITY,
+        DownloadingClient,
+        DownloadingContentCache,
 
-        EXTRACTING_DATA,
+        InstallingClient,
+        InstallingContentCache,
 
-        LAUNCHING_HIGH_FIDELITY
+        LaunchingHighFidelity
     };
-    Q_ENUMS(ApplicationState);
-
+    Q_ENUM(ApplicationState);
 
     enum LastLoginError {
         NONE = 0,
@@ -60,19 +74,23 @@ public:
         CREDENTIALS,
         LAST_ERROR_NUM
     };
-    Q_ENUMS(LastLoginError);
+    Q_ENUM(LastLoginError);
+
     Q_INVOKABLE QString getCurrentUISource() const;
 
-    void LauncherState::ASSERT_STATE(LauncherState::ApplicationState state) const;
+    void ASSERT_STATE(LauncherState::ApplicationState state);
+    void ASSERT_STATE(std::vector<LauncherState::ApplicationState> states);
 
     static void declareQML();
 
-    void setUIState(UIState state);
     UIState getUIState() const;
 
     void setLastLoginError(LastLoginError lastLoginError);
     LastLoginError getLastLoginError() const;
 
+    void setApplicationState(ApplicationState state);
+    ApplicationState getApplicationState() const;
+
     // Request builds
     void requestBuilds();
     Q_INVOKABLE void receivedBuildsReply();
@@ -81,22 +99,45 @@ public:
     Q_INVOKABLE void login(QString username, QString password);
     Q_INVOKABLE void receivedLoginReply();
 
-    // Download
-    void download();
-    Q_INVOKABLE void contentDownloadComplete();
-    Q_INVOKABLE void clientDownloadComplete();
+    // Client
+    void downloadClient();
+    void installClient();
+
+    // Content Cache
+    void downloadContentCache();
+    void installContentCache();
 
     // Launching
     void launchClient();
 
+    Q_INVOKABLE float getDownloadProgress() const { return _downloadProgress; }
+
 signals:
     void updateSourceUrl(QString sourceUrl);
+    void uiStateChanged();
+    void applicationStateChanged();
+    void downloadProgressChanged();
+
+private slots:
+    void clientDownloadComplete();
+    void contentCacheDownloadComplete();
 
 private:
+    bool shouldDownloadContentCache() const;
+    QString getContentCachePath() const;
+
     QNetworkAccessManager _networkAccessManager;
     LatestBuilds _latestBuilds;
+    QDir _launcherDirectory;
 
-    ApplicationState _appState { ApplicationState::INIT };
-    UIState _uiState { SPLASH_SCREEN };
+    // Application State
+    ApplicationState _applicationState { ApplicationState::Init };
+    LoginResponse _loginResponse;
     LastLoginError _lastLoginError { NONE };
+    QString _buildTag { QString::null };
+    QString _contentCacheURL{ "https://orgs.highfidelity.com/content-cache/content_cache_small-only_data8.zip" }; // QString::null }; // If null, there is no content cache to download
+    QFile _clientZipFile;
+    QFile _contentZipFile;
+
+    float _downloadProgress { 0 };
 };
diff --git a/launchers/qt/src/main.cpp b/launchers/qt/src/main.cpp
index 9ed6f42390..93a0c0e7dc 100644
--- a/launchers/qt/src/main.cpp
+++ b/launchers/qt/src/main.cpp
@@ -10,7 +10,7 @@ Q_IMPORT_PLUGIN(QtQuickControls2Plugin);
 Q_IMPORT_PLUGIN(QtQuickTemplates2Plugin);
 
 int main(int argc, char *argv[]) {
-    QString name { "HQLauncher" };
+    QString name { "High Fidelity" };
     QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
     QCoreApplication::setOrganizationName(name);