From 8e640722c62692818ab840d50b3758f89a41a54e Mon Sep 17 00:00:00 2001 From: Unit 193 Date: Wed, 25 Nov 2015 16:48:41 -0500 Subject: Imported Upstream version 3.0.7 --- Plugins/DbAndroid/DbAndroid.pro | 53 +++ Plugins/DbAndroid/adbmanager.cpp | 426 +++++++++++++++++ Plugins/DbAndroid/adbmanager.h | 65 +++ Plugins/DbAndroid/dbandroid.cpp | 209 ++++++++ Plugins/DbAndroid/dbandroid.h | 63 +++ Plugins/DbAndroid/dbandroid.json | 9 + Plugins/DbAndroid/dbandroid.qrc | 1 + Plugins/DbAndroid/dbandroid_global.h | 12 + Plugins/DbAndroid/dbandroidconnection.cpp | 14 + Plugins/DbAndroid/dbandroidconnection.h | 44 ++ Plugins/DbAndroid/dbandroidconnectionfactory.cpp | 28 ++ Plugins/DbAndroid/dbandroidconnectionfactory.h | 21 + Plugins/DbAndroid/dbandroidinstance.cpp | 143 ++++++ Plugins/DbAndroid/dbandroidinstance.h | 51 ++ Plugins/DbAndroid/dbandroidjsonconnection.cpp | 430 +++++++++++++++++ Plugins/DbAndroid/dbandroidjsonconnection.h | 65 +++ Plugins/DbAndroid/dbandroidmode.h | 13 + Plugins/DbAndroid/dbandroidpathdialog.cpp | 579 +++++++++++++++++++++++ Plugins/DbAndroid/dbandroidpathdialog.h | 80 ++++ Plugins/DbAndroid/dbandroidpathdialog.ui | 244 ++++++++++ Plugins/DbAndroid/dbandroidshellconnection.cpp | 363 ++++++++++++++ Plugins/DbAndroid/dbandroidshellconnection.h | 59 +++ Plugins/DbAndroid/dbandroidurl.cpp | 217 +++++++++ Plugins/DbAndroid/dbandroidurl.h | 59 +++ Plugins/DbAndroid/sqlqueryandroid.cpp | 160 +++++++ Plugins/DbAndroid/sqlqueryandroid.h | 47 ++ Plugins/DbAndroid/sqlresultrowandroid.cpp | 12 + Plugins/DbAndroid/sqlresultrowandroid.h | 13 + 28 files changed, 3480 insertions(+) create mode 100644 Plugins/DbAndroid/DbAndroid.pro create mode 100644 Plugins/DbAndroid/adbmanager.cpp create mode 100644 Plugins/DbAndroid/adbmanager.h create mode 100644 Plugins/DbAndroid/dbandroid.cpp create mode 100644 Plugins/DbAndroid/dbandroid.h create mode 100644 Plugins/DbAndroid/dbandroid.json create mode 100644 Plugins/DbAndroid/dbandroid.qrc create mode 100644 Plugins/DbAndroid/dbandroid_global.h create mode 100644 Plugins/DbAndroid/dbandroidconnection.cpp create mode 100644 Plugins/DbAndroid/dbandroidconnection.h create mode 100644 Plugins/DbAndroid/dbandroidconnectionfactory.cpp create mode 100644 Plugins/DbAndroid/dbandroidconnectionfactory.h create mode 100644 Plugins/DbAndroid/dbandroidinstance.cpp create mode 100644 Plugins/DbAndroid/dbandroidinstance.h create mode 100644 Plugins/DbAndroid/dbandroidjsonconnection.cpp create mode 100644 Plugins/DbAndroid/dbandroidjsonconnection.h create mode 100644 Plugins/DbAndroid/dbandroidmode.h create mode 100644 Plugins/DbAndroid/dbandroidpathdialog.cpp create mode 100644 Plugins/DbAndroid/dbandroidpathdialog.h create mode 100644 Plugins/DbAndroid/dbandroidpathdialog.ui create mode 100644 Plugins/DbAndroid/dbandroidshellconnection.cpp create mode 100644 Plugins/DbAndroid/dbandroidshellconnection.h create mode 100644 Plugins/DbAndroid/dbandroidurl.cpp create mode 100644 Plugins/DbAndroid/dbandroidurl.h create mode 100644 Plugins/DbAndroid/sqlqueryandroid.cpp create mode 100644 Plugins/DbAndroid/sqlqueryandroid.h create mode 100644 Plugins/DbAndroid/sqlresultrowandroid.cpp create mode 100644 Plugins/DbAndroid/sqlresultrowandroid.h (limited to 'Plugins/DbAndroid') diff --git a/Plugins/DbAndroid/DbAndroid.pro b/Plugins/DbAndroid/DbAndroid.pro new file mode 100644 index 0000000..2895c7f --- /dev/null +++ b/Plugins/DbAndroid/DbAndroid.pro @@ -0,0 +1,53 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2015-01-04T19:37:23 +# +#------------------------------------------------- + +QT += widgets network + +include($$PWD/../../../sqlitestudio/SQLiteStudio3/plugins.pri) + +TARGET = DbAndroid +TEMPLATE = lib + +DEFINES += DBANDROID_LIBRARY + +SOURCES += dbandroid.cpp \ + dbandroidinstance.cpp \ + sqlqueryandroid.cpp \ + dbandroidurl.cpp \ + dbandroidpathdialog.cpp \ + adbmanager.cpp \ + sqlresultrowandroid.cpp \ + dbandroidjsonconnection.cpp \ + dbandroidshellconnection.cpp \ + dbandroidconnection.cpp \ + dbandroidconnectionfactory.cpp + +HEADERS += dbandroid.h\ + dbandroid_global.h \ + dbandroidinstance.h \ + sqlqueryandroid.h \ + dbandroidurl.h \ + dbandroidpathdialog.h \ + adbmanager.h \ + dbandroidconnection.h \ + dbandroidmode.h \ + sqlresultrowandroid.h \ + dbandroidjsonconnection.h \ + dbandroidshellconnection.h \ + dbandroidconnectionfactory.h + +win32: { + LIBS += -lcoreSQLiteStudio -lguiSQLiteStudio +} + +DISTFILES += \ + dbandroid.json + +FORMS += \ + dbandroidpathdialog.ui + +RESOURCES += \ + dbandroid.qrc diff --git a/Plugins/DbAndroid/adbmanager.cpp b/Plugins/DbAndroid/adbmanager.cpp new file mode 100644 index 0000000..ed012a1 --- /dev/null +++ b/Plugins/DbAndroid/adbmanager.cpp @@ -0,0 +1,426 @@ +#include "adbmanager.h" +#include "dbandroid.h" +#include "common/utils.h" +#include +#include +#include +#include + +AdbManager::AdbManager(DbAndroid* dbAndroidPlugin) : + QObject(dbAndroidPlugin), plugin(dbAndroidPlugin) +{ + connect(this, SIGNAL(internalDeviceListUpdate(QStringList)), this, SLOT(handleNewDeviceList(QStringList))); + connect(this, SIGNAL(deviceDetailsChanged(QList)), this, SLOT(handleNewDetails(QList))); + + adbRunMonitor = new QTimer(this); + connect(adbRunMonitor, SIGNAL(timeout()), this, SLOT(updateDeviceList())); + adbRunMonitor->setSingleShot(false); + adbRunMonitor->setInterval(1000); + adbRunMonitor->start(); + updateDeviceList(); +} + +AdbManager::~AdbManager() +{ + adbRunMonitor->stop(); + updateDevicesFuture.waitForFinished(); +} + +const QStringList& AdbManager::getDevices(bool forceSyncUpdate) +{ + if (forceSyncUpdate) + syncDeviceListUpdate(); + + return currentDeviceList; +} + +AdbManager::Device AdbManager::getDetails(const QString& deviceId) +{ + if (!currentDeviceDetails.contains(deviceId)) + { + AdbManager::Device device; + device.id = deviceId; + return device; + } + + return currentDeviceDetails[deviceId]; +} + +QList AdbManager::getDeviceDetails() +{ + return currentDeviceDetails.values(); +} + +QHash> AdbManager::getForwards() +{ + QHash> forwards; + QString stdOut; + if (!exec(QStringList({"forward", "--list"}), &stdOut)) + return forwards; + + QRegularExpression re("(.*)\\s+tcp:(\\d+)\\s+tcp:(\\d+)"); + QRegularExpressionMatch match; + QPair forward; + QStringList lines = stdOut.split("\n"); + for (const QString& line : lines) + { + match = re.match(line); + if (!match.hasMatch()) + continue; + + forward.first = match.captured(2).toInt(); + forward.second = match.captured(3).toInt(); + forwards[match.captured(1)] = forward; + } + + return forwards; +} + +int AdbManager::makeForwardFor(const QString& device, int targetPort) +{ + static_qstring(portTpl, "tcp:%1"); + + QHash> forwards = getForwards(); + if (forwards.contains(device) && forwards[device].second == targetPort) + return forwards[device].first; + + int localPort = targetPort; + QStringList args = QStringList({"-s", device, "forward"}); + args << portTpl.arg(localPort); + args << portTpl.arg(targetPort); + + int tryCount = 0; + QString stdOut; + bool res; + while (!(res = exec(args, &stdOut)) && tryCount++ < 3) + { + localPort = rand(1025, 65000); + args.replace(3, portTpl.arg(localPort)); + } + + if (!res) + return -1; + + return localPort; +} + +QString AdbManager::findAdb() +{ + QStringList candidates; +#ifdef Q_OS_WIN32 + candidates << "adb.exe"; +#endif + +#ifdef Q_OS_MACX + candidates << (QDir::homePath() + "/Library/Android/sdk/platform-tools/adb"); +#endif + +#ifdef Q_OS_UNIX + candidates << "adb" << "./adb"; + + QProcess locate; + locate.start("locate", QStringList({"adb"})); + if (waitForProc(locate, true)) + { + QFileInfo fi; + QStringList locateLines = decode(locate.readAllStandardOutput()).split("\n"); + for (const QString& filePath : locateLines) + { + fi.setFile(filePath); + if (fi.fileName() != "adb" || !fi.isReadable() || !fi.isExecutable()) + continue; + + candidates << filePath; + } + } +#endif + +#ifdef Q_OS_WIN32 + if (testAdb("adb.exe", true)) + return "adb.exe"; + + static_qstring(winAdbPath, "/../Android/sdk/platform-tools/adb.exe"); + QString fullPath; + for (const QString& path : QStandardPaths::standardLocations(QStandardPaths::AppLocalDataLocation)) + { + fullPath = QDir::cleanPath(path + winAdbPath); + if (testAdb(fullPath, true)) + return fullPath; + } +#endif + + return QString(); +} + +bool AdbManager::testCurrentAdb() +{ + return testAdb(plugin->getCurrentAdb(), false); +} + +bool AdbManager::testAdb(const QString& adbPath, bool quiet) +{ + if (adbPath.isEmpty()) + return false; + + QProcess adbApp; + adbApp.start(adbPath, QStringList({"version"})); + if (!waitForProc(adbApp, quiet)) + return false; + + QString verStr = decode(adbApp.readAllStandardOutput()); + bool res = verStr.startsWith("Android Debug Bridge", Qt::CaseInsensitive); + if (!res && !quiet) + qWarning() << "Adb binary correct, but its version string is incorrect:" << verStr; + + return res; +} + +bool AdbManager::execBytes(const QStringList& arguments, QByteArray* stdOut, QByteArray* stdErr) +{ + if (!ensureAdbRunning()) + return false; + + QProcess proc; + if (arguments.join(" ").size() > 800) + { + if (!execLongCommand(arguments, proc, stdErr)) + return false; + } + else + { + proc.start(plugin->getCurrentAdb(), arguments); + if (!waitForProc(proc, false)) + return false; + } + + if (stdOut) + *stdOut = proc.readAllStandardOutput(); + + if (stdErr) + *stdErr = proc.readAllStandardError(); + + return true; +} + +bool AdbManager::waitForProc(QProcess& proc, bool quiet) +{ + if (!proc.waitForFinished(-1)) + { + if (!quiet) + qDebug() << "DbAndroid QProcess timed out."; + + return false; + } + + if (proc.exitStatus() == QProcess::CrashExit) + { + if (!quiet) + { + qDebug() << "DbAndroid QProcess finished by crashing."; + qDebug() << proc.readAllStandardOutput() << proc.readAllStandardError(); + } + + return false; + } + + if (proc.exitCode() != 0) + { + if (!quiet) + { + qDebug() << "DbAndroid QProcess finished with code:" << proc.exitCode(); + qDebug() << proc.readAllStandardOutput() << proc.readAllStandardError(); + } + + return false; + } + + return true; +} + +bool AdbManager::ensureAdbRunning() +{ + if (!plugin->isAdbValid()) + return false; + + QProcess adbApp; + adbApp.start(plugin->getCurrentAdb(), QStringList({"start-server"})); + if (!waitForProc(adbApp, false)) + return false; + + return true; +} + +bool AdbManager::exec(const QStringList& arguments, QString* stdOut, QString* stdErr) +{ + QByteArray* out = stdOut ? new QByteArray() : nullptr; + QByteArray* err = stdErr ? new QByteArray() : nullptr; + bool res = execBytes(arguments, out, err); + + if (stdOut) + { + *stdOut = decode(*out); + delete out; + } + + if (stdErr) + { + *stdErr = decode(*err); + delete err; + } + + return res; +} + +QByteArray AdbManager::encode(const QString& input) +{ + return input.toUtf8(); +} + +QString AdbManager::decode(const QByteArray& input) +{ + return QString::fromUtf8(input); +} + +bool AdbManager::execLongCommand(const QStringList& arguments, QProcess& proc, QByteArray* stdErr) +{ + // Take off initial arguments from ADB, store it to use with "push". + QStringList primaryArguments; + QStringList args = arguments; + while (args.first() != "shell") + primaryArguments << args.takeFirst(); + + args.removeFirst(); // remove the shell itself + + // Escape remaining arguments for the script + QString cmd = " '" + args.replaceInStrings("'", "'\''").join("' '") + "'"; + + // Now, the temporary file for the script + QTemporaryFile tmpFile("SQLiteStudio-XXXXXX.sh"); + if (!tmpFile.open()) + { + if (stdErr) + *stdErr = encode(QString("Could not create temporary file: %1").arg(tmpFile.fileName())); + + return false; + } + + tmpFile.write(cmd.toUtf8()); + tmpFile.close(); + + // Push the file + args = primaryArguments; + args << "push" << tmpFile.fileName() << "/data/local/tmp"; + proc.start(plugin->getCurrentAdb(), args); + if (!waitForProc(proc, false)) + return false; + + QString remoteFile = ("/data/local/tmp/" + QFileInfo(tmpFile.fileName()).fileName()); + + // Execute the file + args = primaryArguments; + args << "shell" << "sh" << remoteFile; + proc.start(plugin->getCurrentAdb(), args); + if (!waitForProc(proc, false)) + return false; + + // Delete the file from device + args = primaryArguments; + args << "shell" << "rm" << remoteFile; + QProcess localProc; + localProc.start(plugin->getCurrentAdb(), args); + if (!waitForProc(localProc, false)) + { + // Not a critical issue... + qWarning() << "Could not clean up execution script from the device: " << remoteFile << "\nDetails:\n" + << localProc.readAllStandardOutput() << "\n" << localProc.readAllStandardError(); + } + + return true; +} + +QStringList AdbManager::getDevicesInternal(bool emitSignal) +{ + QStringList devices; + QString stdOut; + if (!exec(QStringList({"devices"}), &stdOut)) + { + if (emitSignal) + emit internalDeviceListUpdate(devices); + + return devices; + } + + QRegularExpression re("(.*)\\s+device$"); + QRegularExpressionMatch match; + QStringList lines = stdOut.split("\n"); + for (const QString& line : lines) + { + match = re.match(line.trimmed()); + if (!match.hasMatch()) + continue; + + devices << match.captured(1).trimmed(); + } + + if (emitSignal) + emit internalDeviceListUpdate(devices); + + return devices; +} + +void AdbManager::syncDeviceListUpdate() +{ + currentDeviceList = getDevicesInternal(false); + updateDetails(currentDeviceList); +} + +void AdbManager::updateDetails(const QStringList& devices) +{ + QString stdOut; + QList detailList; + for (const QString& deviceId : devices) + { + Device deviceDetails; + deviceDetails.id = deviceId; + if (exec(QStringList({"-s", deviceId, "shell", "getprop", "ro.product.manufacturer"}), &stdOut)) + deviceDetails.fullName = stdOut.trimmed(); + else + qWarning() << "Could not read brand for device" << deviceId; + + if (exec(QStringList({"-s", deviceId, "shell", "getprop", "ro.product.model"}), &stdOut)) + deviceDetails.fullName += " " + stdOut.trimmed(); + else + qWarning() << "Could not read brand for device" << deviceId; + + deviceDetails.fullName = deviceDetails.fullName.trimmed(); + detailList << deviceDetails; + } + + emit deviceDetailsChanged(detailList); +} + +void AdbManager::updateDeviceList() +{ + if (!plugin->isAdbValid()) + return; + + updateDevicesFuture = QtConcurrent::run(this, &AdbManager::getDevicesInternal, true); +} + +void AdbManager::handleNewDeviceList(const QStringList& devices) +{ + if (currentDeviceList == devices) + return; + + currentDeviceList = devices; + QtConcurrent::run(this, &AdbManager::updateDetails, devices); + + emit deviceListChanged(devices); +} + +void AdbManager::handleNewDetails(const QList& devices) +{ + currentDeviceDetails.clear(); + for (Device device : devices) + currentDeviceDetails[device.id] = device; +} diff --git a/Plugins/DbAndroid/adbmanager.h b/Plugins/DbAndroid/adbmanager.h new file mode 100644 index 0000000..adeca90 --- /dev/null +++ b/Plugins/DbAndroid/adbmanager.h @@ -0,0 +1,65 @@ +#ifndef ADBMANAGER_H +#define ADBMANAGER_H + +#include +#include +#include + +class DbAndroid; +class QTimer; + +class AdbManager : public QObject +{ + Q_OBJECT + public: + struct Device + { + QString id; + QString fullName; + }; + + AdbManager(DbAndroid* plugin); + ~AdbManager(); + + const QStringList& getDevices(bool forceSyncUpdate = false); + Device getDetails(const QString& deviceId); + QList getDeviceDetails(); + QHash> getForwards(); + int makeForwardFor(const QString& device, int targetPort); + QString findAdb(); + bool testCurrentAdb(); + bool testAdb(const QString& adbPath, bool quiet = false); + bool execBytes(const QStringList& arguments, QByteArray* stdOut = nullptr, QByteArray* stdErr = nullptr); + bool exec(const QStringList& arguments, QString* stdOut = nullptr, QString* stdErr = nullptr); + + static QByteArray encode(const QString& input); + static QString decode(const QByteArray& input); + + private: + bool execLongCommand(const QStringList& arguments, QProcess& proc, QByteArray* stdErr); + bool waitForProc(QProcess& proc, bool quiet = false); + bool ensureAdbRunning(); + QStringList getDevicesInternal(bool emitSignal); + void syncDeviceListUpdate(); + void updateDetails(const QStringList& devices); + + DbAndroid* plugin; + QTimer* adbRunMonitor = nullptr; + QStringList currentDeviceList; + QHash currentDeviceDetails; + QFuture updateDevicesFuture; + + private slots: + void updateDeviceList(); + void handleNewDeviceList(const QStringList& devices); + void handleNewDetails(const QList& devices); + + signals: + void internalDeviceListUpdate(const QStringList& devices); + void deviceListChanged(const QStringList& devices); + void deviceDetailsChanged(const QList& details); +}; + +Q_DECLARE_METATYPE(QList) + +#endif // ADBMANAGER_H diff --git a/Plugins/DbAndroid/dbandroid.cpp b/Plugins/DbAndroid/dbandroid.cpp new file mode 100644 index 0000000..99150cd --- /dev/null +++ b/Plugins/DbAndroid/dbandroid.cpp @@ -0,0 +1,209 @@ +#include "adbmanager.h" +#include "dbandroid.h" +#include "dbandroidinstance.h" +#include "dbandroidpathdialog.h" +#include "mainwindow.h" +#include "services/notifymanager.h" +#include "uiconfig.h" +#include "statusfield.h" +#include "services/dbmanager.h" +#include "dbandroidconnectionfactory.h" +#include +#include +#include +#include +#include + +DbAndroid::DbAndroid() +{ +} + +QString DbAndroid::getLabel() const +{ + return "Android SQLite"; +} + +bool DbAndroid::checkIfDbServedByPlugin(Db* db) const +{ + return (db && dynamic_cast(db)); +} + +Db* DbAndroid::getInstance(const QString& name, const QString& path, const QHash& options, QString* errorMessage) +{ + DbAndroidUrl url(path); + if (!url.isValid()) + { + if (errorMessage) + *errorMessage = tr("Invalid or incomplete Android Database URL."); + + return nullptr; + } + + DbAndroidInstance* db = new DbAndroidInstance(this, name, path, options); + return db; +} + +QList DbAndroid::getOptionsList() const +{ + QList options; + + DbPluginOption customBrowseOpt; + customBrowseOpt.type = DbPluginOption::Type::CUSTOM_PATH_BROWSE; + customBrowseOpt.label = tr("Android database URL"); + customBrowseOpt.toolTip = tr("Select Android database"); + customBrowseOpt.customBrowseHandler = [this](const QString& initialPath) -> QString + { + DbAndroidPathDialog dialog(this, MAINWINDOW); + dialog.setUrl(initialPath); + if (!dialog.exec()) + return QString(); + + return dialog.getUrl().toUrlString(); + }; + options << customBrowseOpt; + + return options; +} + +QString DbAndroid::generateDbName(const QVariant& baseValue) +{ + // android://drgh:port/dbName + QUrl url(baseValue.toString()); + if (!url.isValid()) + return baseValue.toString(); + + return url.fileName(); +} + +bool DbAndroid::init() +{ + Q_INIT_RESOURCE(dbandroid); + + qRegisterMetaType>("QList"); + + connect(this, SIGNAL(adbReady(bool)), this, SLOT(handleValidAdb(bool))); + connect(this, SIGNAL(invalidAdb()), this, SLOT(handleInvalidAdb())); + connect(MAINWINDOW->getStatusField(), SIGNAL(linkActivated(QString)), this, SLOT(statusFieldLinkClicked(QString))); + + connectionFactory = new DbAndroidConnectionFactory(this); + + adbManager = new AdbManager(this); + connect(adbManager, SIGNAL(deviceListChanged(QStringList)), this, SLOT(deviceListChanged())); + + if (adbManager->testCurrentAdb()) + { + qDebug() << "Using ADB binary:" << cfg.DbAndroid.AdbPath.get(); + adbValid = true; + adbManager->getDevices(true); + } + else + { + QtConcurrent::run(this, &DbAndroid::initAdb); + } + return true; +} + +void DbAndroid::deinit() +{ + safe_delete(connectionFactory); + safe_delete(adbManager); + Q_CLEANUP_RESOURCE(dbandroid); +} + +QString DbAndroid::getCurrentAdb() +{ + return cfg.DbAndroid.AdbPath.get(); +} + +void DbAndroid::initAdb() +{ + QString adbPath = adbManager->findAdb(); + if (!adbPath.isEmpty()) + { + cfg.DbAndroid.AdbPath.set(adbPath); + qDebug() << "Found ADB binary:" << cfg.DbAndroid.AdbPath.get(); + emit adbReady(true); + return; + } + + emit invalidAdb(); +} + +QString DbAndroid::askForAdbPath() +{ +#if defined(Q_OS_UNIX) + QString adbAppName = "adb"; +#elif defined(Q_OS_WIN32) + QString adbAppName = "adb.exe"; +#else + qCritical() << "Unsupported OS for DbAndroid."; + return QString(); +#endif + QString file = QFileDialog::getOpenFileName(MAINWINDOW, tr("Select ADB"), getFileDialogInitPath(), QString("Android Debug Bridge (%1)").arg(adbAppName)); + if (file.isEmpty()) + return file; + + setFileDialogInitPathByFile(file); + return file; +} + +bool DbAndroid::isAdbValid() const +{ + return adbValid; +} + +DbAndroidConnectionFactory*DbAndroid::getConnectionFactory() const +{ + return connectionFactory; +} + +void DbAndroid::handleValidAdb(bool showMessage) +{ + adbValid = true; + if (showMessage) + notifyInfo(tr("Using Android Debug Bridge: %1").arg(cfg.DbAndroid.AdbPath.get())); + + DBLIST->rescanInvalidDatabasesForPlugin(this); +} + +void DbAndroid::handleInvalidAdb() +{ + notifyError(tr("Could not find Android Debug Bridge application. Click here to point out the location of the ADB application, " + "otherwise the %2 plugin will not support USB cable connections, only the network connection..").arg(SELECT_ADB_URL, getLabel())); +} + +void DbAndroid::statusFieldLinkClicked(const QString& link) +{ + if (link == SELECT_ADB_URL) + { + QString file = askForAdbPath(); + while (!file.isEmpty()) + { + if (adbManager->testAdb(file)) + { + cfg.DbAndroid.AdbPath.set(file); + emit adbReady(true); + return; + } + + int res = QMessageBox::warning(MAINWINDOW, tr("Invalid ADB"), tr("The selected ADB is incorrect.\n" + "Would you like to select another one, or leave it unconfigured?"), + tr("Select another ADB"), tr("Leave unconfigured")); + + if (res == 1) + return; + + file = askForAdbPath(); + } + } +} + +void DbAndroid::deviceListChanged() +{ + DBLIST->rescanInvalidDatabasesForPlugin(this); +} + +AdbManager* DbAndroid::getAdbManager() const +{ + return adbManager; +} diff --git a/Plugins/DbAndroid/dbandroid.h b/Plugins/DbAndroid/dbandroid.h new file mode 100644 index 0000000..68893d0 --- /dev/null +++ b/Plugins/DbAndroid/dbandroid.h @@ -0,0 +1,63 @@ +#ifndef DBANDROID_H +#define DBANDROID_H + +#include "dbandroid_global.h" +#include "plugins/dbplugin.h" +#include "plugins/genericplugin.h" +#include "config_builder.h" + +class AdbManager; +class DbAndroidConnectionFactory; + +CFG_CATEGORIES(DbAndroidConfig, + CFG_CATEGORY(DbAndroid, + CFG_ENTRY(QString, AdbPath, QString()) + ) +) + +class DBANDROIDSHARED_EXPORT DbAndroid : public GenericPlugin, public DbPlugin +{ + Q_OBJECT + SQLITESTUDIO_PLUGIN("dbandroid.json") + + public: + DbAndroid(); + + QString getLabel() const; + bool checkIfDbServedByPlugin(Db* db) const; + Db* getInstance(const QString& name, const QString& path, const QHash& options, QString* errorMessage); + QList getOptionsList() const; + QString generateDbName(const QVariant& baseValue); + bool init(); + void deinit(); + QString getCurrentAdb(); + AdbManager* getAdbManager() const; + bool isAdbValid() const; + DbAndroidConnectionFactory* getConnectionFactory() const; + + static_char* PASSWORD_OPT = "remote_access_password"; + + private: + void initAdb(); + QString askForAdbPath(); + + AdbManager* adbManager = nullptr; + DbAndroidConnectionFactory* connectionFactory = nullptr; + bool adbValid = false; + + static_char* SELECT_ADB_URL = "select_adb://"; + + CFG_LOCAL_PERSISTABLE(DbAndroidConfig, cfg) + + private slots: + void handleValidAdb(bool showMessage); + void handleInvalidAdb(); + void statusFieldLinkClicked(const QString& link); + void deviceListChanged(); + + signals: + void adbReady(bool showMessage); + void invalidAdb(); +}; + +#endif // DBANDROID_H diff --git a/Plugins/DbAndroid/dbandroid.json b/Plugins/DbAndroid/dbandroid.json new file mode 100644 index 0000000..0808bb4 --- /dev/null +++ b/Plugins/DbAndroid/dbandroid.json @@ -0,0 +1,9 @@ +{ + "type": "DbPlugin", + "title": "Android SQLite", + "description": "Provides support for remote SQLite databases on Android devices.", + "version": 10100, + "author": "SalSoft", + "minAppVersion": 30006, + "gui": true +} diff --git a/Plugins/DbAndroid/dbandroid.qrc b/Plugins/DbAndroid/dbandroid.qrc new file mode 100644 index 0000000..7646d2b --- /dev/null +++ b/Plugins/DbAndroid/dbandroid.qrc @@ -0,0 +1 @@ + diff --git a/Plugins/DbAndroid/dbandroid_global.h b/Plugins/DbAndroid/dbandroid_global.h new file mode 100644 index 0000000..aa128c1 --- /dev/null +++ b/Plugins/DbAndroid/dbandroid_global.h @@ -0,0 +1,12 @@ +#ifndef DBANDROID_GLOBAL_H +#define DBANDROID_GLOBAL_H + +#include + +#if defined(DBANDROID_LIBRARY) +# define DBANDROIDSHARED_EXPORT Q_DECL_EXPORT +#else +# define DBANDROIDSHARED_EXPORT Q_DECL_IMPORT +#endif + +#endif // DBANDROID_GLOBAL_H diff --git a/Plugins/DbAndroid/dbandroidconnection.cpp b/Plugins/DbAndroid/dbandroidconnection.cpp new file mode 100644 index 0000000..1d5ba05 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidconnection.cpp @@ -0,0 +1,14 @@ +#include "dbandroidconnection.h" +#include + +QByteArray DbAndroidConnection::convertBlob(const QString& value) +{ + if (!value.startsWith("X'", Qt::CaseInsensitive) || !value.endsWith("'")) + { + qCritical() << "Invalid BLOB value from Android. Doesn't match BLOB pattern:" << value; + return QByteArray(); + } + + return QByteArray::fromHex(value.mid(2, value.length() - 3).toLatin1()); +} + diff --git a/Plugins/DbAndroid/dbandroidconnection.h b/Plugins/DbAndroid/dbandroidconnection.h new file mode 100644 index 0000000..11e9be0 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidconnection.h @@ -0,0 +1,44 @@ +#ifndef DBANDROIDCONNECTION_H +#define DBANDROIDCONNECTION_H + +#include "dbandroidurl.h" +#include +#include +#include + +class DbAndroidConnection : public QObject +{ + Q_OBJECT + + public: + struct ExecutionResult + { + bool wasError = false; + int errorCode = 0; + QString errorMsg; + QStringList resultColumns; + QList resultDataMap; + QList resultDataList; + }; + + DbAndroidConnection(QObject* parent = 0) : QObject(parent) {} + virtual ~DbAndroidConnection() {} + + virtual bool connectToAndroid(const DbAndroidUrl& url) = 0; + virtual void disconnectFromAndroid() = 0; + virtual bool isConnected() const = 0; + virtual QString getDbName() const = 0; + virtual QStringList getDbList() = 0; + virtual QStringList getAppList() = 0; + virtual bool isAppOkay() const = 0; + virtual bool deleteDatabase(const QString& dbName) = 0; + virtual ExecutionResult executeQuery(const QString& query) = 0; + + protected: + static QByteArray convertBlob(const QString& value); + + signals: + void disconnected(); +}; + +#endif // DBANDROIDCONNECTION_H diff --git a/Plugins/DbAndroid/dbandroidconnectionfactory.cpp b/Plugins/DbAndroid/dbandroidconnectionfactory.cpp new file mode 100644 index 0000000..22377b4 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidconnectionfactory.cpp @@ -0,0 +1,28 @@ +#include "dbandroidconnectionfactory.h" +#include "dbandroidjsonconnection.h" +#include "dbandroidshellconnection.h" + +DbAndroidConnectionFactory::DbAndroidConnectionFactory(DbAndroid* plugin) : + plugin(plugin) +{ +} + +DbAndroidConnection* DbAndroidConnectionFactory::create(const QString& url, QObject* parent) +{ + return create(DbAndroidUrl(url), parent); +} + +DbAndroidConnection* DbAndroidConnectionFactory::create(const DbAndroidUrl& url, QObject* parent) +{ + switch (url.getMode()) + { + case DbAndroidMode::SHELL: + return new DbAndroidShellConnection(plugin, parent); + case DbAndroidMode::NETWORK: + case DbAndroidMode::USB: + return new DbAndroidJsonConnection(plugin, parent); + case DbAndroidMode::null: + break; + } + return nullptr; +} diff --git a/Plugins/DbAndroid/dbandroidconnectionfactory.h b/Plugins/DbAndroid/dbandroidconnectionfactory.h new file mode 100644 index 0000000..0e46c9f --- /dev/null +++ b/Plugins/DbAndroid/dbandroidconnectionfactory.h @@ -0,0 +1,21 @@ +#ifndef DBANDROIDCONNECTIONFACTORY_H +#define DBANDROIDCONNECTIONFACTORY_H + +#include "dbandroidurl.h" + +class DbAndroidConnection; +class DbAndroid; + +class DbAndroidConnectionFactory +{ + public: + explicit DbAndroidConnectionFactory(DbAndroid* plugin); + + DbAndroidConnection* create(const QString& url, QObject* parent = nullptr); + DbAndroidConnection* create(const DbAndroidUrl& url, QObject* parent = nullptr); + + private: + DbAndroid* plugin = nullptr; +}; + +#endif // DBANDROIDCONNECTIONFACTORY_H diff --git a/Plugins/DbAndroid/dbandroidinstance.cpp b/Plugins/DbAndroid/dbandroidinstance.cpp new file mode 100644 index 0000000..b60203e --- /dev/null +++ b/Plugins/DbAndroid/dbandroidinstance.cpp @@ -0,0 +1,143 @@ +#include "dbandroidconnection.h" +#include "dbandroidinstance.h" +#include "sqlqueryandroid.h" +#include "db/sqlerrorcodes.h" +#include "common/unused.h" +#include "dbandroid.h" +#include "dbandroidjsonconnection.h" +#include "dbandroidconnectionfactory.h" +#include "dbandroidurl.h" +#include "schemaresolver.h" +#include "services/notifymanager.h" +#include +#include +#include +#include + +DbAndroidInstance::DbAndroidInstance(DbAndroid* plugin, const QString& name, const QString& path, const QHash& connOptions) : + AbstractDb(name, path, connOptions), plugin(plugin) +{ + this->connOptions[SchemaResolver::USE_SCHEMA_CACHING] = true; +} + +DbAndroidInstance::~DbAndroidInstance() +{ + closeInternal(); +} + +SqlQueryPtr DbAndroidInstance::prepare(const QString& query) +{ + return SqlQueryPtr(new SqlQueryAndroid(this, connection, query)); +} + +QString DbAndroidInstance::getTypeLabel() +{ + return plugin->getLabel(); +} + +bool DbAndroidInstance::deregisterFunction(const QString& name, int argCount) +{ + // Unsupported by native Android driver + UNUSED(name); + UNUSED(argCount); + return true; +} + +bool DbAndroidInstance::registerScalarFunction(const QString& name, int argCount) +{ + // Unsupported by native Android driver + UNUSED(name); + UNUSED(argCount); + return true; +} + +bool DbAndroidInstance::registerAggregateFunction(const QString& name, int argCount) +{ + // Unsupported by native Android driver + UNUSED(name); + UNUSED(argCount); + return true; +} + +bool DbAndroidInstance::initAfterCreated() +{ + version = 3; + return AbstractDb::initAfterCreated(); +} + +bool DbAndroidInstance::isOpenInternal() +{ + return (connection && connection->isConnected()); +} + +void DbAndroidInstance::interruptExecution() +{ + // Unsupported by native Android driver +} + +QString DbAndroidInstance::getErrorTextInternal() +{ + return errorText; +} + +int DbAndroidInstance::getErrorCodeInternal() +{ + return errorCode; +} + +bool DbAndroidInstance::openInternal() +{ + connection = createConnection(); + bool res = connection->connectToAndroid(DbAndroidUrl(path)); + if (!res) + { + safe_delete(connection); + } + else + { + connect(connection, SIGNAL(disconnected()), this, SLOT(handleDisconnected())); + } + + return res; +} + +bool DbAndroidInstance::closeInternal() +{ + if (!connection) + return false; + + disconnect(connection, SIGNAL(disconnected()), this, SLOT(handleDisconnected())); + connection->disconnectFromAndroid(); + safe_delete(connection); + return true; +} + +bool DbAndroidInstance::registerCollationInternal(const QString& name) +{ + // Unsupported by native Android driver + UNUSED(name); + return true; +} + +bool DbAndroidInstance::deregisterCollationInternal(const QString& name) +{ + // Unsupported by native Android driver + UNUSED(name); + return true; +} + +DbAndroidConnection* DbAndroidInstance::createConnection() +{ + DbAndroidUrl url(path); + if (!url.isValid(false)) + return nullptr; + + return plugin->getConnectionFactory()->create(url, this); +} + +void DbAndroidInstance::handleDisconnected() +{ + safe_delete(connection); + notifyWarn(tr("Connection with Android database '%1' lost.").arg(getName())); + emit disconnected(); +} diff --git a/Plugins/DbAndroid/dbandroidinstance.h b/Plugins/DbAndroid/dbandroidinstance.h new file mode 100644 index 0000000..453b370 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidinstance.h @@ -0,0 +1,51 @@ +#ifndef DBANDROIDINSTANCE_H +#define DBANDROIDINSTANCE_H + +#include "db/abstractdb.h" +#include +#include +#include + +class DbAndroidConnection; +class DbAndroid; + +class DbAndroidInstance : public AbstractDb +{ + Q_OBJECT + + public: + typedef std::function AsyncDbListResponseHandler; + + DbAndroidInstance(DbAndroid* plugin, const QString& name, const QString& path, const QHash& connOptions); + ~DbAndroidInstance(); + + SqlQueryPtr prepare(const QString& query); + QString getTypeLabel(); + bool deregisterFunction(const QString& name, int argCount); + bool registerScalarFunction(const QString& name, int argCount); + bool registerAggregateFunction(const QString& name, int argCount); + bool initAfterCreated(); + + protected: + bool isOpenInternal(); + void interruptExecution(); + QString getErrorTextInternal(); + int getErrorCodeInternal(); + bool openInternal(); + bool closeInternal(); + bool registerCollationInternal(const QString& name); + bool deregisterCollationInternal(const QString& name); + + private: + DbAndroidConnection* createConnection(); + + DbAndroid* plugin = nullptr; + DbAndroidConnection* connection = nullptr; + int errorCode = 0; + QString errorText; + + private slots: + void handleDisconnected(); +}; + +#endif // DBANDROIDINSTANCE_H diff --git a/Plugins/DbAndroid/dbandroidjsonconnection.cpp b/Plugins/DbAndroid/dbandroidjsonconnection.cpp new file mode 100644 index 0000000..05d6469 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidjsonconnection.cpp @@ -0,0 +1,430 @@ +#include "dbandroidjsonconnection.h" +#include "dbandroid.h" +#include "adbmanager.h" +#include "services/notifymanager.h" +#include "common/blockingsocket.h" +#include "db/sqlerrorcodes.h" +#include +#include +#include + +DbAndroidJsonConnection::DbAndroidJsonConnection(DbAndroid* plugin, QObject *parent) : + DbAndroidConnection(parent), plugin(plugin) +{ + socket = new BlockingSocket(this); + adbManager = plugin->getAdbManager(); + connect(socket, SIGNAL(disconnected()), this, SLOT(handlePossibleDisconnection())); +} + +DbAndroidJsonConnection::~DbAndroidJsonConnection() +{ + cleanUp(); +} + +bool DbAndroidJsonConnection::connectToAndroid(const DbAndroidUrl& url) +{ + if (isConnected()) + { + qWarning() << "Already connected while calling DbAndroidConnection::connect()."; + return false; + } + + dbUrl = url; + mode = url.getMode(); + + switch (mode) + { + case DbAndroidMode::NETWORK: + return connectToNetwork(); + case DbAndroidMode::USB: + return connectToDevice(); + case DbAndroidMode::SHELL: + qCritical() << "SHELL mode encountered in DbAndroidJsonConnection"; + break; + case DbAndroidMode::null: + qCritical() << "Null mode encountered in DbAndroidJsonConnection"; + break; + } + + qCritical() << "Invalid Android db mode while connecting:" << static_cast(mode); + return false; +} + +void DbAndroidJsonConnection::disconnectFromAndroid() +{ + socket->disconnectFromHost(); + connectedState = false; +} + +bool DbAndroidJsonConnection::isConnected() const +{ + if (!socket) + return false; + + return connectedState; +} + +QByteArray DbAndroidJsonConnection::send(const QByteArray& data) +{ + QByteArray bytes = sizeToBytes(data.size()); + bytes.append(data); + return sendBytes(bytes); +} + +QString DbAndroidJsonConnection::getDbName() const +{ + return dbUrl.getDbName(); +} + +QByteArray DbAndroidJsonConnection::sendBytes(const QByteArray& data) +{ + //qDebug() << "Sending" << data; + bool success = socket->send(data); + if (!success) + { + qCritical() << "Error writing bytes to Android socket:" << socket->getErrorText(); + return QByteArray(); + } + + QByteArray sizeBytes = socket->read(4, 5000, &success); + if (!success) + { + qCritical() << "Error reading response size from Android socket:" << socket->getErrorText(); + return QByteArray(); + } + + qint32 size = bytesToSize(sizeBytes); + QByteArray responseBytes = socket->read(size, 5000, &success); + if (!success) + { + qCritical() << "Error reading response from Android socket:" << socket->getErrorText(); + return QByteArray(); + } + //qDebug() << "Received" << responseBytes; + return responseBytes; +} + +void DbAndroidJsonConnection::handleSocketError() +{ + qWarning() << "Blocking socket error in Android connection:" << socket->getErrorText(); + handlePossibleDisconnection(); +} + +void DbAndroidJsonConnection::handlePossibleDisconnection() +{ + if (connectedState && !socket->isConnected()) + { + connectedState = false; + emit disconnected(); + } +} + +QByteArray DbAndroidJsonConnection::sizeToBytes(qint32 size) +{ + QByteArray bytes; + for (int i = 0; i < 4; i++) + bytes.append((size >> (8*i)) & 0xff); + + return bytes; +} + +qint32 DbAndroidJsonConnection::bytesToSize(const QByteArray& bytes) +{ + int size = (((unsigned char)bytes[3]) << 24) | + (((unsigned char)bytes[2]) << 16) | + (((unsigned char)bytes[1]) << 8) | + ((unsigned char)bytes[0]); + + return size; +} + +QVariant DbAndroidJsonConnection::convertJsonValue(const QJsonValue& value) +{ + if (value.isArray()) + { + // BLOB + QJsonArray blobContainer = value.toArray(); + if (blobContainer.size() < 1) + { + qCritical() << "Invalid blob value from Android - empty array."; + return QByteArray(); + } + + return convertBlob(blobContainer.first().toString()); + } + + // Regular value + return value.toVariant(); +} + +bool DbAndroidJsonConnection::connectToNetwork() +{ + if (!dbUrl.isHostValid()) + return false; + + return connectToTcp(dbUrl.getHost(), dbUrl.getPort()); +} + +bool DbAndroidJsonConnection::connectToDevice() +{ + if (!plugin->isAdbValid()) + return false; + + if (!plugin->getAdbManager()->getDevices().contains(dbUrl.getDevice())) + { + notifyWarn(tr("Cannot connect to device %1, because it's not visible to your computer.").arg(dbUrl.getDevice())); + return false; + } + + int localPort = plugin->getAdbManager()->makeForwardFor(dbUrl.getDevice(), dbUrl.getPort()); + if (localPort < 0) + { + notifyError(tr("Failed to create port forwarding for device %1 for port %2.") + .arg(dbUrl.getDevice(), QString::number(dbUrl.getPort()))); + return false; + } + + return connectToTcp("127.0.0.1", localPort); +} + +bool DbAndroidJsonConnection::connectToTcp(const QString& ip, int port) +{ + bool success = socket->connectToHost(ip, port); + if (!success) + { + qWarning() << "Could not connect to network host for Android DB:" << ip << ":" << port << ", details:" << socket->getErrorText(); + notifyWarn(tr("Could not connect to network host: %1:%2").arg(ip, QString::number(port))); + return false; + } + + connectedState = true; + + // Evaluate the connection and send the security token + static_qstring(pingCmd, "{ping:\"%1\"}"); + QDate date = QDateTime::currentDateTime().date(); + QString tokenString = "06fn43" + QString::number(date.dayOfYear()) + "3ig7ws" + QString::number(date.year()) + "53"; + QByteArray md5 = QCryptographicHash::hash(tokenString.toUtf8(), QCryptographicHash::Md5); + QByteArray token = md5.toBase64(); + + QByteArray response = send(pingCmd.arg(QString::fromLatin1(token)).toUtf8()); + if (response != PING_RESPONSE_OK) + { + qWarning() << "Connection to" << ip << ":" << port << "has failed, because response to PING was:" << response; + notifyWarn(tr("Cannot connect to SQLiteStudio service on Android device. The remote service is unavailable or invalid.")); + handleConnectionFailed(); + return false; + } + + // Authenticate + QString pass = dbUrl.getPassword(); + if (!pass.isEmpty()) + { + static_qstring(passPharse, "{auth:\"%1\"}"); + response = send(passPharse.arg(pass.replace("\"", "\\\"")).toUtf8()); + if (response != PASS_RESPONSE_OK) + { + notifyWarn(tr("Cannot connect to %1:%2, because password is invalid.").arg(ip, QString::number(port))); + handleConnectionFailed(); + return false; + } + } + + return true; +} + +void DbAndroidJsonConnection::handleConnectionFailed() +{ + connectedState = false; + socket->disconnectFromHost(); +} + +void DbAndroidJsonConnection::cleanUp() +{ + disconnectFromAndroid(); + safe_delete(socket); +} + +QStringList DbAndroidJsonConnection::getDbList() +{ + if (!isConnected()) + { + qWarning() << "Called DbAndroidJsonConnection::getDbList() on closed connection."; + return QStringList(); + } + + QByteArray result = send(LIST_CMD); + return handleDbListResult(result); +} + +QStringList DbAndroidJsonConnection::getAppList() +{ + return QStringList(); +} + +bool DbAndroidJsonConnection::isAppOkay() const +{ + return true; +} + +QStringList DbAndroidJsonConnection::handleDbListResult(const QByteArray& results) +{ + QJsonParseError jsonError; + QJsonDocument jsonResponse = QJsonDocument::fromJson(results, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qCritical() << "Error while parsing response from Android:" << jsonError.errorString(); + return QStringList(); + } + + QJsonObject responseObject = jsonResponse.object(); + if (responseObject.contains("generic_error")) + { + qCritical() << "Generic error from Android:" << responseObject["generic_error"].toInt(); + return QStringList(); + } + + if (!responseObject.contains("list")) + { + qCritical() << "Missing 'list' in response from Android."; + return QStringList(); + } + + QStringList dbNames; + for (const QVariant& name : responseObject["list"].toArray().toVariantList()) + dbNames << name.toString(); + + return dbNames; +} + +bool DbAndroidJsonConnection::deleteDatabase(const QString& dbName) +{ + if (!isConnected()) + { + qWarning() << "Called DbAndroidConnection::deleteDatabase() on closed database."; + return false; + } + + QByteArray result = send(QString(DELETE_DB_CMD).arg(dbName).toUtf8()); + return handleStdResult(result); +} + +DbAndroidConnection::ExecutionResult DbAndroidJsonConnection::executeQuery(const QString& query) +{ + DbAndroidConnection::ExecutionResult executionResults; + + QJsonDocument json = wrapQueryInJson(query); + QByteArray responseBytes = send(json.toJson(QJsonDocument::Compact)); + + QJsonParseError jsonError; + QJsonDocument jsonResponse = QJsonDocument::fromJson(responseBytes, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + executionResults.wasError = true; + executionResults.errorMsg = tr("Error while parsing response from Android: %1").arg(jsonError.errorString()); + return executionResults; + } + + QJsonObject responseObject = jsonResponse.object(); + if (responseObject.contains("generic_error")) + { + executionResults.wasError = true; + executionResults.errorMsg = tr("Generic error from Android: %1").arg(responseObject["generic_error"].toInt()); + return executionResults; + } + + if (responseObject.contains("error_code")) + { + executionResults.errorCode = responseObject["error_code"].toInt(); + executionResults.errorMsg = responseObject["error_message"].toString(); + return executionResults; + } + + if (!responseObject.contains("columns")) + { + executionResults.wasError = true; + executionResults.errorMsg = tr("Missing 'columns' in response from Android."); + return executionResults; + } + + if (!responseObject.contains("data")) + { + executionResults.wasError = true; + executionResults.errorMsg = tr("Missing 'columns' in response from Android."); + return executionResults; + } + + for (const QVariant& col : responseObject["columns"].toArray().toVariantList()) + executionResults.resultColumns << col.toString(); + + QJsonArray jsonRows = responseObject["data"].toArray(); + QJsonObject jsonRow; + QJsonValue jsonValue; + QVariantHash rowAsMap; + QVariantList rowAsList; + QVariant cellValue; + for (int i = 0, total = jsonRows.size(); i < total; ++i) + { + jsonRow = jsonRows[i].toObject(); + for (const QString& colName : executionResults.resultColumns) + { + if (!jsonRow.contains(colName)) + { + executionResults.wasError = true; + executionResults.errorMsg = tr("Response from Android has missing data for column '%1' in row %2.").arg(colName, QString::number(i+1)); + return executionResults; + } + + jsonValue = jsonRow[colName]; + cellValue = convertJsonValue(jsonValue); + rowAsMap[colName] = cellValue; + rowAsList << cellValue; + } + + executionResults.resultDataMap << rowAsMap; + executionResults.resultDataList << rowAsList; + + rowAsMap.clear(); + rowAsList.clear(); + } + + return executionResults; +} + +QJsonDocument DbAndroidJsonConnection::wrapQueryInJson(const QString& query) +{ + QJsonDocument doc; + + QJsonObject rootObj; + rootObj["cmd"] = "QUERY"; + rootObj["db"] = dbUrl.getDbName(); + rootObj["query"] = query; + + doc.setObject(rootObj); + return doc; +} + +bool DbAndroidJsonConnection::handleStdResult(const QByteArray& results) +{ + QJsonParseError jsonError; + QJsonDocument jsonResponse = QJsonDocument::fromJson(results, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qCritical() << "Error while parsing response from Android:" << jsonError.errorString(); + return false; + } + + QJsonObject responseObject = jsonResponse.object(); + if (responseObject.contains("generic_error")) + { + qCritical() << "Generic error from Android:" << responseObject["generic_error"].toInt(); + return false; + } + + if (!responseObject.contains("result")) + { + qCritical() << "Missing 'result' in response from Android."; + return false; + } + + return (responseObject["result"].toString() == "ok"); +} diff --git a/Plugins/DbAndroid/dbandroidjsonconnection.h b/Plugins/DbAndroid/dbandroidjsonconnection.h new file mode 100644 index 0000000..ef0c943 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidjsonconnection.h @@ -0,0 +1,65 @@ +#ifndef DBANDROIDJSONCONNECTION_H +#define DBANDROIDJSONCONNECTION_H + +#include "dbandroidmode.h" +#include "common/global.h" +#include "common/expiringcache.h" +#include "dbandroidconnection.h" +#include + +class DbAndroid; +class AdbManager; +class BlockingSocket; + +class DbAndroidJsonConnection : public DbAndroidConnection +{ + Q_OBJECT + + public: + DbAndroidJsonConnection(DbAndroid* plugin, QObject *parent = 0); + ~DbAndroidJsonConnection(); + + bool connectToAndroid(const DbAndroidUrl& url); + void disconnectFromAndroid(); + bool isConnected() const; + QByteArray send(const QByteArray& data); + QString getDbName() const; + QStringList getDbList(); + QStringList getAppList(); + bool isAppOkay() const; + bool deleteDatabase(const QString& dbName); + ExecutionResult executeQuery(const QString& query); + + private: + QJsonDocument wrapQueryInJson(const QString& query); + bool connectToNetwork(); + bool connectToDevice(); + bool connectToTcp(const QString& ip, int port); + void cleanUp(); + QByteArray sendBytes(const QByteArray& data); + void handleSocketError(); + void handleConnectionFailed(); + QStringList handleDbListResult(const QByteArray& results); + bool handleStdResult(const QByteArray& results); + + static QByteArray sizeToBytes(qint32 size); + static qint32 bytesToSize(const QByteArray& bytes); + static QVariant convertJsonValue(const QJsonValue& value); + + DbAndroid* plugin = nullptr; + AdbManager* adbManager = nullptr; + BlockingSocket* socket = nullptr; + DbAndroidUrl dbUrl; + DbAndroidMode mode = DbAndroidMode::NETWORK; + bool connectedState = false; + + static_char* PASS_RESPONSE_OK = "{\"result\":\"ok\"}"; + static_char* PING_RESPONSE_OK = "{\"result\":\"pong\"}"; + static_char* LIST_CMD = "{cmd:\"LIST\"}"; + static_char* DELETE_DB_CMD = "{cmd:\"DELETE_DB\",db:\"%1\"}"; + + private slots: + void handlePossibleDisconnection(); +}; + +#endif // DBANDROIDJSONCONNECTION_H diff --git a/Plugins/DbAndroid/dbandroidmode.h b/Plugins/DbAndroid/dbandroidmode.h new file mode 100644 index 0000000..523fdd8 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidmode.h @@ -0,0 +1,13 @@ +#ifndef DBANDROIDMODE +#define DBANDROIDMODE + +enum class DbAndroidMode +{ + USB, + NETWORK, + SHELL, + null +}; + +#endif // DBANDROIDMODE + diff --git a/Plugins/DbAndroid/dbandroidpathdialog.cpp b/Plugins/DbAndroid/dbandroidpathdialog.cpp new file mode 100644 index 0000000..da1cf5c --- /dev/null +++ b/Plugins/DbAndroid/dbandroidpathdialog.cpp @@ -0,0 +1,579 @@ +#include "dbandroidpathdialog.h" +#include "ui_dbandroidpathdialog.h" +#include "common/ipvalidator.h" +#include "dbandroid.h" +#include "common/widgetcover.h" +#include "adbmanager.h" +#include "uiutils.h" +#include "iconmanager.h" +#include "dbandroidconnection.h" +#include "dbandroidconnectionfactory.h" +#include +#include +#include +#include +#include +#include +#include + +DbAndroidPathDialog::DbAndroidPathDialog(const DbAndroid* plugin, QWidget *parent) : + QDialog(parent), + plugin(plugin), + ui(new Ui::DbAndroidPathDialog) +{ + init(); +} + +DbAndroidPathDialog::~DbAndroidPathDialog() +{ + delete ui; +} + +void DbAndroidPathDialog::setUrl(const QString& url) +{ + dbUrl = DbAndroidUrl(url); + loadUrl(); +} + +void DbAndroidPathDialog::setUrl(const DbAndroidUrl& url) +{ + dbUrl = url; + loadUrl(); +} + +const DbAndroidUrl& DbAndroidPathDialog::getUrl() const +{ + return dbUrl; +} + +void DbAndroidPathDialog::init() +{ + ui->setupUi(this); + + dbListCover = new WidgetCover(ui->databaseCombo); + appListCover = new WidgetCover(ui->appCombo); + new UserInputFilter(ui->appFilterEdit, this, SLOT(applyAppFilter(QString))); + + ui->createDatabaseButton->setIcon(ICONS.PLUS); + ui->deleteDatabaseButton->setIcon(ICONS.DELETE); + + dbListUpdateTimer = new QTimer(this); + dbListUpdateTimer->setSingleShot(true); + dbListUpdateTimer->setInterval(500); + connect(dbListUpdateTimer, SIGNAL(timeout()), this, SLOT(refreshDbList())); + + appListUpdateTimer = new QTimer(this); + appListUpdateTimer->setSingleShot(true); + appListUpdateTimer->setInterval(500); + connect(appListUpdateTimer, SIGNAL(timeout()), this, SLOT(refreshAppList())); + + connect(ui->deviceCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(scheduleAppListUpdate())); + connect(ui->databaseCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateState())); + connect(ui->portSpin, SIGNAL(valueChanged(int)), this, SLOT(scheduleDbListUpdate())); + connect(ui->createDatabaseButton, SIGNAL(clicked()), this, SLOT(createNewDatabase())); + connect(ui->deleteDatabaseButton, SIGNAL(clicked()), this, SLOT(deleteSelectedDatabase())); + connect(ui->passwordGroup, SIGNAL(toggled(bool)), this, SLOT(updateState())); + connect(ui->passwordGroup, SIGNAL(toggled(bool)), this, SLOT(scheduleDbListUpdate())); + connect(ui->passwordEdit, SIGNAL(textChanged(QString)), this, SLOT(scheduleDbListUpdate())); + + connect(this, SIGNAL(asyncDbListUpdatingFinished(bool)), this, SLOT(handleFinishedAsyncDbListUpdate(bool))); + connect(this, SIGNAL(asyncAppListUpdatingFinished()), this, SLOT(handleFinishedAsyncAppListUpdate())); + connect(this, SIGNAL(callForDbListUpdate(QStringList)), this, SLOT(handleUpdateDbList(QStringList))); + connect(this, SIGNAL(callForAppListUpdate(QStringList)), this, SLOT(handleUpdateAppList(QStringList))); + connect(this, SIGNAL(callForValidations()), this, SLOT(updateValidations())); + connect(this, SIGNAL(callForDbCreationUpdate(bool)), this, SLOT(handleDbCreationUpdate(bool))); + + if (!plugin->isAdbValid()) + { + ui->ipRadio->setChecked(true); + ui->usbRadio->setEnabled(false); + ui->shellRadio->setEnabled(false); + } + else + { + refreshDevices(); + connect(plugin->getAdbManager(), SIGNAL(deviceDetailsChanged(QList)), this, SLOT(updateDeviceList())); + } + + connect(ui->ipRadio, SIGNAL(toggled(bool)), this, SLOT(modeChanged(bool))); + connect(ui->usbRadio, SIGNAL(toggled(bool)), this, SLOT(modeChanged(bool))); + connect(ui->shellRadio, SIGNAL(toggled(bool)), this, SLOT(modeChanged(bool))); + connect(ui->ipEdit, SIGNAL(textChanged(QString)), this, SLOT(scheduleDbListUpdate())); + setDbListUpdatesEnabled(true); + + handleDbCreationUpdate(false); + updateState(); + adjustSize(); +} + +void DbAndroidPathDialog::updateUrl() +{ + DbAndroidMode mode = getSelectedMode(); + dbUrl.setEnforcedMode(mode); + switch (mode) + { + case DbAndroidMode::NETWORK: + dbUrl.setHost(ui->ipEdit->text()); + dbUrl.setPort(ui->portSpin->value()); + break; + case DbAndroidMode::USB: + dbUrl.setDevice(ui->deviceCombo->currentData().toString()); + dbUrl.setPort(ui->portSpin->value()); + break; + case DbAndroidMode::SHELL: + dbUrl.setDevice(ui->deviceCombo->currentData().toString()); + dbUrl.setApplication(ui->appCombo->currentText()); + break; + case DbAndroidMode::null: + qCritical() << "Unknown mode in DbAndroidPathDialog::updateUrl()"; + return; + } + + dbUrl.setDbName(ui->databaseCombo->currentText()); + if (ui->passwordGroup->isChecked()) + dbUrl.setPassword(ui->passwordEdit->text()); + else + dbUrl.setPassword(QString()); +} + +void DbAndroidPathDialog::loadUrl() +{ + if (!dbUrl.isValid()) + return; + + switch (dbUrl.getMode()) + { + case DbAndroidMode::NETWORK: + ui->ipRadio->setChecked(true); + ui->ipEdit->setText(dbUrl.getHost()); + break; + case DbAndroidMode::SHELL: + ui->shellRadio->setChecked(true); + ui->deviceCombo->setCurrentIndex(ui->deviceCombo->findData(dbUrl.getDevice())); + setDbListUpdatesEnabled(false); + if (ui->appCombo->findText(dbUrl.getApplication()) == -1) + ui->appCombo->addItem(dbUrl.getApplication()); + + ui->appCombo->setCurrentText(dbUrl.getApplication()); + setDbListUpdatesEnabled(true); + break; + case DbAndroidMode::USB: + ui->usbRadio->setChecked(true); + ui->deviceCombo->setCurrentIndex(ui->deviceCombo->findData(dbUrl.getDevice())); + break; + case DbAndroidMode::null: + qCritical() << "Cannot load URL of mode 'null' in DbAndroidPathDialog::loadUrl()."; + return; + } + + ui->portSpin->setValue(dbUrl.getPort()); + if (ui->databaseCombo->findText(dbUrl.getDbName()) == -1) + ui->databaseCombo->addItem(dbUrl.getDbName()); + + ui->databaseCombo->setCurrentText(dbUrl.getDbName()); + + if (!dbUrl.getPassword().isNull()) + { + ui->passwordGroup->setChecked(true); + ui->passwordEdit->setText(dbUrl.getPassword()); + } +} + +void DbAndroidPathDialog::scheduleDbListUpdate() +{ + if (suspendDbListUpdates) + return; + + bool startCover = true; + if (dbListUpdateTimer->isActive()) + { + dbListUpdateTimer->stop(); + startCover = false; + } + + dbListUpdateTimer->start(); + if (startCover) + dbListCover->show(); + + handleDbCreationUpdate(false); + updateValidations(); +} + +void DbAndroidPathDialog::scheduleAppListUpdate() +{ + if (getSelectedMode() != DbAndroidMode::SHELL) + return; + + if (suspendAppListUpdates) + return; + + bool startCover = true; + if (appListUpdateTimer->isActive()) + { + appListUpdateTimer->stop(); + startCover = false; + } + + appListUpdateTimer->start(); + if (startCover) + appListCover->show(); + + updateValidations(); +} + +void DbAndroidPathDialog::refreshDbList() +{ + if (updatingDbList) + { + // Already busy, schedule next update afterwards. + scheduleDbListUpdate(); + return; + } + + updateUrl(); + ui->databaseCombo->clear(); + + if (!dbUrl.isValid(false)) + { + dbListCover->hide(); + return; + } + + updatingDbList = true; + QtConcurrent::run(this, &DbAndroidPathDialog::asyncDbUpdate, dbUrl.toUrlString(), dbUrl.getMode()); +} + +void DbAndroidPathDialog::refreshAppList() +{ + if (updatingAppList) + { + // Already busy, schedule next update afterwards. + scheduleAppListUpdate(); + return; + } + + updateUrl(); + setDbListUpdatesEnabled(false); + ui->appCombo->clear(); + setDbListUpdatesEnabled(true); + + if (!dbUrl.isValid(false)) + { + appListCover->hide(); + return; + } + + updatingAppList = true; + QtConcurrent::run(this, &DbAndroidPathDialog::asyncAppUpdate, dbUrl.toUrlString(), dbUrl.getMode()); +} + +void DbAndroidPathDialog::asyncDbUpdate(const QString& connectionUrl, DbAndroidMode enforcedMode) +{ + DbAndroidUrl url(connectionUrl); + url.setEnforcedMode(enforcedMode); + + QScopedPointer connection(plugin->getConnectionFactory()->create(url)); + if (!connection->connectToAndroid(url)) + { + qDebug() << "Could not open db connection" << connectionUrl; + emit asyncDbListUpdatingFinished(connection->isAppOkay()); + emit callForValidations(); + return; + } + + QStringList dbList = connection->getDbList(); + bool appOk = connection->isAppOkay(); + + connection->disconnectFromAndroid(); + + emit callForDbCreationUpdate(appOk); + emit callForDbListUpdate(dbList); + emit asyncDbListUpdatingFinished(appOk); + emit callForValidations(); +} + +void DbAndroidPathDialog::asyncAppUpdate(const QString& connectionUrl, DbAndroidMode enforcedMode) +{ + DbAndroidUrl url(connectionUrl); + url.setEnforcedMode(enforcedMode); + + QScopedPointer connection(plugin->getConnectionFactory()->create(url)); + QStringList appList = connection->getAppList(); + emit callForAppListUpdate(appList); + emit asyncAppListUpdatingFinished(); + emit callForValidations(); +} + +void DbAndroidPathDialog::refreshDevices() +{ + static_qstring(displayNameTpl, "%1 (%2)"); + ui->deviceCombo->clear(); + + QString displayName; + QList deviceDetails = plugin->getAdbManager()->getDeviceDetails(); + for (const AdbManager::Device& details : deviceDetails) + { + if (details.fullName.isEmpty()) + displayName = details.id; + else + displayName = displayNameTpl.arg(details.fullName, details.id); + + ui->deviceCombo->addItem(displayName, details.id); + } +} + +DbAndroidMode DbAndroidPathDialog::getSelectedMode() const +{ + if (ui->ipRadio->isChecked()) + return DbAndroidMode::NETWORK; + + if (ui->usbRadio->isChecked()) + return DbAndroidMode::USB; + + return DbAndroidMode::SHELL; +} + +void DbAndroidPathDialog::setDbListUpdatesEnabled(bool enabled) +{ + suspendDbListUpdates = !enabled; + if (enabled) + { + connect(ui->deviceCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(scheduleDbListUpdate())); + connect(ui->appCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(scheduleDbListUpdate())); + } + else + { + disconnect(ui->deviceCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(scheduleDbListUpdate())); + disconnect(ui->appCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(scheduleDbListUpdate())); + } +} + +void DbAndroidPathDialog::updateDeviceList() +{ + suspendDbListUpdates = true; + + bool dbListNeedsUpdate = false; + QString oldValue = ui->deviceCombo->currentData().toString(); + + refreshDevices(); + int idx = ui->deviceCombo->findData(oldValue); + if (idx > -1) + ui->deviceCombo->setCurrentIndex(idx); + else + dbListNeedsUpdate = true; + + suspendDbListUpdates = false; + + updateValidations(); + + if (dbListNeedsUpdate) + scheduleDbListUpdate(); +} + +void DbAndroidPathDialog::updateValidations() +{ + bool isUpdating = dbListUpdateTimer->isActive(); + bool ipOk = true; + bool deviceOk = true; + if (ui->ipRadio->isChecked()) + { + ipOk = IpValidator::check(ui->ipEdit->text()); + setValidState(ui->ipEdit, ipOk, tr("Enter valid IP address.")); + } + else + { + deviceOk = !ui->deviceCombo->currentData().toString().isEmpty(); + setValidState(ui->deviceCombo, deviceOk, tr("Pick Android device.")); + } + + bool dbOk = !ui->databaseCombo->currentText().isEmpty(); + setValidState(ui->databaseCombo, dbOk, tr("Pick Android database.")); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(ipOk && deviceOk && dbOk && !isUpdating); +} + +void DbAndroidPathDialog::handleUpdateDbList(const QStringList& dbList) +{ + ui->databaseCombo->addItems(dbList); + if (dbList.contains(dbUrl.getDbName())) + ui->databaseCombo->setCurrentText(dbUrl.getDbName()); +} + +void DbAndroidPathDialog::handleUpdateAppList(const QStringList& apps) +{ + fullAppList = apps; + QStringList filtered = apps.filter(ui->appFilterEdit->text(), Qt::CaseInsensitive); + ui->appCombo->addItems(filtered); + if (filtered.contains(dbUrl.getApplication())) + ui->appCombo->setCurrentText(dbUrl.getApplication()); +} + +void DbAndroidPathDialog::handleFinishedAsyncDbListUpdate(bool appOkay) +{ + if (getSelectedMode() == DbAndroidMode::SHELL) + setValidState(ui->appCombo, appOkay, tr("Selected Android application is unknown, or not debuggable.")); + + dbListCover->hide(); + updatingDbList = false; +} + +void DbAndroidPathDialog::handleFinishedAsyncAppListUpdate() +{ + appListCover->hide(); + updatingAppList = false; +} + +void DbAndroidPathDialog::handleDbCreationUpdate(bool canCreateDatabases) +{ + ui->createDatabaseButton->setEnabled(canCreateDatabases); +} + +void DbAndroidPathDialog::createNewDatabase() +{ + DbAndroidUrl tmpUrl(dbUrl); + tmpUrl.setDbName(QString()); + + DbAndroidConnection::ExecutionResult results; + QString name; + bool ok = false; + while (!ok) + { + name = QInputDialog::getText(this, tr("Create new database"), tr("Please provide name for the new database.\n" + "It's the name which Android application will use to connect to the database:")); + + if (name.isNull()) + break; + + if (ui->databaseCombo->findText(name) > -1) + { + QMessageBox::warning(this, tr("Invalid name"), tr("Database with the same name (%1) already exists on the device.\n" + "The name must be unique.")); + continue; + } + + tmpUrl.setDbName(name); + QScopedPointer connection(plugin->getConnectionFactory()->create(tmpUrl)); + if (!connection->connectToAndroid(tmpUrl)) + { + QMessageBox::warning(this, tr("Invalid name"), tr("Could not create database '%1', because could not connect to the device.").arg(name)); + continue; + } + + results = connection->executeQuery("PRAGMA encoding;"); + ok = !results.wasError && results.resultDataList.size() > 0; + connection->disconnectFromAndroid(); + + if (!ok) + QMessageBox::warning(this, tr("Invalid name"), tr("Could not create database '%1'.\nDetails: %2").arg(name, results.errorMsg)); + } + + if (ok) + { + ui->databaseCombo->addItem(name); + ui->databaseCombo->setCurrentText(name); + } +} + +void DbAndroidPathDialog::deleteSelectedDatabase() +{ + updateUrl(); + QString dbName = dbUrl.getDbName(); + + QMessageBox::StandardButton res = QMessageBox::question(this, tr("Delete database"), tr("Are you sure you want to delete database '%1' from %2?") + .arg(dbName, dbUrl.getDisplayName())); + + if (res != QMessageBox::Yes) + return; + + int idx = ui->databaseCombo->findText(dbName); + if (idx < 0) + { + QStringList dbList; + for (int i = 0, total = ui->databaseCombo->count(); i < total; ++i) + dbList << ui->databaseCombo->itemText(i); + + qCritical() << "Tried to delete database, but it's not in the list of databases:" << dbName << "and the list is:" << dbList; + return; + } + + // Local db instance, will close on deletion. + QScopedPointer connection(plugin->getConnectionFactory()->create(dbUrl)); + if (!connection->connectToAndroid(dbUrl)) + { + QMessageBox::critical(this, tr("Error deleting"), tr("Could not connect to %1 in order to delete database '%2'.").arg(dbUrl.getDisplayName(), dbName)); + return; + } + + if (!connection->deleteDatabase(dbName)) + { + QMessageBox::critical(this, tr("Error deleting"), tr("Could not delete database named '%1' from the device.\n" + "Android device refused deletion, or it was impossible.").arg(dbName)); + + connection->disconnectFromAndroid(); + return; + } + connection->disconnectFromAndroid(); + + ui->databaseCombo->removeItem(idx); + if (ui->databaseCombo->count() > 0) + { + if (idx < ui->databaseCombo->count()) + ui->databaseCombo->setCurrentIndex(idx); + else + ui->databaseCombo->setCurrentIndex(ui->databaseCombo->count() - 1); + } +} + +void DbAndroidPathDialog::modeChanged(bool checked) +{ + if (!checked) + return; + + updateState(); + adjustSize(); + scheduleAppListUpdate(); + + if (getSelectedMode() != DbAndroidMode::SHELL) + scheduleDbListUpdate(); +} + +void DbAndroidPathDialog::applyAppFilter(const QString& value) +{ + QString selectedApp = ui->appCombo->currentText(); + QStringList filtered = fullAppList.filter(value, Qt::CaseInsensitive); + bool callDbListUpdate = false; + + setDbListUpdatesEnabled(false); + ui->appCombo->clear(); + ui->appCombo->addItems(filtered); + if (filtered.contains(selectedApp)) + ui->appCombo->setCurrentText(selectedApp); + else + callDbListUpdate = true; + + setDbListUpdatesEnabled(true); + + if (callDbListUpdate) + scheduleDbListUpdate(); +} + +void DbAndroidPathDialog::accept() +{ + updateUrl(); + QDialog::accept(); +} + +void DbAndroidPathDialog::updateState() +{ + DbAndroidMode mode = getSelectedMode(); + + ui->deviceGroup->setVisible(mode == DbAndroidMode::SHELL || mode == DbAndroidMode::USB); + ui->ipGroup->setVisible(mode == DbAndroidMode::NETWORK); + ui->portGroup->setVisible(mode == DbAndroidMode::NETWORK || mode == DbAndroidMode::USB); + ui->appGroup->setVisible(mode == DbAndroidMode::SHELL); + ui->passwordGroup->setVisible(mode == DbAndroidMode::NETWORK || mode == DbAndroidMode::USB); + + ui->deleteDatabaseButton->setEnabled(ui->databaseCombo->currentIndex() > -1); + ui->passwordEdit->setEnabled(ui->passwordGroup->isChecked()); + updateValidations(); +} diff --git a/Plugins/DbAndroid/dbandroidpathdialog.h b/Plugins/DbAndroid/dbandroidpathdialog.h new file mode 100644 index 0000000..4c4496f --- /dev/null +++ b/Plugins/DbAndroid/dbandroidpathdialog.h @@ -0,0 +1,80 @@ +#ifndef DBANDROIDPATHDIALOG_H +#define DBANDROIDPATHDIALOG_H + +#include "dbandroidurl.h" +#include + +namespace Ui { + class DbAndroidPathDialog; +} + +class DbAndroid; +class QTimer; +class WidgetCover; +class DbAndroidInstance; + +class DbAndroidPathDialog : public QDialog +{ + Q_OBJECT + + public: + DbAndroidPathDialog(const DbAndroid* plugin, QWidget *parent = 0); + ~DbAndroidPathDialog(); + void setUrl(const QString& url); + void setUrl(const DbAndroidUrl& url); + const DbAndroidUrl& getUrl() const; + + private: + void init(); + void updateUrl(); + void loadUrl(); + void asyncDbUpdate(const QString& connectionUrl, DbAndroidMode enforcedMode); + void asyncAppUpdate(const QString& connectionUrl, DbAndroidMode enforcedMode); + void refreshDevices(); + DbAndroidMode getSelectedMode() const; + void setDbListUpdatesEnabled(bool enabled); + + const DbAndroid* plugin = nullptr; + DbAndroidUrl dbUrl; + Ui::DbAndroidPathDialog *ui; + QTimer* dbListUpdateTimer = nullptr; + QTimer* appListUpdateTimer = nullptr; + WidgetCover* dbListCover = nullptr; + WidgetCover* appListCover = nullptr; + bool updatingDbList = false; + bool updatingAppList = false; + bool suspendDbListUpdates = false; + bool suspendAppListUpdates = false; + QStringList fullAppList; + + private slots: + void scheduleDbListUpdate(); + void scheduleAppListUpdate(); + void updateState(); + void refreshDbList(); + void refreshAppList(); + void updateDeviceList(); + void updateValidations(); + void handleUpdateDbList(const QStringList& dbList); + void handleUpdateAppList(const QStringList& apps); + void handleFinishedAsyncDbListUpdate(bool appOkay); + void handleFinishedAsyncAppListUpdate(); + void handleDbCreationUpdate(bool canCreateDatabases); + void createNewDatabase(); + void deleteSelectedDatabase(); + void modeChanged(bool checked); + void applyAppFilter(const QString& value); + + public slots: + void accept(); + + signals: + void callForValidations(); + void callForDbCreationUpdate(bool canCreateDatabases); + void asyncDbListUpdatingFinished(bool appOkay); + void asyncAppListUpdatingFinished(); + void callForDbListUpdate(const QStringList& newList); + void callForAppListUpdate(const QStringList& newList); +}; + +#endif // DBANDROIDPATHDIALOG_H diff --git a/Plugins/DbAndroid/dbandroidpathdialog.ui b/Plugins/DbAndroid/dbandroidpathdialog.ui new file mode 100644 index 0000000..3643a9c --- /dev/null +++ b/Plugins/DbAndroid/dbandroidpathdialog.ui @@ -0,0 +1,244 @@ + + + DbAndroidPathDialog + + + + 0 + 0 + 402 + 546 + + + + + 400 + 0 + + + + Android database URL + + + + + + Connection method + + + + + + USB cable - port forwarding + + + true + + + + + + + USB cable - sqlite3 command + + + + + + + Network (IP address) + + + + + + + + + + Device + + + + + + + + + + + + IP address + + + + + + 15 + + + ???.???.???.??? + + + true + + + + + + + + + + Port + + + + + + 1 + + + 65535 + + + 12121 + + + + + + + + + + Remote access password + + + true + + + false + + + + + + <p>This is password configured in the SQLiteStudio service being embeded in the Android application.</p> + + + QLineEdit::Password + + + + + + + + + + Application + + + + + + + + + + 100 + 16777215 + + + + Filter + + + true + + + + + + + + + + Database + + + + + + + + + Create a new database directly on the device. + + + + + + + + + + Delete currently selected database from the device. The currently selected database is the one picked in the list on the left of this button. + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + DbAndroidPathDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DbAndroidPathDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/Plugins/DbAndroid/dbandroidshellconnection.cpp b/Plugins/DbAndroid/dbandroidshellconnection.cpp new file mode 100644 index 0000000..1a76f8d --- /dev/null +++ b/Plugins/DbAndroid/dbandroidshellconnection.cpp @@ -0,0 +1,363 @@ +#include "dbandroidshellconnection.h" +#include "adbmanager.h" +#include "dbandroid.h" +#include "services/notifymanager.h" +#include "common/utils_sql.h" +#include "csvserializer.h" +#include + +const CsvFormat DbAndroidShellConnection::CSV_FORMAT = CsvFormat(",", "\r\n", true, true); + +DbAndroidShellConnection::DbAndroidShellConnection(DbAndroid* plugin, QObject* parent) : + DbAndroidConnection(parent), plugin(plugin) +{ + this->adbManager = plugin->getAdbManager(); + connect(adbManager, SIGNAL(deviceListChanged(QStringList)), this, SLOT(checkForDisconnection(QStringList))); +} + +DbAndroidShellConnection::~DbAndroidShellConnection() +{ + +} + +bool DbAndroidShellConnection::connectToAndroid(const DbAndroidUrl& url) +{ + if (url.getMode() != DbAndroidMode::SHELL) + return false; + + if (!adbManager->getDevices().contains(url.getDevice())) + { + notifyWarn(tr("Cannot connect to device %1, because it's not visible to your computer.").arg(url.getDevice())); + return false; + } + + // Check if application is correct + if (url.getApplication().isEmpty()) + { + qCritical() << "Tried to connect to an empty application in DbAndroidShellConnection::connectToAndroid()"; + return false; + } + + QString stdOut; + bool res = adbManager->exec(QStringList({"shell", "run-as", url.getApplication(), "echo", "1"}), &stdOut); + if (!res) + { + notifyWarn(tr("Cannot connect to device %1, because the application %2 doesn't seem to be installed on the device.").arg(url.getDevice(), url.getApplication())); + return false; + } + + QMutexLocker lock(&appOkMutex); + appOkay = true; + if (stdOut.startsWith("run-as:")) + { + appOkay = false; + qWarning() << "Cannot connect to device" << url.getDevice() << "/" << url.getApplication() << "\nDetails:\n" << stdOut.trimmed(); + notifyWarn(tr("Cannot connect to device %1, because the application %2 is not debuggable.") + .arg(url.getDevice(), url.getApplication())); + return false; + } + + // Check if sqlite3 is available + res = adbManager->exec(QStringList({"shell", "run-as", url.getApplication(), "sqlite3", "--version"})); + if (!res) + { + notifyWarn(tr("Cannot connect to device %1, because '%2' command doesn't seem to be available on the device.").arg(url.getDevice(), "sqlite3")); + return false; + } + + // Check if databases directory exists + res = adbManager->exec(QStringList({"shell", "run-as", url.getApplication(), "ls", "databases"})); + if (!res) + { + // Doesn't exist. Create if possible. + res = adbManager->exec(QStringList({"shell", "run-as", url.getApplication(), "mkdir", "databases"})); + if (!res) + { + notifyWarn(tr("Cannot connect to device %1, because '%2' database cannot be accessed on the device.").arg(url.getDevice(), "sqlite3")); + return false; + } + } + + // Try to connect to target database. + connectionUrl = url; + connected = true; + + ExecutionResult response = executeQuery("select sqlite_version()"); + if (response.wasError) + { + disconnectFromAndroid(); + notifyWarn(tr("Cannot connect to device %1, because '%2' database cannot be accessed on the device. Details: %3") + .arg(url.getDevice(), "sqlite3", response.errorMsg)); + return false; + } + + return true; +} + +void DbAndroidShellConnection::disconnectFromAndroid() +{ + connectionUrl = DbAndroidUrl(); + connected = false; +} + +bool DbAndroidShellConnection::isConnected() const +{ + return connected; +} + +QString DbAndroidShellConnection::getDbName() const +{ + return connectionUrl.getDbName(); +} + +QStringList DbAndroidShellConnection::getDbList() +{ + QMutexLocker lock(&appOkMutex); + appOkay = true; + QString out; + bool res = adbManager->exec(QStringList({"shell", "run-as", connectionUrl.getApplication(), "ls", "databases"}), &out); + if (!res) + return QStringList(); + + if (out.startsWith("run-as:")) // means error + { + appOkay = false; + notifyWarn(tr("Cannot get list of databases for application %1. Details: %2").arg(connectionUrl.getApplication(), out.trimmed())); + qWarning() << "DbAndroidShellConnection::getDbList():" << out; + return QStringList(); + } + + QStringList finalList; + for (const QString& dbName : out.trimmed().split("\n", QString::SkipEmptyParts)) + { + if (dbName.trimmed().endsWith("-journal")) + continue; + + finalList << dbName.trimmed(); + } + + return finalList; +} + +QStringList DbAndroidShellConnection::getAppList() +{ + QString out; + bool res = adbManager->exec(QStringList({"shell", "pm list packages -3"}), &out); + if (!res) + return QStringList(); + + QStringList appList; + for (const QString& line : out.trimmed().split("\n", QString::SkipEmptyParts)) + appList << line.mid(8).trimmed(); // skip "package:" prefix + + return appList; +} + +bool DbAndroidShellConnection::isAppOkay() const +{ + QMutexLocker lock(&appOkMutex); + return appOkay; +} + +bool DbAndroidShellConnection::deleteDatabase(const QString& dbName) +{ + return adbManager->exec(QStringList({"shell", "run-as", connectionUrl.getApplication(), "rm", "-f", "databases/" + dbName, "databases/" + dbName + "-journal"})); +} + +DbAndroidConnection::ExecutionResult DbAndroidShellConnection::executeQuery(const QString& query) +{ + const static QStringList stdArguments = QStringList({"shell", "run-as", "", "sqlite3", "-csv", "-separator", ",", "-batch", "-header"}); + + // Prepare usual arguments + QStringList args = stdArguments; + args.replace(2, connectionUrl.getApplication()); + args << "databases/" + connectionUrl.getDbName(); + args << AdbManager::encode(query); + + // In case of SELECT we want to union typeof() for all columns first, then original query + bool isSelect = false; + getQueryAccessMode(query, Dialect::Sqlite3, &isSelect); + QStringList columnNames; + bool firstHalfForTypes = false; + if (isSelect) + { + columnNames = findColumns(args, query); + if (columnNames.size() > 0) + { + firstHalfForTypes = true; + args.removeLast(); + args << appendTypeQueryPart(query, columnNames); + } + } + + // Execute query and handle results + DbAndroidConnection::ExecutionResult results; + QByteArray out; + QByteArray err; + bool res = adbManager->execBytes(args, &out, &err); + if (!res) + { + results.wasError = true; + results.errorMsg = tr("Could not execute query on database '%1': %2").arg(connectionUrl.getDbName(), AdbManager::decode(err)); + return results; + } + + if (out.startsWith("run-as:")) // means error + { + results.wasError = true; + results.errorMsg = tr("Could not execute query on database '%1': %2").arg(connectionUrl.getDbName(), AdbManager::decode(out).trimmed()); + return results; + } + + + QList> deserialized = CsvSerializer::deserialize(out, CSV_FORMAT); + if (deserialized.size() == 0) + return results; // no results + + extractResultData(deserialized, firstHalfForTypes, results); + return results; +} + +QStringList DbAndroidShellConnection::findColumns(const QStringList& originalArgs, const QString& query) +{ + static_qstring(colQueryTpl, "SELECT * FROM (%1) LIMIT 1"); + + QStringList tmpArgs = originalArgs; + QString tmpQuery = query.trimmed(); + if (tmpQuery.endsWith(";")) + tmpQuery.chop(1); + + tmpQuery = colQueryTpl.arg(tmpQuery); + + tmpArgs.removeLast(); + tmpArgs << tmpQuery; + + QString out; + QString err; + bool res = adbManager->exec(tmpArgs, &out, &err); + if (!res) + { + qCritical() << "Error querying columns in DbAndroidShellConnection::findColumns(): " << out << "\n" << err; + return QStringList(); + } + + QList deserialized = CsvSerializer::deserialize(out, CSV_FORMAT); + if (deserialized.size() < 1) + { + // There will be no results. + return QStringList(); + } + + return deserialized.first(); +} + +QString DbAndroidShellConnection::appendTypeQueryPart(const QString& query, const QStringList& columnNames) +{ + static_qstring(typeTpl, "typeof(%1)"); + static_qstring(hexTpl, "hex(%1) AS %1"); + static_qstring(finalQueryTpl, "SELECT %3 FROM (%2) UNION ALL SELECT %1 FROM (%2)"); + + QString tmpQuery = query.trimmed(); + if (tmpQuery.endsWith(";")) + tmpQuery.chop(1); + + QStringList hexColumns; + QStringList typeColumns; + QString wrappedCol; + for (const QString& colName : columnNames) + { + wrappedCol = wrapObjIfNeeded(colName, Dialect::Sqlite3); + typeColumns << typeTpl.arg(wrappedCol); + hexColumns << hexTpl.arg(wrappedCol); + } + + return finalQueryTpl.arg(typeColumns.join(", "), tmpQuery, hexColumns.join(", ")); +} + +void DbAndroidShellConnection::extractResultData(const QList>& deserialized, bool firstHalfForTypes, DbAndroidConnection::ExecutionResult& results) +{ + for (const QByteArray& cell : deserialized.first()) + results.resultColumns << AdbManager::decode(cell); + + QList> data = deserialized.mid(1); // first row are column names + QList> types; + if (firstHalfForTypes) + { + types = data.mid(data.size() / 2); + data = data.mid(0, data.size() / 2); + + QVariantList rowDataList; + QVariantHash rowDataMap; + QList rowData; + QList rowTypes; + QVariant value; + for (int rowIdx = 0, totalRows = data.size(); rowIdx < totalRows; ++rowIdx) + { + rowData = data[rowIdx]; + rowTypes = types[rowIdx]; + + rowDataList.clear(); + rowDataMap.clear(); + for (int i = 0, total = rowData.size(); i < total; ++i) + { + value = valueFromString(rowData[i], rowTypes[i]); + rowDataList << value; + rowDataMap[results.resultColumns[i]] = value; + } + results.resultDataList << rowDataList; + results.resultDataMap << rowDataMap; + } + } + else + { + QVariantList rowDataList; + QVariantHash rowDataMap; + for (const QList& row : data) + { + rowDataList.clear(); + rowDataMap.clear(); + for (int i = 0, total = row.size(); i < total; ++i) + { + rowDataList << AdbManager::decode(row[i]); + rowDataMap[results.resultColumns[i]] = row[i]; + } + results.resultDataList << rowDataList; + results.resultDataMap << rowDataMap; + } + } +} + +QVariant DbAndroidShellConnection::valueFromString(const QByteArray& bytes, const QByteArray& type) +{ + static const QStringList types = QStringList({"null", "integer", "real", "text", "blob"}); + + DataType dataType = static_cast(types.indexOf(AdbManager::decode(type))); + QByteArray decodedBytes = QByteArray::fromHex(bytes); + switch (dataType) + { + case DataType::BLOB: + return decodedBytes; + case DataType::INTEGER: + return QString::fromLatin1(decodedBytes).toLongLong(); + case DataType::REAL: + return QString::fromLatin1(decodedBytes).toDouble(); + case DataType::TEXT: + return QString::fromUtf8(decodedBytes); + case DataType::_NULL: + break; + case DataType::UNKNOWN: + qCritical() << "Unknown type passed to DbAndroidShellConnection::valueFromString():" << type; + break; + } + return QVariant(QString::null); +} + +void DbAndroidShellConnection::checkForDisconnection(const QStringList& devices) +{ + if (connected && !devices.contains(connectionUrl.getDevice())) + { + disconnectFromAndroid(); + emit disconnected(); + } +} + diff --git a/Plugins/DbAndroid/dbandroidshellconnection.h b/Plugins/DbAndroid/dbandroidshellconnection.h new file mode 100644 index 0000000..1c0ae0f --- /dev/null +++ b/Plugins/DbAndroid/dbandroidshellconnection.h @@ -0,0 +1,59 @@ +#ifndef DBANDROIDSHELLCONNECTION_H +#define DBANDROIDSHELLCONNECTION_H + +#include "dbandroidconnection.h" +#include "csvformat.h" + +#include + +class DbAndroid; +class AdbManager; + +class DbAndroidShellConnection : public DbAndroidConnection +{ + Q_OBJECT + + public: + DbAndroidShellConnection(DbAndroid* plugin, QObject *parent = 0); + ~DbAndroidShellConnection(); + + bool connectToAndroid(const DbAndroidUrl& url); + void disconnectFromAndroid(); + bool isConnected() const; + QString getDbName() const; + QStringList getDbList(); + QStringList getAppList(); + bool isAppOkay() const; + bool deleteDatabase(const QString& dbName); + ExecutionResult executeQuery(const QString& query); + + private: + enum class DataType + { + UNKNOWN = -1, + _NULL = 0, + INTEGER = 1, + REAL = 2, + TEXT = 3, + BLOB = 4 + }; + + QStringList findColumns(const QStringList& originalArgs, const QString& query); + QString appendTypeQueryPart(const QString& query, const QStringList& columnNames); + void extractResultData(const QList >& deserialized, bool firstHalfForTypes, ExecutionResult& results); + QVariant valueFromString(const QByteArray& bytes, const QByteArray& type); + + DbAndroid* plugin = nullptr; + AdbManager* adbManager = nullptr; + bool connected = false; + DbAndroidUrl connectionUrl; + bool appOkay = false; + mutable QMutex appOkMutex; + + static const CsvFormat CSV_FORMAT; + + private slots: + void checkForDisconnection(const QStringList& devices); +}; + +#endif // DBANDROIDSHELLCONNECTION_H diff --git a/Plugins/DbAndroid/dbandroidurl.cpp b/Plugins/DbAndroid/dbandroidurl.cpp new file mode 100644 index 0000000..766d810 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidurl.cpp @@ -0,0 +1,217 @@ +#include "dbandroidurl.h" +#include "common/ipvalidator.h" +#include + +DbAndroidUrl::DbAndroidUrl() +{ +} + +DbAndroidUrl::DbAndroidUrl(DbAndroidMode enforcedMode) : + enforcedMode(enforcedMode) +{ +} + +DbAndroidUrl::DbAndroidUrl(const DbAndroidUrl& other) : + enforcedMode(other.enforcedMode), host(other.host), device(other.device), port(other.port), dbName(other.dbName), password(other.password), + application(other.application) +{ +} + +DbAndroidUrl::DbAndroidUrl(const QString& path, bool obfuscatedPassword) +{ + parse(path, obfuscatedPassword); +} + +DbAndroidUrl::~DbAndroidUrl() +{ +} + +QString DbAndroidUrl::toUrlString(bool obfuscatedPassword) const +{ + return toUrl(obfuscatedPassword).toString(); +} + +QUrl DbAndroidUrl::toUrl(bool obfuscatedPassword) const +{ + QUrl url; + url.setScheme(SCHEME); + url.setHost(host); + url.setUserName(device); + url.setPort(port); + url.setPassword(getPassword(obfuscatedPassword)); + url.setPath("/" + (application.isEmpty() ? "!" : application) + "/" + dbName); + return url; +} + +QString DbAndroidUrl::getDisplayName() const +{ + if (!device.isNull()) + return device; + + return host; +} + +void DbAndroidUrl::parse(const QString& path, bool obfuscatedPassword) +{ + QUrl url(path); + if (url.scheme() != SCHEME) + return; + + host = url.host(); + device = url.userName(); + port = url.port(); + + QString urlPath = url.path(); + if (urlPath.startsWith("/")) + urlPath = urlPath.mid(1); + + QStringList pathParts = urlPath.split("/"); + + application = QString(); + if (pathParts.first() != "!") + application = pathParts.first(); + + dbName = QStringList(pathParts.mid(1)).join("/"); + if (!url.password().isEmpty()) + setPassword(url.password(), obfuscatedPassword); + else + setPassword(QString()); +} + +QString DbAndroidUrl::getApplication() const +{ + return application; +} + +void DbAndroidUrl::setApplication(const QString& value) +{ + application = value; +} + +QString DbAndroidUrl::getDevice() const +{ + return device; +} + +void DbAndroidUrl::setDevice(const QString& value) +{ + device = value; +} + +QString DbAndroidUrl::getHost() const +{ + return host; +} + +void DbAndroidUrl::setHost(const QString& value) +{ + host = value; +} + + +QString DbAndroidUrl::getPassword(bool obfuscated) const +{ + if (obfuscated) + return QString::fromLatin1(password.toUtf8().toHex().toBase64()); + + return password; +} + +void DbAndroidUrl::setPassword(const QString& value, bool obfuscated) +{ + if (obfuscated) + { + password = QString::fromUtf8(QByteArray::fromHex(QByteArray::fromBase64(value.toLatin1()))); + return; + } + + password = value; +} + + +QString DbAndroidUrl::getDbName() const +{ + return dbName; +} + +void DbAndroidUrl::setDbName(const QString& value) +{ + dbName = value; +} + +DbAndroidMode DbAndroidUrl::getMode() const +{ + if (enforcedMode != DbAndroidMode::null) + return enforcedMode; + + if (!application.isEmpty()) + return DbAndroidMode::SHELL; + + return host.isEmpty() ? DbAndroidMode::USB : DbAndroidMode::NETWORK; +} + +void DbAndroidUrl::setEnforcedMode(DbAndroidMode mode) +{ + enforcedMode = mode; +} + +bool DbAndroidUrl::isValid(bool validateConnectionIrrelevantParts) const +{ + if (isNull()) + return false; + + if (validateConnectionIrrelevantParts && dbName.isEmpty()) + return false; + + switch (getMode()) + { + case DbAndroidMode::NETWORK: + { + if (!isHostValid()) + return false; + + if (port <= 0) + return false; + + break; + } + case DbAndroidMode::USB: + { + if (port <= 0) + return false; + + break; + } + case DbAndroidMode::SHELL: + { + if (validateConnectionIrrelevantParts && application.isEmpty()) + return false; + + break; + } + case DbAndroidMode::null: + return false; + } + + return true; +} + +bool DbAndroidUrl::isHostValid() const +{ + return IpValidator::check(host); +} + +bool DbAndroidUrl::isNull() const +{ + return host.isEmpty() && device.isEmpty(); +} + +int DbAndroidUrl::getPort() const +{ + return port; +} + +void DbAndroidUrl::setPort(int value) +{ + port = value; +} diff --git a/Plugins/DbAndroid/dbandroidurl.h b/Plugins/DbAndroid/dbandroidurl.h new file mode 100644 index 0000000..594b21c --- /dev/null +++ b/Plugins/DbAndroid/dbandroidurl.h @@ -0,0 +1,59 @@ +#ifndef DBANDROIDURL_H +#define DBANDROIDURL_H + +#include "dbandroidmode.h" +#include +#include + +class DbAndroidUrl +{ + public: + DbAndroidUrl(); + DbAndroidUrl(const DbAndroidUrl& other); + explicit DbAndroidUrl(DbAndroidMode enforcedMode); + explicit DbAndroidUrl(const QString& path, bool obfuscatedPassword = true); + ~DbAndroidUrl(); + + QString toUrlString(bool obfuscatedPassword = true) const; + QUrl toUrl(bool obfuscatedPassword = true) const; + QString getDisplayName() const; + + int getPort() const; + void setPort(int value); + + QString getDbName() const; + void setDbName(const QString& value); + + DbAndroidMode getMode() const; + void setEnforcedMode(DbAndroidMode mode); + bool isValid(bool validateConnectionIrrelevantParts = true) const; + bool isHostValid() const; + bool isNull() const; + + QString getPassword(bool obfuscated = false) const; + void setPassword(const QString& value, bool obfuscated = false); + + QString getHost() const; + void setHost(const QString& value); + + QString getDevice() const; + void setDevice(const QString& value); + + QString getApplication() const; + void setApplication(const QString& value); + + private: + void parse(const QString& path, bool obfuscatedPassword = false); + + static const constexpr char* SCHEME = "android"; + + DbAndroidMode enforcedMode = DbAndroidMode::null; + QString host; + QString device; + int port = -1; + QString dbName; + QString password; + QString application; +}; + +#endif // DBANDROIDURL_H diff --git a/Plugins/DbAndroid/sqlqueryandroid.cpp b/Plugins/DbAndroid/sqlqueryandroid.cpp new file mode 100644 index 0000000..08fe5cc --- /dev/null +++ b/Plugins/DbAndroid/sqlqueryandroid.cpp @@ -0,0 +1,160 @@ +#include "dbandroidconnection.h" +#include "sqlqueryandroid.h" +#include "sqlresultrowandroid.h" +#include "parser/lexer.h" +#include "db/sqlerrorcodes.h" +#include "common/utils_sql.h" +#include "log.h" +#include "dbandroidinstance.h" +#include + +SqlQueryAndroid::SqlQueryAndroid(DbAndroidInstance* db, DbAndroidConnection* connection, const QString& query) : + db(db), connection(connection), queryString(query) +{ + tokenizedQuery = Lexer::tokenize(query, Dialect::Sqlite3); +} + +SqlQueryAndroid::~SqlQueryAndroid() +{ +} + +QString SqlQueryAndroid::getErrorText() +{ + return errorText; +} + +int SqlQueryAndroid::getErrorCode() +{ + return errorCode; +} + +QStringList SqlQueryAndroid::getColumnNames() +{ + return resultColumns; +} + +int SqlQueryAndroid::columnCount() +{ + return resultColumns.size(); +} + +void SqlQueryAndroid::rewind() +{ + currentRow = -1; +} + +SqlResultsRowPtr SqlQueryAndroid::nextInternal() +{ + if (resultDataList.size() == 0) + return SqlResultsRowPtr(); + + currentRow++; + SqlResultRowAndroid* resultRow = new SqlResultRowAndroid(resultDataMap[currentRow], resultDataList[currentRow]); + return SqlResultsRowPtr(resultRow); +} + +bool SqlQueryAndroid::hasNextInternal() +{ + return (currentRow + 1 < resultDataList.size()); +} + +bool SqlQueryAndroid::execInternal(const QList& args) +{ + resetResponse(); + logSql(db, queryString, args, flags); + + int argIdx = 0; + QString query; + for (const TokenPtr& token : tokenizedQuery) + { + if (token->type != Token::BIND_PARAM) + { + query += token->value; + continue; + } + + query += convertArg(args[argIdx++]); + } + + return executeAndHandleResponse(query); +} + +bool SqlQueryAndroid::execInternal(const QHash& args) +{ + resetResponse(); + logSql(db, queryString, args, flags); + + QString argName; + QString query; + for (const TokenPtr& token : tokenizedQuery) + { + if (token->type != Token::BIND_PARAM) + { + query += token->value; + continue; + } + + argName = token->value; + if (!args.contains(argName)) + { + errorCode = SqlErrorCode::OTHER_EXECUTION_ERROR; + errorText = QObject::tr("Cannot bind argument '%1' of the query, because it's value is missing.").arg(argName); + return false; + } + + query += convertArg(args[argName]); + } + + return executeAndHandleResponse(query); +} + +QString SqlQueryAndroid::convertArg(const QVariant& value) +{ + if (value.isNull() || !value.isValid()) + return "NULL"; + + switch (value.type()) + { + case QVariant::Int: + case QVariant::UInt: + case QVariant::LongLong: + case QVariant::ULongLong: + case QVariant::Double: + return value.toString(); + case QVariant::String: + return "'" + value.toString().replace("'", "''") + "'"; + case QVariant::ByteArray: + return "x'" + value.toByteArray().toHex() + "'"; + default: + break; + } + + qCritical() << "Unhandled argument type in SqlQueryAndroid::convertArg():" << value.type(); + return ""; +} + +bool SqlQueryAndroid::executeAndHandleResponse(const QString& query) +{ + DbAndroidConnection::ExecutionResult results = connection->executeQuery(query); + if (results.wasError) + { + errorCode = (results.errorCode != 0) ? results.errorCode : SqlErrorCode::OTHER_EXECUTION_ERROR; + errorText = results.errorMsg; + return false; + } + + resultColumns = results.resultColumns; + resultDataMap = results.resultDataMap; + resultDataList = results.resultDataList; + return true; +} + +void SqlQueryAndroid::resetResponse() +{ + resultColumns.clear(); + resultDataMap.clear(); + resultDataList.clear(); + currentRow = -1; + errorCode = 0; + errorText = QString(); +} diff --git a/Plugins/DbAndroid/sqlqueryandroid.h b/Plugins/DbAndroid/sqlqueryandroid.h new file mode 100644 index 0000000..3944c21 --- /dev/null +++ b/Plugins/DbAndroid/sqlqueryandroid.h @@ -0,0 +1,47 @@ +#ifndef SQLQUERYANDROID_H +#define SQLQUERYANDROID_H + +#include "db/sqlquery.h" +#include "parser/token.h" +#include + +class DbAndroidConnection; +class DbAndroidInstance; + +class SqlQueryAndroid : public SqlQuery +{ + public: + SqlQueryAndroid(DbAndroidInstance* db, DbAndroidConnection* connection, const QString& query); + ~SqlQueryAndroid(); + + QString getErrorText(); + int getErrorCode(); + QStringList getColumnNames(); + int columnCount(); + void rewind(); + + protected: + SqlResultsRowPtr nextInternal(); + bool hasNextInternal(); + bool execInternal(const QList& args); + bool execInternal(const QHash& args); + + private: + bool executeAndHandleResponse(const QString& query); + void resetResponse(); + + static QString convertArg(const QVariant& value); + + DbAndroidInstance* db = nullptr; + DbAndroidConnection* connection = nullptr; + QString queryString; + TokenList tokenizedQuery; + int errorCode = 0; + QString errorText; + QStringList resultColumns; + QList resultDataMap; + QList resultDataList; + int currentRow = -1; +}; + +#endif // SQLQUERYANDROID_H diff --git a/Plugins/DbAndroid/sqlresultrowandroid.cpp b/Plugins/DbAndroid/sqlresultrowandroid.cpp new file mode 100644 index 0000000..cc08fa5 --- /dev/null +++ b/Plugins/DbAndroid/sqlresultrowandroid.cpp @@ -0,0 +1,12 @@ +#include "sqlresultrowandroid.h" + +SqlResultRowAndroid::SqlResultRowAndroid(const QVariantHash& resultMap, const QVariantList& resultList) +{ + valuesMap = resultMap; + values = resultList; +} + +SqlResultRowAndroid::~SqlResultRowAndroid() +{ +} + diff --git a/Plugins/DbAndroid/sqlresultrowandroid.h b/Plugins/DbAndroid/sqlresultrowandroid.h new file mode 100644 index 0000000..5acc95a --- /dev/null +++ b/Plugins/DbAndroid/sqlresultrowandroid.h @@ -0,0 +1,13 @@ +#ifndef SQLRESULTROWANDROID_H +#define SQLRESULTROWANDROID_H + +#include "db/sqlresultsrow.h" + +class SqlResultRowAndroid : public SqlResultsRow +{ + public: + SqlResultRowAndroid(const QVariantHash& resultMap, const QVariantList& resultList); + ~SqlResultRowAndroid(); +}; + +#endif // SQLRESULTROWANDROID_H -- cgit v1.2.3