diff options
Diffstat (limited to 'Plugins/DbAndroid/dbandroidshellconnection.cpp')
| -rw-r--r-- | Plugins/DbAndroid/dbandroidshellconnection.cpp | 363 |
1 files changed, 363 insertions, 0 deletions
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(); + } +} + |
