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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
40
docker/BuildAndroid-aarch64.dockerfile
Normal file
40
docker/BuildAndroid-aarch64.dockerfile
Normal 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 \
|
||||
"]
|
@@ -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.
|
||||
|
@@ -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"));
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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");
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user