aboutsummaryrefslogtreecommitdiffstats
path: root/Plugins/DbAndroid
diff options
context:
space:
mode:
authorLibravatarUnit 193 <unit193@ubuntu.com>2015-11-25 16:48:49 -0500
committerLibravatarUnit 193 <unit193@ubuntu.com>2015-11-25 16:48:49 -0500
commit7412693e086a7eafaa7ea861164caf523943e5fa (patch)
tree0aee322e40572df306b9813546c7a12b3093bcea /Plugins/DbAndroid
parent640196993d31cf5d6fdf36386990ec05f473a048 (diff)
parent8e640722c62692818ab840d50b3758f89a41a54e (diff)
Merge tag 'upstream/3.0.7'
Upstream version 3.0.7 # gpg: Signature made Wed 25 Nov 2015 04:48:48 PM EST using RSA key ID EBE9BD91 # gpg: Good signature from "Unit 193 <unit193@gmail.com>" # gpg: aka "Unit 193 <unit193@ninthfloor.org>" # gpg: aka "Unit 193 <unit193@ubuntu.com>" # gpg: aka "Unit 193 <unit193@ninthfloor.com>"
Diffstat (limited to 'Plugins/DbAndroid')
-rw-r--r--Plugins/DbAndroid/DbAndroid.pro53
-rw-r--r--Plugins/DbAndroid/adbmanager.cpp426
-rw-r--r--Plugins/DbAndroid/adbmanager.h65
-rw-r--r--Plugins/DbAndroid/dbandroid.cpp209
-rw-r--r--Plugins/DbAndroid/dbandroid.h63
-rw-r--r--Plugins/DbAndroid/dbandroid.json9
-rw-r--r--Plugins/DbAndroid/dbandroid.qrc1
-rw-r--r--Plugins/DbAndroid/dbandroid_global.h12
-rw-r--r--Plugins/DbAndroid/dbandroidconnection.cpp14
-rw-r--r--Plugins/DbAndroid/dbandroidconnection.h44
-rw-r--r--Plugins/DbAndroid/dbandroidconnectionfactory.cpp28
-rw-r--r--Plugins/DbAndroid/dbandroidconnectionfactory.h21
-rw-r--r--Plugins/DbAndroid/dbandroidinstance.cpp143
-rw-r--r--Plugins/DbAndroid/dbandroidinstance.h51
-rw-r--r--Plugins/DbAndroid/dbandroidjsonconnection.cpp430
-rw-r--r--Plugins/DbAndroid/dbandroidjsonconnection.h65
-rw-r--r--Plugins/DbAndroid/dbandroidmode.h13
-rw-r--r--Plugins/DbAndroid/dbandroidpathdialog.cpp579
-rw-r--r--Plugins/DbAndroid/dbandroidpathdialog.h80
-rw-r--r--Plugins/DbAndroid/dbandroidpathdialog.ui244
-rw-r--r--Plugins/DbAndroid/dbandroidshellconnection.cpp363
-rw-r--r--Plugins/DbAndroid/dbandroidshellconnection.h59
-rw-r--r--Plugins/DbAndroid/dbandroidurl.cpp217
-rw-r--r--Plugins/DbAndroid/dbandroidurl.h59
-rw-r--r--Plugins/DbAndroid/sqlqueryandroid.cpp160
-rw-r--r--Plugins/DbAndroid/sqlqueryandroid.h47
-rw-r--r--Plugins/DbAndroid/sqlresultrowandroid.cpp12
-rw-r--r--Plugins/DbAndroid/sqlresultrowandroid.h13
28 files changed, 3480 insertions, 0 deletions
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 <QFileInfo>
+#include <QDebug>
+#include <QTimer>
+#include <QRegularExpression>
+
+AdbManager::AdbManager(DbAndroid* dbAndroidPlugin) :
+ QObject(dbAndroidPlugin), plugin(dbAndroidPlugin)
+{
+ connect(this, SIGNAL(internalDeviceListUpdate(QStringList)), this, SLOT(handleNewDeviceList(QStringList)));
+ connect(this, SIGNAL(deviceDetailsChanged(QList<Device>)), this, SLOT(handleNewDetails(QList<Device>)));
+
+ 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::Device> AdbManager::getDeviceDetails()
+{
+ return currentDeviceDetails.values();
+}
+
+QHash<QString, QPair<int, int>> AdbManager::getForwards()
+{
+ QHash<QString, QPair<int, int>> forwards;
+ QString stdOut;
+ if (!exec(QStringList({"forward", "--list"}), &stdOut))
+ return forwards;
+
+ QRegularExpression re("(.*)\\s+tcp:(\\d+)\\s+tcp:(\\d+)");
+ QRegularExpressionMatch match;
+ QPair<int, int> 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<QString, QPair<int, int>> 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<Device> 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<AdbManager::Device>& 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 <QObject>
+#include <QProcess>
+#include <QtConcurrent/QtConcurrent>
+
+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<Device> getDeviceDetails();
+ QHash<QString,QPair<int,int>> 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<QString,Device> currentDeviceDetails;
+ QFuture<QStringList> updateDevicesFuture;
+
+ private slots:
+ void updateDeviceList();
+ void handleNewDeviceList(const QStringList& devices);
+ void handleNewDetails(const QList<Device>& devices);
+
+ signals:
+ void internalDeviceListUpdate(const QStringList& devices);
+ void deviceListChanged(const QStringList& devices);
+ void deviceDetailsChanged(const QList<Device>& details);
+};
+
+Q_DECLARE_METATYPE(QList<AdbManager::Device>)
+
+#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 <QUrl>
+#include <QDebug>
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QtConcurrent/QtConcurrent>
+
+DbAndroid::DbAndroid()
+{
+}
+
+QString DbAndroid::getLabel() const
+{
+ return "Android SQLite";
+}
+
+bool DbAndroid::checkIfDbServedByPlugin(Db* db) const
+{
+ return (db && dynamic_cast<DbAndroidInstance*>(db));
+}
+
+Db* DbAndroid::getInstance(const QString& name, const QString& path, const QHash<QString, QVariant>& 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<DbPluginOption> DbAndroid::getOptionsList() const
+{
+ QList<DbPluginOption> 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<AdbManager::Device>>("QList<Device>");
+
+ 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. <a href=\"%1\">Click here</a> 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<QString, QVariant>& options, QString* errorMessage);
+ QList<DbPluginOption> 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 @@
+<RCC/>
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 <QtCore/qglobal.h>
+
+#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 <QDebug>
+
+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 <QObject>
+#include <QStringList>
+#include <QHash>
+
+class DbAndroidConnection : public QObject
+{
+ Q_OBJECT
+
+ public:
+ struct ExecutionResult
+ {
+ bool wasError = false;
+ int errorCode = 0;
+ QString errorMsg;
+ QStringList resultColumns;
+ QList<QVariantHash> resultDataMap;
+ QList<QVariantList> 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 <QJsonObject>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QDebug>
+
+DbAndroidInstance::DbAndroidInstance(DbAndroid* plugin, const QString& name, const QString& path, const QHash<QString, QVariant>& 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 <QObject>
+#include <functional>
+#include <QCache>
+
+class DbAndroidConnection;
+class DbAndroid;
+
+class DbAndroidInstance : public AbstractDb
+{
+ Q_OBJECT
+
+ public:
+ typedef std::function<void(const QStringList&)> AsyncDbListResponseHandler;
+
+ DbAndroidInstance(DbAndroid* plugin, const QString& name, const QString& path, const QHash<QString, QVariant>& 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 <QJsonObject>
+#include <QJsonArray>
+#include <QtConcurrent/QtConcurrent>
+
+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<int>(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 <QObject>
+
+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 <QDebug>
+#include <QTimer>
+#include <QPushButton>
+#include <QInputDialog>
+#include <QMessageBox>
+#include <QtConcurrent/QtConcurrent>
+#include <common/userinputfilter.h>
+
+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<Device>)), 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<DbAndroidConnection> 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<DbAndroidConnection> 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<AdbManager::Device> 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<DbAndroidConnection> 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<DbAndroidConnection> 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 <QDialog>
+
+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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>DbAndroidPathDialog</class>
+ <widget class="QDialog" name="DbAndroidPathDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>402</width>
+ <height>546</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>400</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Android database URL</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>Connection method</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QRadioButton" name="usbRadio">
+ <property name="text">
+ <string>USB cable - port forwarding</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="shellRadio">
+ <property name="text">
+ <string>USB cable - sqlite3 command</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="ipRadio">
+ <property name="text">
+ <string>Network (IP address)</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="deviceGroup">
+ <property name="title">
+ <string>Device</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QComboBox" name="deviceCombo"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="ipGroup">
+ <property name="title">
+ <string>IP address</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QLineEdit" name="ipEdit">
+ <property name="maxLength">
+ <number>15</number>
+ </property>
+ <property name="placeholderText">
+ <string notr="true">???.???.???.???</string>
+ </property>
+ <property name="clearButtonEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="portGroup">
+ <property name="title">
+ <string>Port</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <widget class="QSpinBox" name="portSpin">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65535</number>
+ </property>
+ <property name="value">
+ <number>12121</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="passwordGroup">
+ <property name="title">
+ <string>Remote access password</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QLineEdit" name="passwordEdit">
+ <property name="toolTip">
+ <string>&lt;p&gt;This is password configured in the SQLiteStudio service being embeded in the Android application.&lt;/p&gt;</string>
+ </property>
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="appGroup">
+ <property name="title">
+ <string>Application</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QComboBox" name="appCombo"/>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="appFilterEdit">
+ <property name="maximumSize">
+ <size>
+ <width>100</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ <property name="clearButtonEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="databaseGroup">
+ <property name="title">
+ <string>Database</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QComboBox" name="databaseCombo"/>
+ </item>
+ <item>
+ <widget class="QToolButton" name="createDatabaseButton">
+ <property name="toolTip">
+ <string>Create a new database directly on the device.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="deleteDatabaseButton">
+ <property name="toolTip">
+ <string>Delete currently selected database from the device. The currently selected database is the one picked in the list on the left of this button.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>DbAndroidPathDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>DbAndroidPathDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
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 <QMutexLocker>
+
+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<QList<QByteArray>> 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<QStringList> 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<QList<QByteArray>>& deserialized, bool firstHalfForTypes, DbAndroidConnection::ExecutionResult& results)
+{
+ for (const QByteArray& cell : deserialized.first())
+ results.resultColumns << AdbManager::decode(cell);
+
+ QList<QList<QByteArray>> data = deserialized.mid(1); // first row are column names
+ QList<QList<QByteArray>> types;
+ if (firstHalfForTypes)
+ {
+ types = data.mid(data.size() / 2);
+ data = data.mid(0, data.size() / 2);
+
+ QVariantList rowDataList;
+ QVariantHash rowDataMap;
+ QList<QByteArray> rowData;
+ QList<QByteArray> 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<QByteArray>& 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<DataType>(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 <QMutex>
+
+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<QList<QByteArray> >& 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 <QStringList>
+
+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 <QString>
+#include <QUrl>
+
+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 <QDebug>
+
+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<QVariant>& 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<QString, QVariant>& 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 <QJsonDocument>
+
+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<QVariant>& args);
+ bool execInternal(const QHash<QString, QVariant>& 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<QVariantHash> resultDataMap;
+ QList<QVariantList> 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