소스 검색

Merge pull request #5198 from Laserlicht/android_native_copy

[1.6.x] Android native copy files
Ivan Savenko 9 달 전
부모
커밋
d825868e26

+ 48 - 0
android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java

@@ -1,18 +1,25 @@
 package eu.vcmi.vcmi.util;
 package eu.vcmi.vcmi.util;
 
 
 import android.app.Activity;
 import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
 import android.net.Uri;
 import android.net.Uri;
+import android.provider.OpenableColumns;
 
 
 import androidx.annotation.Nullable;
 import androidx.annotation.Nullable;
 import androidx.documentfile.provider.DocumentFile;
 import androidx.documentfile.provider.DocumentFile;
 
 
 import java.io.File;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStream;
+import java.lang.Exception;
 import java.util.List;
 import java.util.List;
 
 
+import eu.vcmi.vcmi.Const;
 import eu.vcmi.vcmi.Storage;
 import eu.vcmi.vcmi.Storage;
 
 
 /**
 /**
@@ -104,4 +111,45 @@ public class FileUtil
             target.write(buffer, 0, read);
             target.write(buffer, 0, read);
         }
         }
     }
     }
+
+    @SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
+    private static void copyFileFromUri(String sourceFileUri, String destinationFile, Context context)
+    {
+        try
+        {
+            final InputStream inputStream = new FileInputStream(context.getContentResolver().openFileDescriptor(Uri.parse(sourceFileUri), "r").getFileDescriptor());
+            final OutputStream outputStream = new FileOutputStream(new File(destinationFile));
+
+            copyStream(inputStream, outputStream);
+        }
+        catch (IOException e)
+        {
+            Log.e("copyFileFromUri failed: ", e);
+        }
+    }
+
+    @SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
+    private static String getFilenameFromUri(String sourceFileUri, Context context)
+    {
+        String fileName = "";
+        try
+        {
+            ContentResolver contentResolver = context.getContentResolver();
+            Cursor cursor = contentResolver.query(Uri.parse(sourceFileUri), null, null, null, null);
+
+            if (cursor != null && cursor.moveToFirst()) {
+                int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+                if (nameIndex != -1) {
+                    fileName = cursor.getString(nameIndex);
+                }
+                cursor.close();
+            }
+        }
+        catch (Exception e)
+        {
+            Log.e("getFilenameFromUri failed: ", e);
+        }
+
+        return fileName;
+    }
 }
 }

+ 40 - 0
docker/BuildAndroid-aarch64.dockerfile

@@ -0,0 +1,40 @@
+FROM ubuntu:noble
+WORKDIR /usr/local/app
+
+RUN apt-get update && apt-get install -y openjdk-17-jdk python3 pipx cmake ccache ninja-build wget git xz-utils
+
+ENV PIPX_HOME="/opt/pipx"
+ENV PIPX_BIN_DIR="/usr/local/bin"
+ENV PIPX_MAN_DIR="/usr/local/share/man"
+RUN pipx install 'conan<2.0'
+RUN pipx install 'sdkmanager==0.6.10'
+
+RUN conan profile new conan --detect
+
+RUN wget https://github.com/vcmi/vcmi-dependencies/releases/download/1.3/dependencies-android-arm64-v8a.txz
+RUN tar -xf dependencies-android-arm64-v8a.txz -C ~/.conan
+RUN rm dependencies-android-arm64-v8a.txz
+
+ENV JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
+ENV ANDROID_HOME="/usr/lib/android-sdk"
+ENV GRADLE_USER_HOME="/vcmi/.cache/grandle"
+ENV GENERATE_ONLY_BUILT_CONFIG=1
+
+RUN sdkmanager --install "platform-tools"
+RUN sdkmanager --install "platforms;android-34"
+RUN yes | sdkmanager --licenses
+
+RUN conan download android-ndk/r25c@:4db1be536558d833e52e862fd84d64d75c2b3656 -r conancenter
+
+CMD ["sh", "-c", " \
+    # switch to mounted dir
+    cd /vcmi ; \
+    # install conan stuff
+    conan install . --install-folder=conan-generated --no-imports --build=never --profile:build=default --profile:host=CI/conan/android-64-ndk ; \
+    # link conan ndk that grandle can find it
+    mkdir -p /usr/lib/android-sdk/ndk ; \
+    ln -s -T ~/.conan/data/android-ndk/r25c/_/_/package/4db1be536558d833e52e862fd84d64d75c2b3656/bin /usr/lib/android-sdk/ndk/25.2.9519653 ; \
+    # build
+    cmake --preset android-daily-release ; \
+    cmake --build --preset android-daily-release \
+"]

+ 14 - 0
docs/developers/Building_Android.md

@@ -68,3 +68,17 @@ cmake --build ../build
 ```
 ```
 
 
 You can also see a more detailed walkthrough on CMake configuration at [How to build VCMI (macOS)](./Building_macOS.md).
 You can also see a more detailed walkthrough on CMake configuration at [How to build VCMI (macOS)](./Building_macOS.md).
+
+## Docker
+
+For developing it's also possible to use Docker to build android APK. The only requirement is to have Docker installed. The container image contains all the other prerequisites.
+
+To build using docker just open a terminal with `vcmi` as working directory.
+
+Build the image with (only needed once):
+`docker build -f docker/BuildAndroid-aarch64.dockerfile -t vcmi-android-build .`
+
+After building the image you can compile vcmi with:
+`docker run -it --rm -v $PWD/:/vcmi vcmi-android-build`
+
+The current dockerfile is aarch64 only but can adjusted manually for armv7.

+ 7 - 3
launcher/firstLaunch/firstlaunch_moc.cpp

@@ -369,8 +369,9 @@ void FirstLaunchView::extractGogData()
 
 
 		QString tmpFileExe = tempDir.filePath("h3_gog.exe");
 		QString tmpFileExe = tempDir.filePath("h3_gog.exe");
 		QString tmpFileBin = tempDir.filePath("h3_gog-1.bin");
 		QString tmpFileBin = tempDir.filePath("h3_gog-1.bin");
-		QFile(fileExe).copy(tmpFileExe);
-		QFile(fileBin).copy(tmpFileBin);
+
+		Helper::performNativeCopy(fileExe, tmpFileExe);
+		Helper::performNativeCopy(fileBin, tmpFileBin);
 
 
 		logGlobal->info("Installing exe '%s' ('%s')", tmpFileExe.toStdString(), fileExe.toStdString());
 		logGlobal->info("Installing exe '%s' ('%s')", tmpFileExe.toStdString(), fileExe.toStdString());
 		logGlobal->info("Installing bin '%s' ('%s')", tmpFileBin.toStdString(), fileBin.toStdString());
 		logGlobal->info("Installing bin '%s' ('%s')", tmpFileBin.toStdString(), fileBin.toStdString());
@@ -414,9 +415,13 @@ void FirstLaunchView::extractGogData()
 		{
 		{
 			if(!errorText.isEmpty())
 			if(!errorText.isEmpty())
 			{
 			{
+				logGlobal->error("Gog installer extraction failure! Reason: %s", errorText.toStdString());
 				QMessageBox::critical(this, tr("Extracting error!"), errorText, QMessageBox::Ok, QMessageBox::Ok);
 				QMessageBox::critical(this, tr("Extracting error!"), errorText, QMessageBox::Ok, QMessageBox::Ok);
 				if(!hashError.isEmpty())
 				if(!hashError.isEmpty())
+				{
+					logGlobal->error("Hash error: %s", hashError.toStdString());
 					QMessageBox::critical(this, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok);
 					QMessageBox::critical(this, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok);
+				}
 			}
 			}
 			else
 			else
 				QMessageBox::critical(this, tr("No Heroes III data!"), tr("Selected files do not contain Heroes III data!"), QMessageBox::Ok, QMessageBox::Ok);
 				QMessageBox::critical(this, tr("No Heroes III data!"), tr("Selected files do not contain Heroes III data!"), QMessageBox::Ok, QMessageBox::Ok);
@@ -641,4 +646,3 @@ void FirstLaunchView::on_pushButtonGithub_clicked()
 {
 {
 	QDesktopServices::openUrl(QUrl("https://github.com/vcmi/vcmi"));
 	QDesktopServices::openUrl(QUrl("https://github.com/vcmi/vcmi"));
 }
 }
-

+ 32 - 1
launcher/helper.cpp

@@ -1,5 +1,5 @@
 /*
 /*
- * jsonutils.cpp, part of VCMI engine
+ * helper.cpp, part of VCMI engine
  *
  *
  * Authors: listed in file AUTHORS in main folder
  * Authors: listed in file AUTHORS in main folder
  *
  *
@@ -15,6 +15,11 @@
 #include <QObject>
 #include <QObject>
 #include <QScroller>
 #include <QScroller>
 
 
+#ifdef VCMI_ANDROID
+#include <QAndroidJniObject>
+#include <QtAndroid>
+#endif
+
 #ifdef VCMI_MOBILE
 #ifdef VCMI_MOBILE
 static QScrollerProperties generateScrollerProperties()
 static QScrollerProperties generateScrollerProperties()
 {
 {
@@ -44,4 +49,30 @@ void enableScrollBySwiping(QObject * scrollTarget)
 	scroller->setScrollerProperties(generateScrollerProperties());
 	scroller->setScrollerProperties(generateScrollerProperties());
 #endif
 #endif
 }
 }
+
+QString getRealPath(QString path)
+{
+#ifdef VCMI_ANDROID
+	if(path.contains("content://", Qt::CaseInsensitive))
+	{
+		auto str = QAndroidJniObject::fromString(path);
+		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();
+	}
+	else
+		return path;
+#else
+	return path;
+#endif
+}
+
+void performNativeCopy(QString src, QString dst)
+{
+#ifdef VCMI_ANDROID
+	auto srcStr = QAndroidJniObject::fromString(src);
+	auto dstStr = QAndroidJniObject::fromString(dst);
+	QAndroidJniObject::callStaticObjectMethod("eu/vcmi/vcmi/util/FileUtil", "copyFileFromUri", "(Ljava/lang/String;Ljava/lang/String;Landroid/content/Context;)V", srcStr.object<jstring>(), dstStr.object<jstring>(), QtAndroid::androidContext().object());
+#else
+	QFile::copy(src, dst);
+#endif
+}
 }
 }

+ 5 - 1
launcher/helper.h

@@ -1,5 +1,5 @@
 /*
 /*
- * jsonutils.h, part of VCMI engine
+ * helper.h, part of VCMI engine
  *
  *
  * Authors: listed in file AUTHORS in main folder
  * Authors: listed in file AUTHORS in main folder
  *
  *
@@ -9,10 +9,14 @@
  */
  */
 #pragma once
 #pragma once
 
 
