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);
}