1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-10 22:31:40 +02:00

Merge pull request #5198 from Laserlicht/android_native_copy

[1.6.x] Android native copy files
This commit is contained in:
Ivan Savenko
2025-01-18 15:36:19 +02:00
committed by GitHub
9 changed files with 167 additions and 15 deletions

View File

@@ -1,18 +1,25 @@
package eu.vcmi.vcmi.util;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
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 java.lang.Exception;
import java.util.List;
import eu.vcmi.vcmi.Const;
import eu.vcmi.vcmi.Storage;
/**
@@ -104,4 +111,45 @@ public class FileUtil
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;
}
}

View File

@@ -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 \
"]

View File

@@ -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).
## 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.

View File

@@ -369,8 +369,9 @@ void FirstLaunchView::extractGogData()
QString tmpFileExe = tempDir.filePath("h3_gog.exe");
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 bin '%s' ('%s')", tmpFileBin.toStdString(), fileBin.toStdString());
@@ -414,9 +415,13 @@ void FirstLaunchView::extractGogData()
{
if(!errorText.isEmpty())
{
logGlobal->error("Gog installer extraction failure! Reason: %s", errorText.toStdString());
QMessageBox::critical(this, tr("Extracting error!"), errorText, QMessageBox::Ok, QMessageBox::Ok);
if(!hashError.isEmpty())
{
logGlobal->error("Hash error: %s", hashError.toStdString());
QMessageBox::critical(this, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok);
}
}
else
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"));
}

View File

@@ -1,5 +1,5 @@
/*
* jsonutils.cpp, part of VCMI engine
* helper.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
@@ -15,6 +15,11 @@
#include <QObject>
#include <QScroller>
#ifdef VCMI_ANDROID
#include <QAndroidJniObject>
#include <QtAndroid>
#endif
#ifdef VCMI_MOBILE
static QScrollerProperties generateScrollerProperties()
{
@@ -44,4 +49,30 @@ void enableScrollBySwiping(QObject * scrollTarget)
scroller->setScrollerProperties(generateScrollerProperties());
#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
}
}

View File

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

View File

@@ -264,11 +264,13 @@ void MainWindow::dropEvent(QDropEvent* event)
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();
QString fileName = QFileInfo{filePath}.fileName();
if(filePath.endsWith(".zip", Qt::CaseInsensitive))
if(realFilePath.endsWith(".zip", Qt::CaseInsensitive))
{
QString filenameClean = fileName.toLower()
// 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");
}
else if(filePath.endsWith(".json", Qt::CaseInsensitive))
else if(realFilePath.endsWith(".json", Qt::CaseInsensitive))
{
QDir configDir(QString::fromStdString(VCMIDirs::get().userConfigPath().string()));
QStringList configFile = configDir.entryList({fileName}, QDir::Filter::Files); // case insensitive check

View File

@@ -15,6 +15,7 @@
#include "../../lib/filesystem/CArchiveLoader.h"
#include "../innoextract.h"
#include "../helper.h"
ChroniclesExtractor::ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb) :
parent(p), cb(cb)
@@ -72,10 +73,15 @@ bool ChroniclesExtractor::extractGogInstaller(QString file)
if(!errorText.isEmpty())
{
logGlobal->error("Gog chronicles installer extraction failure! Reason: %s", errorText.toStdString());
QString hashError = Innoextract::getHashError(file, {}, {}, {});
QMessageBox::critical(parent, tr("Extracting error!"), errorText);
if(!hashError.isEmpty())
{
logGlobal->error("Hash error: %s", hashError.toStdString());
QMessageBox::critical(parent, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok);
}
return false;
}
@@ -226,14 +232,15 @@ void ChroniclesExtractor::installChronicles(QStringList exe)
if(!createTempDir())
continue;
logGlobal->info("Copying offline installer");
// 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
// Qt can handle those like it does regular 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
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);
logGlobal->info("Extracting offline installer");

View File

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