+#include <QString>
+
 class QObject;
 class QObject;
 
 
 namespace Helper
 namespace Helper
 {
 {
 void loadSettings();
 void loadSettings();
 void enableScrollBySwiping(QObject * scrollTarget);
 void enableScrollBySwiping(QObject * scrollTarget);
+QString getRealPath(QString path);
+void performNativeCopy(QString src, QString dst);
 }
 }

+ 5 - 3
launcher/mainwindow_moc.cpp

@@ -264,11 +264,13 @@ void MainWindow::dropEvent(QDropEvent* event)
 
 
 void MainWindow::manualInstallFile(QString filePath)
 void MainWindow::manualInstallFile(QString filePath)
 {
 {
-	if(filePath.endsWith(".zip", Qt::CaseInsensitive) || filePath.endsWith(".exe", Qt::CaseInsensitive))
+	QString realFilePath = Helper::getRealPath(filePath);
+
+	if(realFilePath.endsWith(".zip", Qt::CaseInsensitive) || realFilePath.endsWith(".exe", Qt::CaseInsensitive))
 		switchToModsTab();
 		switchToModsTab();
 
 
 	QString fileName = QFileInfo{filePath}.fileName();
 	QString fileName = QFileInfo{filePath}.fileName();
-	if(filePath.endsWith(".zip", Qt::CaseInsensitive))
+	if(realFilePath.endsWith(".zip", Qt::CaseInsensitive))
 	{
 	{
 		QString filenameClean = fileName.toLower()
 		QString filenameClean = fileName.toLower()
 			// mod name currently comes from zip file -> remove suffixes from github zip download
 			// mod name currently comes from zip file -> remove suffixes from github zip download
@@ -278,7 +280,7 @@ void MainWindow::manualInstallFile(QString filePath)
 
 
 		getModView()->downloadFile(filenameClean, QUrl::fromLocalFile(filePath), "mods");
 		getModView()->downloadFile(filenameClean, QUrl::fromLocalFile(filePath), "mods");
 	}
 	}
-	else if(filePath.endsWith(".json", Qt::CaseInsensitive))
+	else if(realFilePath.endsWith(".json", Qt::CaseInsensitive))
 	{
 	{
 		QDir configDir(QString::fromStdString(VCMIDirs::get().userConfigPath().string()));
 		QDir configDir(QString::fromStdString(VCMIDirs::get().userConfigPath().string()));
 		QStringList configFile = configDir.entryList({fileName}, QDir::Filter::Files); // case insensitive check
 		QStringList configFile = configDir.entryList({fileName}, QDir::Filter::Files); // case insensitive check

+ 9 - 2
launcher/modManager/chroniclesextractor.cpp

@@ -15,6 +15,7 @@
 #include "../../lib/filesystem/CArchiveLoader.h"
 #include "../../lib/filesystem/CArchiveLoader.h"
 
 
 #include "../innoextract.h"
 #include "../innoextract.h"
+#include "../helper.h"
 
 
 ChroniclesExtractor::ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb) :
 ChroniclesExtractor::ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb) :
 	parent(p), cb(cb)
 	parent(p), cb(cb)
@@ -72,10 +73,15 @@ bool ChroniclesExtractor::extractGogInstaller(QString file)
 
 
 	if(!errorText.isEmpty())
 	if(!errorText.isEmpty())
 	{
 	{
+		logGlobal->error("Gog chronicles installer extraction failure! Reason: %s", errorText.toStdString());
+
 		QString hashError = Innoextract::getHashError(file, {}, {}, {});
 		QString hashError = Innoextract::getHashError(file, {}, {}, {});
 		QMessageBox::critical(parent, tr("Extracting error!"), errorText);
 		QMessageBox::critical(parent, tr("Extracting error!"), errorText);
 		if(!hashError.isEmpty())
 		if(!hashError.isEmpty())
+		{
+			logGlobal->error("Hash error: %s", hashError.toStdString());
 			QMessageBox::critical(parent, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok);
 			QMessageBox::critical(parent, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok);
+		}
 		return false;
 		return false;
 	}
 	}
 
 
@@ -226,14 +232,15 @@ void ChroniclesExtractor::installChronicles(QStringList exe)
 		if(!createTempDir())
 		if(!createTempDir())
 			continue;
 			continue;
 		
 		
-		logGlobal->info("Copying offline installer");
 		// FIXME: this is required at the moment for Android (and possibly iOS)
 		// FIXME: this is required at the moment for Android (and possibly iOS)
 		// Incoming file names are in content URI form, e.g. content://media/internal/chronicles.exe
 		// Incoming file names are in content URI form, e.g. content://media/internal/chronicles.exe
 		// Qt can handle those like it does regular files
 		// Qt can handle those like it does regular files
 		// however, innoextract fails to open such files
 		// however, innoextract fails to open such files
 		// so make a copy in directory to which vcmi always has full access and operate on it
 		// so make a copy in directory to which vcmi always has full access and operate on it
 		QString filepath = tempDir.filePath("chr.exe");
 		QString filepath = tempDir.filePath("chr.exe");
-		QFile(f).copy(filepath);
+		logGlobal->info("Copying offline installer from '%s' to '%s'", f.toStdString(), filepath.toStdString());
+
+		Helper::performNativeCopy(f, filepath);
 		QFile file(filepath);
 		QFile file(filepath);
 
 
 		logGlobal->info("Extracting offline installer");
 		logGlobal->info("Extracting offline installer");

+ 7 - 5
launcher/modManager/cmodlistview_moc.cpp

@@ -739,13 +739,15 @@ void CModListView::installFiles(QStringList files)
 	// TODO: some better way to separate zip's with mods and downloaded repository files
 	// TODO: some better way to separate zip's with mods and downloaded repository files
 	for(QString filename : files)
 	for(QString filename : files)
 	{
 	{
-		if(filename.endsWith(".zip", Qt::CaseInsensitive))
+		QString realFilename = Helper::getRealPath(filename);
+
+		if(realFilename.endsWith(".zip", Qt::CaseInsensitive))
 			mods.push_back(filename);
 			mods.push_back(filename);
-		else if(filename.endsWith(".h3m", Qt::CaseInsensitive) || filename.endsWith(".h3c", Qt::CaseInsensitive) || filename.endsWith(".vmap", Qt::CaseInsensitive) || filename.endsWith(".vcmp", Qt::CaseInsensitive))
+		else if(realFilename.endsWith(".h3m", Qt::CaseInsensitive) || realFilename.endsWith(".h3c", Qt::CaseInsensitive) || realFilename.endsWith(".vmap", Qt::CaseInsensitive) || realFilename.endsWith(".vcmp", Qt::CaseInsensitive))
 			maps.push_back(filename);
 			maps.push_back(filename);
-		if(filename.endsWith(".exe", Qt::CaseInsensitive))
+		if(realFilename.endsWith(".exe", Qt::CaseInsensitive))
 			exe.push_back(filename);
 			exe.push_back(filename);
-		else if(filename.endsWith(".json", Qt::CaseInsensitive))
+		else if(realFilename.endsWith(".json", Qt::CaseInsensitive))
 		{
 		{
 			//download and merge additional files
 			//download and merge additional files
 			JsonNode repoData = JsonUtils::jsonFromFile(filename);
 			JsonNode repoData = JsonUtils::jsonFromFile(filename);
@@ -773,7 +775,7 @@ void CModListView::installFiles(QStringList files)
 				JsonUtils::merge(accumulatedRepositoryData[modNameLower], repoData);
 				JsonUtils::merge(accumulatedRepositoryData[modNameLower], repoData);
 			}
 			}
 		}
 		}
-		else if(filename.endsWith(".png", Qt::CaseInsensitive))
+		else if(realFilename.endsWith(".png", Qt::CaseInsensitive))
 			images.push_back(filename);
 			images.push_back(filename);
 	}
 	}