helper.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. /*
  2. * helper.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "helper.h"
  12. #include "mainwindow_moc.h"
  13. #include "settingsView/csettingsview_moc.h"
  14. #include "modManager/cmodlistview_moc.h"
  15. #include "../lib/CConfigHandler.h"
  16. #include <QObject>
  17. #include <QScroller>
  18. #ifdef VCMI_ANDROID
  19. #include <QAndroidJniObject>
  20. #include <QtAndroid>
  21. #include <QAndroidJniEnvironment>
  22. #include <QAndroidActivityResultReceiver>
  23. #endif
  24. #ifdef VCMI_IOS
  25. #include "ios/revealdirectoryinfiles.h"
  26. #include "ios/selectdirectory.h"
  27. #include "iOS_utils.h"
  28. #endif
  29. #ifdef VCMI_MOBILE
  30. static QScrollerProperties generateScrollerProperties()
  31. {
  32. QScrollerProperties result;
  33. result.setScrollMetric(QScrollerProperties::OvershootDragResistanceFactor, 0.25);
  34. result.setScrollMetric(QScrollerProperties::OvershootDragDistanceFactor, 0.25);
  35. result.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
  36. return result;
  37. }
  38. #endif
  39. #ifdef VCMI_ANDROID
  40. static QString safeEncode(QString uri)
  41. {
  42. // %-encode unencoded parts of string.
  43. // This is needed because Qt returns a mixed content url with %-encoded and unencoded parts. On Android >= 13 this causes problems reading these files, when using spaces and unicode characters in folder or filename.
  44. // Only these should be encoded (other typically %-encoded chars should not be encoded because this leads to errors).
  45. // Related, but seems not completly fixed (at least in our setup): https://bugreports.qt.io/browse/QTBUG-114435
  46. if (!uri.startsWith("content://", Qt::CaseInsensitive))
  47. return uri;
  48. return QString::fromUtf8(QUrl::toPercentEncoding(uri, "!#$&'()*+,/:;=?@[]<>{}\"`^~%"));
  49. }
  50. #endif
  51. namespace Helper
  52. {
  53. void loadSettings()
  54. {
  55. settings.init("config/settings.json", "vcmi:settings");
  56. persistentStorage.init("config/persistentStorage.json", "");
  57. }
  58. void reLoadSettings()
  59. {
  60. loadSettings();
  61. for(const auto widget : qApp->allWidgets())
  62. if(auto settingsView = qobject_cast<CSettingsView *>(widget))
  63. {
  64. settingsView->loadSettings();
  65. break;
  66. }
  67. getMainWindow()->updateTranslation();
  68. getMainWindow()->getModView()->reload();
  69. }
  70. void enableScrollBySwiping(QObject * scrollTarget)
  71. {
  72. #ifdef VCMI_MOBILE
  73. QScroller::grabGesture(scrollTarget, QScroller::LeftMouseButtonGesture);
  74. QScroller * scroller = QScroller::scroller(scrollTarget);
  75. scroller->setScrollerProperties(generateScrollerProperties());
  76. #endif
  77. }
  78. QString getRealPath(QString path)
  79. {
  80. #ifdef VCMI_ANDROID
  81. if(path.contains("content://", Qt::CaseInsensitive))
  82. {
  83. auto str = QAndroidJniObject::fromString(safeEncode(path));
  84. return QAndroidJniObject::callStaticObjectMethod("eu/vcmi/vcmi/util/FileUtil", "getFilenameFromUri", "(Ljava/lang/String;Landroid/content/Context;)Ljava/lang/String;", str.object<jstring>(), QtAndroid::androidContext().object()).toString();
  85. }
  86. return path;
  87. #else
  88. return path;
  89. #endif
  90. }
  91. bool performNativeCopy(QString src, QString dst)
  92. {
  93. #ifdef VCMI_ANDROID
  94. const bool srcIsContent = src.startsWith("content://", Qt::CaseInsensitive);
  95. const bool dstIsContent = dst.startsWith("content://", Qt::CaseInsensitive);
  96. if(srcIsContent || dstIsContent)
  97. {
  98. const QAndroidJniObject jSrc = QAndroidJniObject::fromString(srcIsContent ? safeEncode(src) : src);
  99. const QAndroidJniObject jDst = QAndroidJniObject::fromString(dstIsContent ? safeEncode(dst) : dst);
  100. QAndroidJniObject::callStaticMethod<void>("eu/vcmi/vcmi/util/FileUtil", "copyFileFromUri", "(Ljava/lang/String;Ljava/lang/String;Landroid/content/Context;)V", jSrc.object<jstring>(), jDst.object<jstring>(), QtAndroid::androidContext().object());
  101. return QFileInfo(dst).exists();
  102. }
  103. #endif
  104. // Pure filesystem -> use Qt copy
  105. QFile::remove(dst);
  106. return QFile::copy(src, dst);
  107. }
  108. void revealDirectoryInFileBrowser(QString path)
  109. {
  110. const auto dirUrl = QUrl::fromLocalFile(QFileInfo{path}.absoluteFilePath());
  111. #ifdef VCMI_IOS
  112. iOS_utils::revealDirectoryInFiles(dirUrl);
  113. #else
  114. QDesktopServices::openUrl(dirUrl);
  115. #endif
  116. }
  117. MainWindow * getMainWindow()
  118. {
  119. foreach(QWidget *w, qApp->allWidgets())
  120. if(auto mainWin = qobject_cast<MainWindow*>(w))
  121. return mainWin;
  122. return nullptr;
  123. }
  124. void keepScreenOn(bool isEnabled)
  125. {
  126. #if defined(VCMI_ANDROID)
  127. QtAndroid::runOnAndroidThread([isEnabled]
  128. {
  129. QtAndroid::androidActivity().callMethod<void>("keepScreenOn", "(Z)V", isEnabled);
  130. });
  131. #elif defined(VCMI_IOS)
  132. iOS_utils::keepScreenOn(isEnabled);
  133. #endif
  134. }
  135. bool canUseFolderPicker()
  136. {
  137. #if defined(VCMI_ANDROID)
  138. // Folder picker is available on API >= 21.
  139. // Android/Google TV usually lacks DocumentsUI — hide this option.
  140. QAndroidJniObject context = QtAndroid::androidContext();
  141. QAndroidJniObject uiModeMgr = context.callObjectMethod("getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;", QAndroidJniObject::fromString("uimode").object<jstring>());
  142. if(uiModeMgr.isValid())
  143. {
  144. jint mode = uiModeMgr.callMethod<jint>("getCurrentModeType", "()I");
  145. jint TV = QAndroidJniObject::getStaticField<jint>("android/content/res/Configuration", "UI_MODE_TYPE_TELEVISION");
  146. if(mode == TV)
  147. return false;
  148. }
  149. return true;
  150. #elif defined(VCMI_IOS)
  151. // selecting directory through UIDocumentPickerViewController is available only since iOS 13
  152. return iOS_utils::isOsVersionAtLeast(13);
  153. #else
  154. return true;
  155. #endif
  156. }
  157. #ifdef VCMI_ANDROID
  158. // Request code for Android folder picker (ACTION_OPEN_DOCUMENT_TREE).
  159. // Value is arbitrary, used only to match activity result callback.
  160. static constexpr int kFolderPickerReqCode = 4242;
  161. static jint intentFlags()
  162. {
  163. const jint fRead = QAndroidJniObject::getStaticField<jint>("android/content/Intent", "FLAG_GRANT_READ_URI_PERMISSION");
  164. const jint fWrite = QAndroidJniObject::getStaticField<jint>("android/content/Intent", "FLAG_GRANT_WRITE_URI_PERMISSION");
  165. const jint fPersist = QAndroidJniObject::getStaticField<jint>("android/content/Intent", "FLAG_GRANT_PERSISTABLE_URI_PERMISSION");
  166. const jint fPrefix = QAndroidJniObject::getStaticField<jint>("android/content/Intent", "FLAG_GRANT_PREFIX_URI_PERMISSION");
  167. return fRead | fWrite | fPersist | fPrefix;
  168. }
  169. class FolderPickReceiver final : public QAndroidActivityResultReceiver
  170. {
  171. public:
  172. std::function<void(QString)> onDone;
  173. // One-shot result handler for ACTION_OPEN_DOCUMENT_TREE
  174. void handleActivityResult(int req, int res, const QAndroidJniObject &data) override
  175. {
  176. auto cb = std::exchange(onDone, {}); // guarantee single-use
  177. if(!cb)
  178. return;
  179. if(req != kFolderPickerReqCode || res != -1 /*RESULT_OK*/ || !data.isValid())
  180. {
  181. QMetaObject::invokeMethod(qApp, [cb]{ if (cb) cb({}); }, Qt::QueuedConnection);
  182. return;
  183. }
  184. // Always return content:// tree URI
  185. const QAndroidJniObject uri = data.callObjectMethod("getData","()Landroid/net/Uri;");
  186. const QAndroidJniObject us = uri.callObjectMethod("toString","()Ljava/lang/String;");
  187. const QString pickedTree = us.toString();
  188. // Persist read+write permission
  189. const QAndroidJniObject ctx = QtAndroid::androidContext();
  190. const QAndroidJniObject cr = ctx.callObjectMethod("getContentResolver","()Landroid/content/ContentResolver;");
  191. cr.callMethod<void>("takePersistableUriPermission", "(Landroid/net/Uri;I)V", uri.object<jobject>(), jint(1 | 2));
  192. // Bounce back to Qt thread
  193. QMetaObject::invokeMethod(qApp, [cb, pickedTree]{ if (cb) cb(pickedTree); }, Qt::QueuedConnection);
  194. }
  195. };
  196. static FolderPickReceiver g_receiver;
  197. #endif
  198. void nativeFolderPicker(QWidget *parent, std::function<void(QString)>&& cb)
  199. {
  200. if(!cb)
  201. return;
  202. #if defined(VCMI_ANDROID)
  203. Q_UNUSED(parent);
  204. g_receiver.onDone = std::move(cb);
  205. QAndroidJniObject intent("android/content/Intent","()V");
  206. intent.callObjectMethod("setAction", "(Ljava/lang/String;)Landroid/content/Intent;", QAndroidJniObject::fromString("android.intent.action.OPEN_DOCUMENT_TREE").object<jstring>());
  207. intent.callObjectMethod("addFlags", "(I)Landroid/content/Intent;", intentFlags());
  208. QtAndroid::startActivity(intent, kFolderPickerReqCode, &g_receiver);
  209. #elif defined(VCMI_IOS)
  210. SelectDirectory iosDirectorySelector;
  211. const QString dir = iosDirectorySelector.getExistingDirectory();
  212. cb(dir);
  213. #else
  214. const QString dir = QFileDialog::getExistingDirectory(parent, {}, {}, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
  215. cb(dir);
  216. #endif
  217. }
  218. static inline QString classifyTargetByExt(const QString &baseName)
  219. {
  220. // Case-insensitive suffix checks without making a lowercase copy
  221. auto ends = [&](const char *s){ return baseName.endsWith(QLatin1String(s), Qt::CaseInsensitive); };
  222. if(ends(".lod") || ends(".snd") || ends(".vid") || ends(".pak"))
  223. return QStringLiteral("Data");
  224. if(ends(".h3m"))
  225. return QStringLiteral("Maps");
  226. if(ends(".mp3"))
  227. return QStringLiteral("Mp3");
  228. return {};
  229. }
  230. static void addIfExists(QVector<QDir> &scan, const QDir &base, const char *child)
  231. {
  232. QDir dir(base.filePath(QLatin1String(child)));
  233. if(dir.exists())
  234. scan << dir;
  235. }
  236. QStringList findFilesForCopy(const QString &path)
  237. {
  238. #ifdef VCMI_ANDROID
  239. if(path.startsWith(QLatin1String("content://"), Qt::CaseInsensitive))
  240. {
  241. const QAndroidJniObject jUri = QAndroidJniObject::fromString(safeEncode(path));
  242. const QAndroidJniObject jArr = QAndroidJniObject::callStaticObjectMethod("eu/vcmi/vcmi/util/FileUtil", "findFilesForCopy", "(Ljava/lang/String;Landroid/content/Context;)[Ljava/lang/String;", jUri.object<jstring>(), QtAndroid::androidContext().object());
  243. QStringList out;
  244. if(!jArr.isValid())
  245. return out;
  246. QAndroidJniEnvironment env;
  247. const jobjectArray arr = static_cast<jobjectArray>(jArr.object<jobject>());
  248. const jsize n = env->GetArrayLength(arr);
  249. out.reserve(n);
  250. for(jsize i = 0; i < n; ++i)
  251. {
  252. QAndroidJniObject s((jstring)env->GetObjectArrayElement(arr, i));
  253. out.push_back(s.toString()); // "src \t Target \t Name"
  254. }
  255. return out;
  256. }
  257. #endif
  258. // Non-Android, or Android with real FS path
  259. QStringList out;
  260. QDir root(path);
  261. if(!root.exists())
  262. return out;
  263. // Build list of directories to scan
  264. QVector<QDir> scan;
  265. scan << root;
  266. // If user picked "Data", also scan ../Maps and ../Mp3 (if present)
  267. if(root.dirName().compare(QLatin1String("Data"), Qt::CaseInsensitive) == 0)
  268. {
  269. QDir parent = root;
  270. if(parent.cdUp())
  271. {
  272. addIfExists(scan, parent, "Maps");
  273. addIfExists(scan, parent, "Mp3");
  274. }
  275. }
  276. // Depth-first traversal on each directory; classify by extension
  277. for(const QDir &dir : scan)
  278. {
  279. QDirIterator it(dir.absolutePath(), QDir::Files | QDir::Readable | QDir::NoSymLinks, QDirIterator::Subdirectories);
  280. while(it.hasNext())
  281. {
  282. const QString filePath = it.next();
  283. const QFileInfo file(filePath);
  284. const QString target = classifyTargetByExt(file.fileName());
  285. if(target.isEmpty())
  286. continue;
  287. out.push_back(filePath + QLatin1Char('\t') + target + QLatin1Char('\t') + file.fileName()); // "src \t Target \t Name"
  288. }
  289. }
  290. return out;
  291. }
  292. void sendFileToApp(QString path)
  293. {
  294. #if defined(VCMI_ANDROID)
  295. // delegate to Android activity which will copy to cache and share via FileProvider
  296. auto jstr = QAndroidJniObject::fromString(path);
  297. QtAndroid::runOnAndroidThread([jstr]() mutable {
  298. QtAndroid::androidActivity().callMethod<void>("shareFile", "(Ljava/lang/String;)V", jstr.object<jstring>());
  299. });
  300. #elif defined(VCMI_IOS)
  301. // use iOS share sheet
  302. iOS_utils::shareFile(path.toStdString());
  303. #else
  304. Q_UNUSED(path);
  305. #endif
  306. }
  307. }