From 6c9524a8a30e0fa7742a3ef092da494b4e59bbef Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 20 Dec 2025 14:02:06 +0100 Subject: [PATCH] send logs functionality --- android/AndroidManifest.xml | 9 + android/res/xml/file_paths.xml | 5 + .../java/eu/vcmi/vcmi/ActivityLauncher.java | 42 +++++ ios/iOS_utils.h | 6 + ios/iOS_utils.mm | 29 ++++ launcher/aboutProject/aboutproject_moc.cpp | 160 ++++++++++++++++++ launcher/aboutProject/aboutproject_moc.h | 2 + launcher/aboutProject/aboutproject_moc.ui | 13 ++ launcher/helper.cpp | 16 ++ launcher/helper.h | 1 + 10 files changed, 283 insertions(+) create mode 100644 android/res/xml/file_paths.xml diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 729617277..37dfd5889 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -102,6 +102,15 @@ android:process="eu.vcmi.vcmi.srv" android:description="@string/server_name" android:exported="false"/> + + + + + diff --git a/android/res/xml/file_paths.xml b/android/res/xml/file_paths.xml new file mode 100644 index 000000000..8338d6251 --- /dev/null +++ b/android/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityLauncher.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityLauncher.java index b851ea925..c4597b97a 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityLauncher.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityLauncher.java @@ -12,6 +12,12 @@ import android.view.WindowManager; import androidx.annotation.Nullable; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import androidx.core.content.FileProvider; import eu.vcmi.vcmi.VcmiSDLActivity; import eu.vcmi.vcmi.util.FileUtil; @@ -69,4 +75,40 @@ public class ActivityLauncher extends org.qtproject.qt5.android.bindings.QtActiv { startActivity(new Intent(ActivityLauncher.this, VcmiSDLActivity.class)); } + + public void shareFile(String filePath) + { + File src = new File(filePath); + if (!src.exists()) + return; + + // copy to cache so we can share via FileProvider + File dest = new File(getCacheDir(), src.getName()); + try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dest)) + { + byte[] buf = new byte[4096]; + int len; + while ((len = in.read(buf)) != -1) + out.write(buf, 0, len); + } + catch (IOException e) + { + e.printStackTrace(); + return; + } + + try + { + android.net.Uri uri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", dest); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("application/zip"); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(Intent.createChooser(intent, "Share")); + } + catch (Exception e) + { + e.printStackTrace(); + } + } } diff --git a/ios/iOS_utils.h b/ios/iOS_utils.h index 3f1728bb6..da1fea1a0 100644 --- a/ios/iOS_utils.h +++ b/ios/iOS_utils.h @@ -10,6 +10,7 @@ #pragma once #include +#include #pragma GCC visibility push(default) namespace iOS_utils @@ -17,6 +18,11 @@ namespace iOS_utils const char *documentsPath(); const char *cachesPath(); +// share file using system share sheet (e.g. Mail app) +void shareFile(const std::string & filePath); + +std::string iphoneHardwareId(); + #if TARGET_OS_SIMULATOR const char *hostApplicationSupportPath(); #endif diff --git a/ios/iOS_utils.mm b/ios/iOS_utils.mm index 724673f6f..0d53abd2e 100644 --- a/ios/iOS_utils.mm +++ b/ios/iOS_utils.mm @@ -12,6 +12,9 @@ #import #import +#import +#import +#import namespace { @@ -57,4 +60,30 @@ void keepScreenOn(bool isEnabled) { UIApplication.sharedApplication.idleTimerDisabled = isEnabled ? YES : NO; } + +void shareFile(const std::string & filePath) +{ + NSString *nsPath = [NSString stringWithUTF8String:filePath.c_str()]; + if (nsPath == nil) + return; + + NSURL *url = [NSURL fileURLWithPath:nsPath]; + if (!url) return; + + NSArray *items = @[url]; + UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; + + UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController; + if (root.presentedViewController != nil) + [root.presentedViewController presentViewController:controller animated:YES completion:nil]; + else + [root presentViewController:controller animated:YES completion:nil]; +} + +std::string iphoneHardwareId() +{ + struct utsname systemInfo; + uname(&systemInfo); + return std::string(systemInfo.machine); +} } diff --git a/launcher/aboutProject/aboutproject_moc.cpp b/launcher/aboutProject/aboutproject_moc.cpp index aeee4d16c..f71f2984b 100644 --- a/launcher/aboutProject/aboutproject_moc.cpp +++ b/launcher/aboutProject/aboutproject_moc.cpp @@ -11,11 +11,19 @@ #include "aboutproject_moc.h" #include "ui_aboutproject_moc.h" +#if defined(VCMI_ANDROID) +#include +#endif +#if defined(VCMI_IOS) +#include "ios/iOS_utils.h" +#endif + #include "../updatedialog_moc.h" #include "../helper.h" #include "../../lib/GameConstants.h" #include "../../lib/VCMIDirs.h" +#include "../../lib/filesystem/CZipSaver.h" void AboutProjectView::hideAndStretchWidget(QGridLayout * layout, QWidget * toHide, QWidget * toStretch) { @@ -110,3 +118,155 @@ void AboutProjectView::on_pushButtonBugreport_clicked() { QDesktopServices::openUrl(QUrl("https://github.com/vcmi/vcmi/issues")); } + +static QString gatherDeviceInfo() +{ + QString info; + QTextStream ts(&info); + ts << "Operating system: " << QSysInfo::prettyProductName() << " (" << QSysInfo::kernelVersion() << ")" << '\n'; + ts << "Product name: " << QSysInfo::prettyProductName() << '\n'; + ts << "CPU architecture: " << QSysInfo::currentCpuArchitecture() << '\n'; + ts << "Qt version: " << QT_VERSION_STR << '\n'; +#if defined(VCMI_ANDROID) + QString model = QAndroidJniObject::getStaticObjectField( + "android/os/Build", + "MODEL", + "Ljava/lang/String;" + ).toString(); + QString manufacturer = QAndroidJniObject::getStaticObjectField( + "android/os/Build", + "MANUFACTURER", + "Ljava/lang/String;" + ).toString(); + ts << "Device model: " << model << '\n'; + ts << "Manufacturer: " << manufacturer << '\n'; +#endif +#if defined(VCMI_IOS) + ts << "Device model: " << QString::fromStdString(iOS_utils::iphoneHardwareId()) << '\n'; +#endif + return info; +} + +void AboutProjectView::on_pushButtonSendLogs_clicked() +{ + QDir tempDir(ui->lineEditTempDir->text()); + +#if defined(VCMI_ANDROID) || defined(VCMI_IOS) + // cleanup old temp archives from previous runs (delete now) + { + QDir tdir(QDir::tempPath()); + const QFileInfoList old = tdir.entryInfoList(QStringList() << "vcmi-logs-*.zip", QDir::Files, QDir::Name); + for (const QFileInfo & fi : old) + QFile::remove(fi.absoluteFilePath()); + } + // On mobile: write archive to system temp and send via platform share (no save dialog) + const QString tmpDir = QDir::tempPath(); + const QString outPath = QDir(tmpDir).filePath(QString("vcmi-logs-%1.zip").arg(QString::number(QDateTime::currentMSecsSinceEpoch()))); +#else + const QString defaultName = tempDir.filePath("vcmi-logs.zip"); + QString outPath = QFileDialog::getSaveFileName(this, tr("Save logs"), defaultName, tr("Zip archives (*.zip)")); + if (outPath.isEmpty()) + return; + + if (!outPath.endsWith(".zip", Qt::CaseInsensitive)) + outPath += ".zip"; +#endif + + QFileInfoList files = tempDir.entryInfoList({ "*.txt" }, QDir::Files, QDir::Name); + files.append(QDir(ui->lineEditConfigDir->text()).entryInfoList({ "*.json", "*.ini" }, QDir::Files, QDir::Name)); + + // build data dir file/folder listing and add as a virtual text file + const QString dataDirPath = ui->lineEditUserDataDir->text(); + QString listing; + QDir dataDir(dataDirPath); + QDirIterator it(dataDirPath, QDir::NoDotAndDotDot | QDir::AllEntries, QDirIterator::Subdirectories); + QTextStream ts(&listing); + while (it.hasNext()) + { + const QString path = it.next(); + const QString rel = dataDir.relativeFilePath(path); + QFileInfo info(path); + + if (rel.startsWith(QLatin1String("Saves/"))) + continue; + + if (info.isDir()) + ts << QChar('D') << QLatin1Char(' ') << rel << '\n'; + else + ts << QChar('F') << QLatin1Char(' ') << rel << QLatin1String(" (") << info.size() << QLatin1String(")") << '\n'; + } + + try + { + // create zip and add .txt files + std::shared_ptr api = std::make_shared(); + boost::filesystem::path archivePath(outPath.toStdString()); + CZipSaver saver(api, archivePath); + + for (const QFileInfo & fi : files) + { + QFile f(fi.absoluteFilePath()); + if (!f.open(QIODevice::ReadOnly)) + continue; + + QByteArray data = f.readAll(); + // If JSON, sanitize sensitive fields before packing + QByteArray toWrite = data; + if (fi.suffix().toLower() == "json") + { + QStringList lines = QString::fromUtf8(data).split('\n'); + const QStringList keys = { "accountCookie", "accountID", "displayName" }; + for (QString &line : lines) + { + for (const QString &key : keys) + { + if (line.contains(QRegularExpression(QString("\\b%1\\b").arg(QRegularExpression::escape(key))))) + { + QRegularExpression re("^(\\s*)"); + auto m = re.match(line); + QString indent = m.hasMatch() ? m.captured(1) : QString(); + line = indent + QString("// [removed %1]").arg(key); + break; + } + } + } + toWrite = lines.join('\n').toUtf8(); + } + + auto stream = saver.addFile(fi.fileName().toStdString()); + stream->write(reinterpret_cast(toWrite.constData()), toWrite.size()); + } + + // add generated listing as game-directory-structure.txt + if (!listing.isEmpty()) + { + QByteArray data = listing.toUtf8(); + auto stream = saver.addFile(std::string("data-directory-structure.txt")); + stream->write(reinterpret_cast(data.constData()), data.size()); + } + + // add device information as device-info.txt + { + const QString deviceInfo = gatherDeviceInfo(); + if (!deviceInfo.isEmpty()) + { + QByteArray dataDev = deviceInfo.toUtf8(); + auto streamDev = saver.addFile(std::string("device-info.txt")); + streamDev->write(reinterpret_cast(dataDev.constData()), dataDev.size()); + } + } + } + catch (const std::exception & e) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to create archive: %1").arg(QString::fromUtf8(e.what()))); + return; + } + // On mobile platforms, send file via platform and remove temporary file afterwards. +#if defined(VCMI_ANDROID) || defined(VCMI_IOS) + QMessageBox::information(this, tr("Send logs"), tr("The archive will be sent via another application. Share your logs e.g. over discord to developers.")); + Helper::sendFileToApp(outPath); +#else + // desktop: notify user and do not auto-send + QMessageBox::information(this, tr("Success"), tr("Logs saved to %1, please send them to the developers").arg(outPath)); +#endif +} diff --git a/launcher/aboutProject/aboutproject_moc.h b/launcher/aboutProject/aboutproject_moc.h index dda196905..45fe4a31b 100644 --- a/launcher/aboutProject/aboutproject_moc.h +++ b/launcher/aboutProject/aboutproject_moc.h @@ -47,6 +47,8 @@ private slots: void on_pushButtonBugreport_clicked(); + void on_pushButtonSendLogs_clicked(); + void on_openConfigDir_clicked(); private: diff --git a/launcher/aboutProject/aboutproject_moc.ui b/launcher/aboutProject/aboutproject_moc.ui index f582201d3..f603d0917 100644 --- a/launcher/aboutProject/aboutproject_moc.ui +++ b/launcher/aboutProject/aboutproject_moc.ui @@ -304,6 +304,19 @@ + + + + + 200 + 0 + + + + Send logs + + + diff --git a/launcher/helper.cpp b/launcher/helper.cpp index bc5cbf2f5..23a961792 100644 --- a/launcher/helper.cpp +++ b/launcher/helper.cpp @@ -145,4 +145,20 @@ void keepScreenOn(bool isEnabled) iOS_utils::keepScreenOn(isEnabled); #endif } + +void sendFileToApp(QString path) +{ +#if defined(VCMI_ANDROID) + // delegate to Android activity which will copy to cache and share via FileProvider + auto jstr = QAndroidJniObject::fromString(path); + QtAndroid::runOnAndroidThread([jstr]() mutable { + QtAndroid::androidActivity().callMethod("shareFile", "(Ljava/lang/String;)V", jstr.object()); + }); +#elif defined(VCMI_IOS) + // use iOS share sheet + iOS_utils::shareFile(path.toStdString()); +#else + Q_UNUSED(path); +#endif +} } diff --git a/launcher/helper.h b/launcher/helper.h index bb515d08e..ad86d339f 100644 --- a/launcher/helper.h +++ b/launcher/helper.h @@ -24,4 +24,5 @@ bool performNativeCopy(QString src, QString dst); void revealDirectoryInFileBrowser(QString path); MainWindow * getMainWindow(); void keepScreenOn(bool isEnabled); +void sendFileToApp(QString path); }