mirror of
https://github.com/vcmi/vcmi.git
synced 2026-06-19 22:57:37 +02:00
send logs functionality
This commit is contained in:
@@ -102,6 +102,15 @@
|
|||||||
android:process="eu.vcmi.vcmi.srv"
|
android:process="eu.vcmi.vcmi.srv"
|
||||||
android:description="@string/server_name"
|
android:description="@string/server_name"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<!-- FileProvider for sharing files from app cache -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- allow sharing files from app cache -->
|
||||||
|
<cache-path name="cache" path="." />
|
||||||
|
</paths>
|
||||||
@@ -12,6 +12,12 @@ import android.view.WindowManager;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
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.VcmiSDLActivity;
|
||||||
import eu.vcmi.vcmi.util.FileUtil;
|
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));
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <TargetConditionals.h>
|
#include <TargetConditionals.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#pragma GCC visibility push(default)
|
#pragma GCC visibility push(default)
|
||||||
namespace iOS_utils
|
namespace iOS_utils
|
||||||
@@ -17,6 +18,11 @@ namespace iOS_utils
|
|||||||
const char *documentsPath();
|
const char *documentsPath();
|
||||||
const char *cachesPath();
|
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
|
#if TARGET_OS_SIMULATOR
|
||||||
const char *hostApplicationSupportPath();
|
const char *hostApplicationSupportPath();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <MobileCoreServices/MobileCoreServices.h>
|
||||||
|
#import <objc/message.h>
|
||||||
|
#import <sys/utsname.h>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -57,4 +60,30 @@ void keepScreenOn(bool isEnabled)
|
|||||||
{
|
{
|
||||||
UIApplication.sharedApplication.idleTimerDisabled = isEnabled ? YES : NO;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,19 @@
|
|||||||
#include "aboutproject_moc.h"
|
#include "aboutproject_moc.h"
|
||||||
#include "ui_aboutproject_moc.h"
|
#include "ui_aboutproject_moc.h"
|
||||||
|
|
||||||
|
#if defined(VCMI_ANDROID)
|
||||||
|
#include <QAndroidJniObject>
|
||||||
|
#endif
|
||||||
|
#if defined(VCMI_IOS)
|
||||||
|
#include "ios/iOS_utils.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "../updatedialog_moc.h"
|
#include "../updatedialog_moc.h"
|
||||||
#include "../helper.h"
|
#include "../helper.h"
|
||||||
|
|
||||||
#include "../../lib/GameConstants.h"
|
#include "../../lib/GameConstants.h"
|
||||||
#include "../../lib/VCMIDirs.h"
|
#include "../../lib/VCMIDirs.h"
|
||||||
|
#include "../../lib/filesystem/CZipSaver.h"
|
||||||
|
|
||||||
void AboutProjectView::hideAndStretchWidget(QGridLayout * layout, QWidget * toHide, QWidget * toStretch)
|
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"));
|
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<CIOApi> api = std::make_shared<CDefaultIOApi>();
|
||||||
|
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<const ui8 *>(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<const ui8 *>(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<const ui8 *>(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
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ private slots:
|
|||||||
|
|
||||||
void on_pushButtonBugreport_clicked();
|
void on_pushButtonBugreport_clicked();
|
||||||
|
|
||||||
|
void on_pushButtonSendLogs_clicked();
|
||||||
|
|
||||||
void on_openConfigDir_clicked();
|
void on_openConfigDir_clicked();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -304,6 +304,19 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pushButtonSendLogs">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>200</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Send logs</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="horizontalSpacer">
|
<spacer name="horizontalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
|||||||
@@ -145,4 +145,20 @@ void keepScreenOn(bool isEnabled)
|
|||||||
iOS_utils::keepScreenOn(isEnabled);
|
iOS_utils::keepScreenOn(isEnabled);
|
||||||
#endif
|
#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<void>("shareFile", "(Ljava/lang/String;)V", jstr.object<jstring>());
|
||||||
|
});
|
||||||
|
#elif defined(VCMI_IOS)
|
||||||
|
// use iOS share sheet
|
||||||
|
iOS_utils::shareFile(path.toStdString());
|
||||||
|
#else
|
||||||
|
Q_UNUSED(path);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ bool performNativeCopy(QString src, QString dst);
|
|||||||
void revealDirectoryInFileBrowser(QString path);
|
void revealDirectoryInFileBrowser(QString path);
|
||||||
MainWindow * getMainWindow();
|
MainWindow * getMainWindow();
|
||||||
void keepScreenOn(bool isEnabled);
|
void keepScreenOn(bool isEnabled);
|
||||||
|
void sendFileToApp(QString path);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user