1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-22 22:13:35 +02:00

Merge branch 'vcmi/beta' into 'vcmi/develop'

This commit is contained in:
Ivan Savenko 2024-06-11 19:22:23 +00:00
commit b4c6906471
229 changed files with 9568 additions and 11605 deletions

View File

@ -88,16 +88,16 @@ jobs:
preset: windows-mingw-conan-linux
conan_profile: mingw32-linux.jinja
- platform: android-32
os: ubuntu-22.04
os: macos-14
extension: apk
preset: android-conan-ninja-release
preset: android-daily-release
conan_profile: android-32
conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
artifact_platform: armeabi-v7a
- platform: android-64
os: ubuntu-22.04
os: macos-14
extension: apk
preset: android-conan-ninja-release
preset: android-daily-release
conan_profile: android-64
conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
artifact_platform: arm64-v8a
@ -187,6 +187,12 @@ jobs:
env:
GENERATE_ONLY_BUILT_CONFIG: 1
- uses: actions/setup-java@v4
if: ${{ startsWith(matrix.platform, 'android') }}
with:
distribution: 'temurin'
java-version: '11'
- name: Build Number
run: |
source '${{github.workspace}}/CI/get_package_name.sh'
@ -215,7 +221,7 @@ jobs:
run: |
ctest --preset ${{matrix.preset}}
- name: Kill XProtect to work around CPack issue on macOS
- name: Kill XProtect to work around CPack issue on macOS
if: ${{ startsWith(matrix.platform, 'mac') }}
run: |
# Cf. https://github.com/actions/runner-images/issues/7522#issuecomment-1556766641
@ -234,13 +240,6 @@ jobs:
&& '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)"
rm -rf _CPack_Packages
- name: Create Android package
if: ${{ startsWith(matrix.platform, 'android') }}
run: |
cd android
./gradlew assembleDaily --info
echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/android/vcmi-app/build/outputs/apk/daily/*.${{ matrix.extension }})" >> $GITHUB_ENV
- name: Additional logs
if: ${{ failure() && steps.cpack.outcome == 'failure' && matrix.platform == 'msvc' }}
run: |
@ -254,7 +253,14 @@ jobs:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
path: |
${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }}
- name: Find Android package
if: ${{ startsWith(matrix.platform, 'android') }}
run: |
builtApkPath="$(ls ${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/vcmi-app/build/outputs/apk/release/*.${{ matrix.extension }})"
ANDROID_APK_PATH="${{ github.workspace }}/$VCMI_PACKAGE_FILE_NAME.${{ matrix.extension }}"
mv "$builtApkPath" "$ANDROID_APK_PATH"
echo "ANDROID_APK_PATH=$ANDROID_APK_PATH" >> $GITHUB_ENV
- name: Android artifacts
if: ${{ startsWith(matrix.platform, 'android') }}
uses: actions/upload-artifact@v4
@ -262,7 +268,7 @@ jobs:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
path: |
${{ env.ANDROID_APK_PATH }}
- name: Symbols
if: ${{ matrix.platform == 'msvc' }}
uses: actions/upload-artifact@v4
@ -283,19 +289,17 @@ jobs:
if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' && matrix.platform != 'mingw-32' }}
continue-on-error: true
run: |
if cd '${{github.workspace}}/android/vcmi-app/build/outputs/apk/daily' ; then
mv '${{ env.ANDROID_APK_PATH }}' "$VCMI_PACKAGE_FILE_NAME.${{ matrix.extension }}"
else
if [ -z '${{ env.ANDROID_APK_PATH }}' ] ; then
cd '${{github.workspace}}/out/build/${{matrix.preset}}'
fi
source '${{github.workspace}}/CI/upload_package.sh'
env:
DEPLOY_RSA: ${{ secrets.DEPLOY_RSA }}
PACKAGE_EXTENSION: ${{ matrix.extension }}
# copy-pasted mostly
bundle_release:
needs: build
if: always() && github.ref == 'refs/heads/master'
strategy:
@ -303,7 +307,6 @@ jobs:
include:
- platform: android-32
os: ubuntu-22.04
extension: aab
preset: android-conan-ninja-release
conan_profile: android-32
conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
@ -367,13 +370,13 @@ jobs:
uses: actions/download-artifact@v4
with:
name: Android JNI android-64
path: ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs/
path: ${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/libs
- name: Create Android package
run: |
cd android
cd out/build/${{ matrix.preset }}/android-build
./gradlew bundleRelease --info
echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/android/vcmi-app/build/outputs/bundle/release/*.aab)" >> $GITHUB_ENV
echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/vcmi-app/build/outputs/bundle/release/*.aab)" >> $GITHUB_ENV
env:
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}

View File

@ -403,7 +403,7 @@ void Nullkiller::makeTurn()
if(selectedTasks.empty())
{
return;
selectedTasks.push_back(taskptr(Goals::Invalid()));
}
bool hasAnySuccess = false;

View File

@ -26,9 +26,9 @@ bool ExploreNeighbourTile::operator==(const ExploreNeighbourTile & other) const
void ExploreNeighbourTile::accept(AIGateway * ai)
{
ExplorationHelper h(hero, ai->nullkiller.get());
ExplorationHelper h(hero, ai->nullkiller.get(), true);
for(int i = 0; i < tilesToExplore && hero->movementPointsRemaining() > 0; i++)
for(int i = 0; i < tilesToExplore && ai->myCb->getObj(hero->id, false) && hero->movementPointsRemaining() > 0; i++)
{
int3 pos = hero->visitablePos();
float value = 0;
@ -54,7 +54,14 @@ void ExploreNeighbourTile::accept(AIGateway * ai)
}
});
if(!target.valid() || !ai->moveHeroToTile(target, hero))
if(!target.valid())
{
return;
}
auto danger = ai->nullkiller->pathfinder->getStorage()->evaluateDanger(target, hero, true);
if(danger > 0 || !ai->moveHeroToTile(target, hero))
{
return;
}

View File

@ -24,8 +24,8 @@ namespace NKAI
using namespace Goals;
ExplorationHelper::ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai)
:ai(ai), cbp(ai->cb.get()), hero(hero)
ExplorationHelper::ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai, bool useCPathfinderAccessibility)
:ai(ai), cbp(ai->cb.get()), hero(hero), useCPathfinderAccessibility(useCPathfinderAccessibility)
{
ts = cbp->getPlayerTeam(ai->playerID);
sightRadius = hero->getSightRadius();
@ -104,7 +104,7 @@ bool ExplorationHelper::scanMap()
if(!bestGoal->invalid())
{
return false;
return true;
}
allowDeadEndCancellation = false;
@ -222,7 +222,9 @@ bool ExplorationHelper::hasReachableNeighbor(const int3 & pos) const
int3 tile = pos + dir;
if(cbp->isInTheMap(tile))
{
auto isAccessible = ai->pathfinder->isTileAccessible(hero, tile);
auto isAccessible = useCPathfinderAccessibility
? ai->cb->getPathsInfo(hero)->getPathInfo(tile)->reachable()
: ai->pathfinder->isTileAccessible(hero, tile);
if(isAccessible)
return true;

View File

@ -34,9 +34,10 @@ private:
const TeamState * ts;
int3 ourPos;
bool allowDeadEndCancellation;
bool useCPathfinderAccessibility;
public:
ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai);
ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai, bool useCPathfinderAccessibility = false);
Goals::TSubgoal makeComposition() const;
bool scanSector(int scanRadius);
bool scanMap();

View File

@ -1,8 +1,9 @@
#!/usr/bin/env bash
sudo apt-get update
sudo apt-get install ninja-build
echo "ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/25.2.9519653" >> $GITHUB_ENV
brew install ninja
mkdir ~/.conan ; cd ~/.conan
curl -L "https://github.com/vcmi/vcmi-dependencies/releases/download/android-1.0/$DEPS_FILENAME.txz" \
curl -L "https://github.com/vcmi/vcmi-dependencies/releases/download/android-1.1/$DEPS_FILENAME.txz" \
| tar -xf - --xz

4
CI/conan/android-32-ndk Normal file
View File

@ -0,0 +1,4 @@
include(android-32)
[tool_requires]
android-ndk/r25c

4
CI/conan/android-64-ndk Normal file
View File

@ -0,0 +1,4 @@
include(android-64)
[tool_requires]
android-ndk/r25c

View File

@ -58,13 +58,22 @@ option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" O
# Platform-specific options
if(ANDROID)
set(ANDROID_TARGET_SDK_VERSION "33" CACHE STRING "Android target SDK version")
set(ANDROIDDEPLOYQT_OPTIONS "" CACHE STRING "Additional androiddeployqt options separated by semi-colon")
set(ANDROID_GRADLE_PROPERTIES "" CACHE STRING "Additional Gradle properties separated by semi-colon")
set(ENABLE_STATIC_LIBS ON)
set(ENABLE_LAUNCHER OFF)
set(ENABLE_LAUNCHER ON)
else()
option(ENABLE_STATIC_LIBS "Build library and all components such as AI statically" OFF)
option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
endif()
if(APPLE_IOS)
set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
endif()
if(APPLE_IOS OR ANDROID)
set(ENABLE_MONOLITHIC_INSTALL OFF)
set(ENABLE_SINGLE_APP_BUILD ON)
@ -100,11 +109,6 @@ if (ENABLE_STATIC_LIBS AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
if(APPLE_IOS)
set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
endif()
if(ENABLE_COLORIZED_COMPILER_OUTPUT)
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
add_compile_options(-fcolor-diagnostics)
@ -147,10 +151,6 @@ set(CMAKE_MODULE_PATH ${CMAKE_HOME_DIRECTORY}/cmake_modules ${PROJECT_SOURCE_DIR
include(VCMIUtils)
include(VersionDefinition)
if(ANDROID)
set(VCMI_VERSION "${APP_SHORT_VERSION}")
configure_file("android/GeneratedVersion.java.in" "${CMAKE_SOURCE_DIR}/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/GeneratedVersion.java" @ONLY)
endif()
vcmi_print_important_variables()
@ -575,8 +575,12 @@ elseif(APPLE)
endif()
elseif(ANDROID)
include(GNUInstallDirs)
set(LIB_DIR "jniLibs/${ANDROID_ABI}")
set(DATA_DIR "assets")
set(LIB_DIR "libs/${ANDROID_ABI}")
# required by Qt
set(androidPackageSourceDir "${CMAKE_SOURCE_DIR}/android")
set(androidQtBuildDir "${CMAKE_BINARY_DIR}/android-build")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${androidQtBuildDir}/${LIB_DIR}")
else()
# includes lib path which determines where to install shared libraries (either /lib or /lib64)
include(GNUInstallDirs)
@ -621,6 +625,13 @@ else()
set(SCRIPTING_LIB_DIR "${LIB_DIR}/scripting")
endif()
# common Qt paths
if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
get_target_property(qmakePath Qt${QT_VERSION_MAJOR}::qmake IMPORTED_LOCATION)
get_filename_component(qtDir "${qmakePath}/../../" ABSOLUTE)
set(qtBinDir "${qtDir}/bin")
endif()
#######################################
# Add subdirectories #
#######################################
@ -682,32 +693,15 @@ endif()
#######################################
if(ANDROID)
string(REPLACE ";" "\n" ANDROID_GRADLE_PROPERTIES_MULTILINE "${ANDROID_GRADLE_PROPERTIES}")
file(WRITE "${androidPackageSourceDir}/vcmi-app/gradle.properties" "signingRoot=${CMAKE_SOURCE_DIR}/CI/android\n${ANDROID_GRADLE_PROPERTIES_MULTILINE}")
if(ANDROID_STL MATCHES "_shared$")
set(stlLibName "${CMAKE_SHARED_LIBRARY_PREFIX}${ANDROID_STL}${CMAKE_SHARED_LIBRARY_SUFFIX}")
install(FILES "${CMAKE_SYSROOT}/usr/lib/${ANDROID_SYSROOT_LIB_SUBDIR}/${stlLibName}"
DESTINATION ${LIB_DIR}
)
endif()
# zip internal assets - 'config' and 'Mods' dirs, save md5 of the zip
install(CODE "
cmake_path(ABSOLUTE_PATH CMAKE_INSTALL_PREFIX
OUTPUT_VARIABLE absolute_install_prefix
)
set(absolute_data_dir \"\${absolute_install_prefix}/${DATA_DIR}\")
file(MAKE_DIRECTORY \"\${absolute_data_dir}\")
set(internal_data_zip \"\${absolute_data_dir}/internalData.zip\")
execute_process(COMMAND
\"${CMAKE_COMMAND}\" -E tar c \"\${internal_data_zip}\" --format=zip -- config Mods
WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"
)
file(MD5 \"\${internal_data_zip}\" internal_data_zip_md5)
file(WRITE \"\${absolute_data_dir}/internalDataHash.txt\"
\${internal_data_zip_md5}
)
")
else()
install(DIRECTORY config DESTINATION ${DATA_DIR})
if (ENABLE_CLIENT OR ENABLE_SERVER)

View File

@ -52,7 +52,7 @@
"hidden": true,
"cacheVariables": {
"ENABLE_LOBBY": "ON",
"ENABLE_TEST": "ON",
"ENABLE_TEST": "ON",
"ENABLE_LUA": "ON"
}
},
@ -294,6 +294,15 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{
"name": "android-daily-release",
"displayName": "Android daily release",
"description": "VCMI Android daily build",
"inherits": "android-conan-ninja-release",
"cacheVariables": {
"ANDROID_GRADLE_PROPERTIES": "applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily"
}
}
],
"buildPresets": [
@ -412,6 +421,11 @@
"name": "android-conan-ninja-release",
"configurePreset": "android-conan-ninja-release",
"inherits": "default-release"
},
{
"name": "android-daily-release",
"configurePreset": "android-daily-release",
"inherits": "android-conan-ninja-release"
}
],
"testPresets": [

4
android/.gitignore vendored
View File

@ -1,9 +1,11 @@
*.iml
.gradle
/local.properties
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
# generated by CMake build
/vcmi-app/gradle.properties

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.vcmi.vcmi">
<!-- %%INSERT_PERMISSIONS -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
Remove the comment if you do not require these default permissions. -->
<!-- %%INSERT_PERMISSIONS_DISABLED -->
<!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
Remove the comment if you do not require these default features. -->
<!-- %%INSERT_FEATURES -->
<supports-screens
android:largeScreens="true"
android:xlargeScreens="true" />
<application
android:name="org.qtproject.qt5.android.bindings.QtApplication"
android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:allowBackup="false"
android:installLocation="auto"
android:icon="@mipmap/ic_launcher"
android:label="${applicationLabel}"
android:testOnly="false"
android:supportsRtl="true"
android:usesCleartextTraffic="false">
<activity
android:name=".ActivityLauncher"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
android:exported="true"
android:screenOrientation="sensorLandscape">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
<!-- Deploy Qt libs as part of package -->
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
<!-- Run with local libs -->
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
<meta-data android:name="android.app.load_local_libs_resource_id" android:resource="@array/load_local_libs"/>
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Messages maps -->
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<meta-data android:value="@string/unsupported_android_version" android:name="android.app.unsupported_android_version"/>
<!-- Messages maps -->
<!-- Background running -->
<!-- Warning: changing this value to true may cause unexpected crashes if the
application still try to draw after
"applicationStateChanged(Qt::ApplicationSuspended)"
signal is sent! -->
<meta-data android:name="android.app.background_running" android:value="false"/>
<!-- Background running -->
<!-- auto screen scale factor -->
<meta-data android:name="android.app.auto_screen_scale_factor" android:value="false"/>
<!-- auto screen scale factor -->
<!-- extract android style -->
<!-- available android:values :
* default - In most cases this will be the same as "full", but it can also be something else if needed, e.g., for compatibility reasons
* full - useful QWidget & Quick Controls 1 apps
* minimal - useful for Quick Controls 2 apps, it is much faster than "full"
* none - useful for apps that don't use any of the above Qt modules
-->
<meta-data android:name="android.app.extract_android_style" android:value="none"/>
<!-- extract android style -->
</activity>
<activity
android:name=".VcmiSDLActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="sensorLandscape" />
<service
android:name=".ServerService"
android:process="eu.vcmi.vcmi.srv"
android:description="@string/server_name"
android:exported="false"/>
</application>
</manifest>

View File

@ -1,9 +0,0 @@
package eu.vcmi.vcmi.util;
/**
* Generated via cmake
*/
public class GeneratedVersion
{
public static final String VCMI_VERSION = "@VCMI_VERSION@";
}

View File

@ -0,0 +1,17 @@
{
"android-min-sdk-version": "@CMAKE_ANDROID_API@",
"android-package-source-directory": "@androidPackageSourceDir@",
"android-target-sdk-version": "@ANDROID_TARGET_SDK_VERSION@",
"application-binary": "vcmiclient",
"architectures": {
"@ANDROID_ABI@": "@ANDROID_SYSROOT_LIB_SUBDIR@"
},
"ndk": "@CMAKE_ANDROID_NDK@",
"ndk-host": "@ANDROID_HOST_TAG@",
"qt": "@qtDir@",
"sdk": "@androidSdkDir@",
"sdkBuildToolsRevision": "31.0.0",
"stdcpp-path": "@ANDROID_TOOLCHAIN_ROOT@/sysroot/usr/lib/",
"tool-prefix": "llvm",
"toolchain-prefix": "llvm"
}

View File

@ -1,4 +1,3 @@
ext {
// these values will be retrieved during gradle build
gitInfoVcmi = "none"
}

View File

@ -7,13 +7,18 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Qt-generated properties

View File

@ -1,8 +0,0 @@
/build
# generated by CMake build
/src/main/assets/internalData.zip
/src/main/assets/internalDataHash.txt
/src/main/java/eu/vcmi/vcmi/util/GeneratedVersion.java
/src/main/jniLibs
/src/main/res/raw/authors.txt

View File

@ -1,20 +1,49 @@
plugins {
id 'com.android.application'
}
apply plugin: 'com.android.application'
android {
compileSdk 33
/*******************************************************
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
* are defined in gradle.properties file. This file is
* updated by QtCreator and androiddeployqt tools.
* Changing them manually might break the compilation!
*******************************************************/
ndkVersion '25.2.9519653'
// Extract native libraries from the APK
packagingOptions.jniLibs.useLegacyPackaging true
defaultConfig {
applicationId "is.xyz.vcmi"
minSdk 19
targetSdk 33
compileSdk = androidCompileSdkVersion.takeAfter("-") as Integer // has "android-" prepended
minSdk = qtMinSdkVersion as Integer
targetSdk = qtTargetSdkVersion as Integer // ANDROID_TARGET_SDK_VERSION in the CMake project
versionCode 1600
versionName "1.6.0"
setProperty("archivesBaseName", "vcmi")
}
sourceSets {
main {
// Qt requires these to be in the android project root
manifest.srcFile '../AndroidManifest.xml'
jniLibs.srcDirs = ['../libs']
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'src/main/res', '../res']
}
}
signingConfigs {
releaseSigning
dailySigning
@ -36,27 +65,18 @@ android {
release {
minifyEnabled false
zipAlignEnabled true
signingConfig signingConfigs.releaseSigning
applicationIdSuffix = project.findProperty('applicationIdSuffix')
signingConfig = signingConfigs[project.findProperty('signingConfig') ?: 'releaseSigning']
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
manifestPlaceholders = [
applicationLabel: '@string/app_name',
applicationLabel: project.findProperty('applicationLabel') ?: 'VCMI',
]
ndk {
debugSymbolLevel 'full'
}
}
daily {
initWith release
applicationIdSuffix '.daily'
signingConfig signingConfigs.dailySigning
manifestPlaceholders = [
applicationLabel: 'VCMI daily',
]
}
}
applicationVariants.all { variant -> RenameOutput(project.archivesBaseName, variant) }
tasks.withType(JavaCompile) {
options.compilerArgs += ["-Xlint:deprecation"]
}
@ -66,30 +86,9 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding true
dataBinding true
}
}
def RenameOutput(final baseName, final variant) {
final def buildTaskId = System.getenv("GITHUB_RUN_ID")
ResolveGitInfo()
def name = baseName + "-" + ext.gitInfoVcmi
if (buildTaskId != null && !buildTaskId.isEmpty()) {
name = buildTaskId + "-" + name
}
if (!variant.buildType.name != "release") {
name += "-" + variant.buildType.name
}
variant.outputs.each { output ->
def oldPath = output.outputFile.getAbsolutePath()
output.outputFileName = name + oldPath.substring(oldPath.lastIndexOf("."))
// Do not compress Qt binary resources file
aaptOptions {
noCompress 'rcc'
}
}
@ -111,16 +110,6 @@ def CommandOutput(final cmd, final arguments, final cwd) {
}
}
def ResolveGitInfo() {
if (ext.gitInfoVcmi != "none") {
return
}
ext.gitInfoVcmi =
CommandOutput("git", ["log", "-1", "--pretty=%D", "--decorate-refs=refs/remotes/origin/*"], ".").replace("origin/", "").replace(", HEAD", "").replaceAll("[^a-zA-Z0-9\\-_]", "_") +
"-" +
CommandOutput("git", ["describe", "--match=", "--always", "--abbrev=7"], ".")
}
def SigningPropertiesPath(final basePath, final signingConfigKey) {
return file("${basePath}/${signingConfigKey}.properties")
}
@ -130,9 +119,8 @@ def SigningKeystorePath(final basePath, final keystoreFileName) {
}
def LoadSigningConfig(final signingConfigKey) {
final def projectRoot = "${project.projectDir}/../../CI/android"
final def props = new Properties()
final def propFile = SigningPropertiesPath(projectRoot, signingConfigKey)
final def propFile = SigningPropertiesPath(signingRoot, signingConfigKey)
def signingConfig = android.signingConfigs.getAt(signingConfigKey)
@ -143,7 +131,7 @@ def LoadSigningConfig(final signingConfigKey) {
&& props.containsKey('STORE_FILE')
&& props.containsKey('KEY_ALIAS')) {
signingConfig.storeFile = SigningKeystorePath(projectRoot, props['STORE_FILE'])
signingConfig.storeFile = SigningKeystorePath(signingRoot, props['STORE_FILE'])
signingConfig.storePassword = props['STORE_PASSWORD']
signingConfig.keyAlias = props['KEY_ALIAS']
@ -167,9 +155,7 @@ def LoadSigningConfig(final signingConfigKey) {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.gms:play-services-base:18.2.0'
implementation 'com.google.android.gms:play-services-basement:18.1.0'
implementation fileTree(dir: '../libs', include: ['*.jar', '*.aar'])
implementation 'androidx.annotation:annotation:1.7.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
}

View File

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.vcmi.vcmi">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:extractNativeLibs="true"
android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:allowBackup="false"
android:installLocation="auto"
android:icon="@mipmap/ic_launcher"
android:label="${applicationLabel}"
android:testOnly="false"
android:supportsRtl="true"
android:theme="@style/Theme.VCMI">
<activity
android:exported="true"
android:name=".ActivityLauncher"
android:screenOrientation="sensorLandscape">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ActivityError"
android:screenOrientation="sensorLandscape" />
<activity
android:name=".ActivityMods"
android:screenOrientation="sensorLandscape" />
<activity
android:name=".ActivityAbout"
android:screenOrientation="sensorLandscape" />
<activity
android:name=".VcmiSDLActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="sensorLandscape"
android:theme="@style/Theme.VCMI.Full" />
<service
android:name=".ServerService"
android:process="eu.vcmi.vcmi.srv"
android:description="@string/server_name"
android:exported="false"/>
</application>
</manifest>

View File

@ -1,94 +0,0 @@
package eu.vcmi.vcmi;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.UnderlineSpan;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import eu.vcmi.vcmi.content.DialogAuthors;
import eu.vcmi.vcmi.util.GeneratedVersion;
import eu.vcmi.vcmi.util.Utils;
/**
* @author F
*/
public class ActivityAbout extends ActivityWithToolbar
{
private static final String DIALOG_AUTHORS_TAG = "DIALOG_AUTHORS_TAG";
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
initToolbar(R.string.about_title);
initControl(R.id.about_version_app, getString(R.string.about_version_app, GeneratedVersion.VCMI_VERSION));
initControl(R.id.about_version_launcher, getString(R.string.about_version_launcher, Utils.appVersionName(this)));
initControlUrl(R.id.about_link_portal, R.string.about_links_main, R.string.url_project_page, this::onUrlPressed);
initControlUrl(R.id.about_link_repo_main, R.string.about_links_repo, R.string.url_project_repo, this::onUrlPressed);
initControlUrl(R.id.about_link_repo_launcher, R.string.about_links_repo_launcher, R.string.url_launcher_repo, this::onUrlPressed);
initControlBtn(R.id.about_btn_authors, this::onBtnAuthorsPressed);
initControlUrl(R.id.about_btn_privacy, R.string.about_btn_privacy, R.string.url_launcher_privacy, this::onUrlPressed);
}
private void initControlBtn(final int viewId, final View.OnClickListener callback)
{
findViewById(viewId).setOnClickListener(callback);
}
private void initControlUrl(final int textViewResId, final int baseTextRes, final int urlTextRes, final IInternalUrlCallback callback)
{
final TextView ctrl = (TextView) findViewById(textViewResId);
final String urlText = getString(urlTextRes);
final String fullText = getString(baseTextRes, urlText);
ctrl.setText(decoratedLinkText(fullText, fullText.indexOf(urlText), fullText.length()));
ctrl.setOnClickListener(v -> callback.onPressed(urlText));
}
private Spanned decoratedLinkText(final String rawText, final int start, final int end)
{
final SpannableString spannableString = new SpannableString(rawText);
spannableString.setSpan(new UnderlineSpan(), start, end, 0);
spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(this, R.color.accent)), start, end, 0);
return spannableString;
}
private void initControl(final int textViewResId, final String text)
{
((TextView) findViewById(textViewResId)).setText(text);
}
private void onBtnAuthorsPressed(final View v)
{
final DialogAuthors dialogAuthors = new DialogAuthors();
dialogAuthors.show(getSupportFragmentManager(), DIALOG_AUTHORS_TAG);
}
private void onUrlPressed(final String url)
{
try
{
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
catch (final ActivityNotFoundException ignored)
{
Toast.makeText(this, R.string.about_error_opening_url, Toast.LENGTH_LONG).show();
}
}
private interface IInternalUrlCallback
{
void onPressed(final String link);
}
}

View File

@ -1,58 +0,0 @@
package eu.vcmi.vcmi;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.util.Log;
import eu.vcmi.vcmi.util.SharedPrefs;
/**
* @author F
*/
public abstract class ActivityBase extends AppCompatActivity
{
protected SharedPrefs mPrefs;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setupExceptionHandler();
mPrefs = new SharedPrefs(this);
}
private void setupExceptionHandler()
{
final Thread.UncaughtExceptionHandler prevHandler = Thread.getDefaultUncaughtExceptionHandler();
if (prevHandler != null && !(prevHandler instanceof VCMIExceptionHandler)) // no need to recreate it if it's already setup
{
Thread.setDefaultUncaughtExceptionHandler(new VCMIExceptionHandler(prevHandler));
}
}
private static class VCMIExceptionHandler implements Thread.UncaughtExceptionHandler
{
private Thread.UncaughtExceptionHandler mPrevHandler;
private VCMIExceptionHandler(final Thread.UncaughtExceptionHandler prevHandler)
{
mPrevHandler = prevHandler;
}
@Override
public void uncaughtException(final Thread thread, final Throwable throwable)
{
Log.e(this, "Unhandled exception", throwable); // to save the exception to file before crashing
if (mPrevHandler != null && !(mPrevHandler instanceof VCMIExceptionHandler))
{
mPrevHandler.uncaughtException(thread, throwable);
}
else
{
System.exit(1);
}
}
}
}

View File

@ -1,48 +0,0 @@
package eu.vcmi.vcmi;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import android.view.View;
import android.widget.TextView;
/**
* @author F
*/
public class ActivityError extends ActivityWithToolbar
{
public static final String ARG_ERROR_MSG = "ActivityError.msg";
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_error);
initToolbar(R.string.launcher_title);
final View btnTryAgain = findViewById(R.id.error_btn_try_again);
btnTryAgain.setOnClickListener(new OnErrorRetryPressed());
final Bundle extras = getIntent().getExtras();
if (extras != null)
{
final String errorMessage = extras.getString(ARG_ERROR_MSG);
final TextView errorMessageView = (TextView) findViewById(R.id.error_message);
if (errorMessage != null)
{
errorMessageView.setText(errorMessage);
}
}
}
private class OnErrorRetryPressed implements View.OnClickListener
{
@Override
public void onClick(final View v)
{
// basically restarts main activity
startActivity(new Intent(ActivityError.this, ActivityLauncher.class));
finish();
}
}
}

View File

@ -1,307 +1,62 @@
package eu.vcmi.vcmi;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONObject;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import android.os.Environment;
import android.provider.DocumentsContract;
import eu.vcmi.vcmi.content.AsyncLauncherInitialization;
import eu.vcmi.vcmi.settings.AdventureAiController;
import eu.vcmi.vcmi.settings.LanguageSettingController;
import eu.vcmi.vcmi.settings.CopyDataController;
import eu.vcmi.vcmi.settings.ExportDataController;
import eu.vcmi.vcmi.settings.LauncherSettingController;
import eu.vcmi.vcmi.settings.ModsBtnController;
import eu.vcmi.vcmi.settings.MusicSettingController;
import eu.vcmi.vcmi.settings.PointerModeSettingController;
import eu.vcmi.vcmi.settings.PointerMultiplierSettingController;
import eu.vcmi.vcmi.settings.ScreenScaleSettingController;
import eu.vcmi.vcmi.settings.ScreenScaleSettingDialog;
import eu.vcmi.vcmi.settings.SoundSettingController;
import eu.vcmi.vcmi.settings.StartGameController;
import androidx.annotation.Nullable;
import java.io.File;
import eu.vcmi.vcmi.VcmiSDLActivity;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
import eu.vcmi.vcmi.util.SharedPrefs;
import org.libsdl.app.SDL;
/**
* @author F
*/
public class ActivityLauncher extends ActivityWithToolbar
public class ActivityLauncher extends org.qtproject.qt5.android.bindings.QtActivity
{
public static final int PERMISSIONS_REQ_CODE = 123;
private static final int PICK_EXTERNAL_VCMI_DATA_TO_COPY = 1;
private final List<LauncherSettingController<?, ?>> mActualSettings = new ArrayList<>();
private View mProgress;
private TextView mErrorMessage;
private Config mConfig;
private LauncherSettingController<String, Config> mCtrlLanguage;
private LauncherSettingController<PointerModeSettingController.PointerMode, Config> mCtrlPointerMode;
private LauncherSettingController<Void, Void> mCtrlStart;
private LauncherSettingController<Float, Config> mCtrlPointerMulti;
private LauncherSettingController<ScreenScaleSettingController.ScreenScale, Config> mCtrlScreenScale;
private LauncherSettingController<Integer, Config> mCtrlSoundVol;
private LauncherSettingController<Integer, Config> mCtrlMusicVol;
private LauncherSettingController<String, Config> mAiController;
private CopyDataController mCtrlCopy;
private ExportDataController mCtrlExport;
private final AsyncLauncherInitialization.ILauncherCallbacks mInitCallbacks = new AsyncLauncherInitialization.ILauncherCallbacks()
{
@Override
public Activity ctx()
{
return ActivityLauncher.this;
}
@Override
public SharedPrefs prefs()
{
return mPrefs;
}
@Override
public void onInitSuccess()
{
loadConfigFile();
mCtrlStart.show();
mCtrlCopy.show();
mCtrlExport.show();
for (LauncherSettingController<?, ?> setting: mActualSettings) {
setting.show();
}
mErrorMessage.setVisibility(View.GONE);
mProgress.setVisibility(View.GONE);
}
@Override
public void onInitFailure(final AsyncLauncherInitialization.InitResult result)
{
mCtrlCopy.show();
if (result.mFailSilently)
{
return;
}
ActivityLauncher.this.onInitFailure(result);
}
};
public boolean justLaunched = true;
@Override
public void onCreate(final Bundle savedInstanceState)
public void onCreate(@Nullable final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
justLaunched = savedInstanceState == null;
SDL.setContext(this);
}
if (savedInstanceState == null) // only clear the log if this is initial onCreate and not config change
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent resultData)
{
if (requestCode == PICK_EXTERNAL_VCMI_DATA_TO_COPY && resultCode == Activity.RESULT_OK)
{
Log.init();
}
Log.i(this, "Starting launcher");
setContentView(R.layout.activity_launcher);
initToolbar(R.string.launcher_title, true);
mProgress = findViewById(R.id.launcher_progress);
mErrorMessage = (TextView) findViewById(R.id.launcher_error);
mErrorMessage.setVisibility(View.GONE);
((TextView) findViewById(R.id.launcher_version_info)).setText(getString(R.string.launcher_version, BuildConfig.VERSION_NAME));
initSettingsGui();
}
@Override
public void onStart()
{
super.onStart();
new AsyncLauncherInitialization(mInitCallbacks).execute((Void) null);
}
@Override
public void onBackPressed()
{
saveConfig();
super.onBackPressed();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu)
{
getMenuInflater().inflate(R.menu.menu_launcher, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
if (item.getItemId() == R.id.menu_launcher_about)
{
startActivity(new Intent(this, ActivityAbout.class));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData)
{
if(requestCode == CopyDataController.PICK_EXTERNAL_VCMI_DATA_TO_COPY
&& resultCode == Activity.RESULT_OK)
{
Uri uri;
if (resultData != null)
{
uri = resultData.getData();
mCtrlCopy.copyData(uri);
}
return;
}
if(requestCode == ExportDataController.PICK_DIRECTORY_TO_EXPORT
&& resultCode == Activity.RESULT_OK)
{
Uri uri = null;
if (resultData != null)
{
uri = resultData.getData();
mCtrlExport.copyData(uri);
}
if (resultData != null && FileUtil.copyData(resultData.getData(), this))
NativeMethods.heroesDataUpdate();
return;
}
super.onActivityResult(requestCode, resultCode, resultData);
}
public void requestStoragePermissions()
public void copyHeroesData()
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
{
requestPermissions(
new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSIONS_REQ_CODE);
}
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "vcmi-data"))
);
startActivityForResult(intent, PICK_EXTERNAL_VCMI_DATA_TO_COPY);
}
private void initSettingsGui()
public void onLaunchGameBtnPressed()
{
mCtrlStart = new StartGameController(this, v -> onLaunchGameBtnPressed()).init(R.id.launcher_btn_start);
(mCtrlCopy = new CopyDataController(this)).init(R.id.launcher_btn_copy);
(mCtrlExport = new ExportDataController(this)).init(R.id.launcher_btn_export);
new ModsBtnController(this, v -> startActivity(new Intent(ActivityLauncher.this, ActivityMods.class))).init(R.id.launcher_btn_mods);
mCtrlLanguage = new LanguageSettingController(this).init(R.id.launcher_btn_cp, mConfig);
mCtrlPointerMode = new PointerModeSettingController(this).init(R.id.launcher_btn_pointer_mode, mConfig);
mCtrlPointerMulti = new PointerMultiplierSettingController(this).init(R.id.launcher_btn_pointer_multi, mConfig);
mCtrlScreenScale = new ScreenScaleSettingController(this).init(R.id.launcher_btn_scale, mConfig);
mCtrlSoundVol = new SoundSettingController(this).init(R.id.launcher_btn_volume_sound, mConfig);
mCtrlMusicVol = new MusicSettingController(this).init(R.id.launcher_btn_volume_music, mConfig);
mAiController = new AdventureAiController(this).init(R.id.launcher_btn_adventure_ai, mConfig);
mActualSettings.clear();
mActualSettings.add(mCtrlLanguage);
mActualSettings.add(mCtrlPointerMode);
mActualSettings.add(mCtrlPointerMulti);
mActualSettings.add(mCtrlScreenScale);
mActualSettings.add(mCtrlSoundVol);
mActualSettings.add(mCtrlMusicVol);
mActualSettings.add(mAiController);
mCtrlStart.hide(); // start is initially hidden, until we confirm that everything is okay via AsyncLauncherInitialization
mCtrlCopy.hide();
mCtrlExport.hide();
}
private void onLaunchGameBtnPressed()
{
saveConfig();
startActivity(new Intent(ActivityLauncher.this, VcmiSDLActivity.class));
}
private void saveConfig()
{
if (mConfig == null)
{
return;
}
try
{
mConfig.save(new File(FileUtil.configFileLocation(Storage.getVcmiDataDir(this))));
}
catch (final Exception e)
{
Toast.makeText(this, getString(R.string.launcher_error_config_saving_failed, e.getMessage()), Toast.LENGTH_LONG).show();
}
}
private void loadConfigFile()
{
try
{
final String settingsFileContent = FileUtil.read(
new File(FileUtil.configFileLocation(Storage.getVcmiDataDir(this))));
mConfig = Config.load(new JSONObject(settingsFileContent));
}
catch (final Exception e)
{
Log.e(this, "Could not load config file", e);
mConfig = new Config();
}
onConfigUpdated();
}
private void onConfigUpdated()
{
if(mConfig.mScreenScale == -1)
mConfig.updateScreenScale(ScreenScaleSettingDialog.getSupportedScalingRange(ActivityLauncher.this)[1]);
updateCtrlConfig(mCtrlLanguage, mConfig);
updateCtrlConfig(mCtrlPointerMode, mConfig);
updateCtrlConfig(mCtrlPointerMulti, mConfig);
updateCtrlConfig(mCtrlScreenScale, mConfig);
updateCtrlConfig(mCtrlSoundVol, mConfig);
updateCtrlConfig(mCtrlMusicVol, mConfig);
updateCtrlConfig(mAiController, mConfig);
}
private <TSetting, TConf> void updateCtrlConfig(
final LauncherSettingController<TSetting, TConf> ctrl,
final TConf config)
{
if (ctrl != null)
{
ctrl.updateConfig(config);
}
}
private void onInitFailure(final AsyncLauncherInitialization.InitResult initResult)
{
Log.d(this, "Init failed with " + initResult);
mProgress.setVisibility(View.GONE);
mCtrlStart.hide();
for (LauncherSettingController<?, ?> setting: mActualSettings)
{
setting.hide();
}
mErrorMessage.setVisibility(View.VISIBLE);
mErrorMessage.setText(initResult.mMessage);
}
}

View File

@ -1,351 +0,0 @@
package eu.vcmi.vcmi;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
import com.google.android.gms.common.GooglePlayServicesRepairableException;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.security.ProviderInstaller;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import eu.vcmi.vcmi.content.ModBaseViewHolder;
import eu.vcmi.vcmi.content.ModsAdapter;
import eu.vcmi.vcmi.mods.VCMIMod;
import eu.vcmi.vcmi.mods.VCMIModContainer;
import eu.vcmi.vcmi.mods.VCMIModsRepo;
import eu.vcmi.vcmi.util.InstallModAsync;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
import eu.vcmi.vcmi.util.ServerResponse;
/**
* @author F
*/
public class ActivityMods extends ActivityWithToolbar
{
private static final boolean ENABLE_REPO_DOWNLOADING = true;
private static final String REPO_URL = "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.5.json";
private VCMIModsRepo mRepo;
private RecyclerView mRecycler;
private VCMIModContainer mModContainer;
private TextView mErrorMessage;
private View mProgress;
private ModsAdapter mModsAdapter;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mods);
initToolbar(R.string.mods_title);
mRepo = new VCMIModsRepo();
mProgress = findViewById(R.id.mods_progress);
mErrorMessage = (TextView) findViewById(R.id.mods_error_text);
mErrorMessage.setVisibility(View.GONE);
mRecycler = (RecyclerView) findViewById(R.id.mods_recycler);
mRecycler.setItemAnimator(new DefaultItemAnimator());
mRecycler.setLayoutManager(new LinearLayoutManager(this));
mRecycler.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
mRecycler.setVisibility(View.GONE);
mModsAdapter = new ModsAdapter(new OnAdapterItemAction());
mRecycler.setAdapter(mModsAdapter);
new AsyncLoadLocalMods().execute((Void) null);
try {
ProviderInstaller.installIfNeeded(this);
} catch (GooglePlayServicesRepairableException e) {
GooglePlayServicesUtil.getErrorDialog(e.getConnectionStatusCode(), this, 0);
} catch (GooglePlayServicesNotAvailableException e) {
Log.e("SecurityException", "Google Play Services not available.");
}
}
private void loadLocalModData() throws IOException, JSONException
{
final File dataRoot = Storage.getVcmiDataDir(this);
final String internalDataRoot = getFilesDir() + "/" + Const.VCMI_DATA_ROOT_FOLDER_NAME;
final File modsRoot = new File(dataRoot,"/Mods");
final File internalModsRoot = new File(internalDataRoot + "/Mods");
if (!modsRoot.exists() && !internalModsRoot.exists())
{
Log.w(this, "We don't have mods folders");
return;
}
final File[] modsFiles = modsRoot.listFiles();
final File[] internalModsFiles = internalModsRoot.listFiles();
final List<File> topLevelModsFolders = new ArrayList<>();
if (modsFiles != null && modsFiles.length > 0)
{
Collections.addAll(topLevelModsFolders, modsFiles);
}
if (internalModsFiles != null && internalModsFiles.length > 0)
{
Collections.addAll(topLevelModsFolders, internalModsFiles);
}
mModContainer = VCMIModContainer.createContainer(topLevelModsFolders);
final File modConfigFile = new File(dataRoot, "config/modSettings.json");
if (!modConfigFile.exists())
{
Log.w(this, "We don't have mods config");
return;
}
JSONObject rootConfigObj = new JSONObject(FileUtil.read(modConfigFile));
JSONObject activeMods = rootConfigObj.getJSONObject("activeMods");
mModContainer.updateContainerFromConfigJson(activeMods, rootConfigObj.optJSONObject("core"));
Log.i(this, "Loaded mods: " + mModContainer);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu)
{
final MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(R.menu.menu_mods, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
if (item.getItemId() == R.id.menu_mods_download_repo)
{
Log.i(this, "Should download repo now...");
if (ENABLE_REPO_DOWNLOADING)
{
mProgress.setVisibility(View.VISIBLE);
mRepo.init(REPO_URL, new OnModsRepoInitialized()); // disabled because the json is broken anyway
}
else
{
Snackbar.make(findViewById(R.id.mods_data_root), "Loading repo is disabled for now, because .json can't be parsed anyway",
Snackbar.LENGTH_LONG).show();
}
}
return super.onOptionsItemSelected(item);
}
private void handleNoData()
{
mProgress.setVisibility(View.GONE);
mRecycler.setVisibility(View.GONE);
mErrorMessage.setVisibility(View.VISIBLE);
mErrorMessage.setText("Could not load local mods list");
}
private void saveModSettingsToFile()
{
mModContainer.saveToFile(
new File(
Storage.getVcmiDataDir(this),
"config/modSettings.json"));
}
private class OnModsRepoInitialized implements VCMIModsRepo.IOnModsRepoDownloaded
{
@Override
public void onSuccess(ServerResponse<List<VCMIMod>> response)
{
Log.i(this, "Initialized mods repo");
if (mModContainer == null)
{
handleNoData();
}
else
{
mModContainer.updateFromRepo(response.mContent);
mModsAdapter.updateModsList(mModContainer.submods());
mProgress.setVisibility(View.GONE);
}
}
@Override
public void onError(final int code)
{
Log.i(this, "Mods repo error: " + code);
}
}
private class AsyncLoadLocalMods extends AsyncTask<Void, Void, Void>
{
@Override
protected void onPreExecute()
{
mProgress.setVisibility(View.VISIBLE);
}
@Override
protected Void doInBackground(final Void... params)
{
try
{
loadLocalModData();
}
catch (IOException e)
{
Log.e(this, "Loading local mod data failed", e);
}
catch (JSONException e)
{
Log.e(this, "Parsing local mod data failed", e);
}
return null;
}
@Override
protected void onPostExecute(final Void aVoid)
{
if (mModContainer == null || !mModContainer.hasSubmods())
{
handleNoData();
}
else
{
mProgress.setVisibility(View.GONE);
mRecycler.setVisibility(View.VISIBLE);
mModsAdapter.updateModsList(mModContainer.submods());
}
}
}
private class OnAdapterItemAction implements ModsAdapter.IOnItemAction
{
@Override
public void onItemPressed(final ModsAdapter.ModItem mod, final RecyclerView.ViewHolder vh)
{
Log.i(this, "Mod pressed: " + mod);
if (mod.mMod.hasSubmods())
{
if (mod.mExpanded)
{
mModsAdapter.detachSubmods(mod, vh);
}
else
{
mModsAdapter.attachSubmods(mod, vh);
mRecycler.scrollToPosition(vh.getAdapterPosition() + 1);
}
mod.mExpanded = !mod.mExpanded;
}
}
@Override
public void onDownloadPressed(final ModsAdapter.ModItem mod, final RecyclerView.ViewHolder vh)
{
Log.i(this, "Mod download pressed: " + mod);
mModsAdapter.downloadProgress(mod, "0%");
installModAsync(mod);
}
@Override
public void onTogglePressed(final ModsAdapter.ModItem item, final ModBaseViewHolder holder)
{
if(!item.mMod.mSystem && item.mMod.mInstalled)
{
item.mMod.mActive = !item.mMod.mActive;
mModsAdapter.notifyItemChanged(holder.getAdapterPosition());
saveModSettingsToFile();
}
}
@Override
public void onUninstall(ModsAdapter.ModItem item, ModBaseViewHolder holder)
{
File installationFolder = item.mMod.installationFolder;
ActivityMods activity = ActivityMods.this;
if(installationFolder != null){
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.mods_removal_title, item.mMod.mName))
.setMessage(activity.getString(R.string.mods_removal_confirmation, item.mMod.mName))
.setIcon(android.R.drawable.ic_dialog_alert)
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) ->
{
FileUtil.clearDirectory(installationFolder);
installationFolder.delete();
mModsAdapter.modRemoved(item);
})
.show();
}
}
}
private void installModAsync(ModsAdapter.ModItem mod){
File dataDir = Storage.getVcmiDataDir(this);
File modFolder = new File(
new File(dataDir, "Mods"),
mod.mMod.mId.toLowerCase(Locale.US));
InstallModAsync modInstaller = new InstallModAsync(
modFolder,
this,
new InstallModCallback(mod)
);
modInstaller.execute(mod.mMod.mArchiveUrl);
}
public class InstallModCallback implements InstallModAsync.PostDownload
{
private ModsAdapter.ModItem mod;
public InstallModCallback(ModsAdapter.ModItem mod)
{
this.mod = mod;
}
@Override
public void downloadDone(Boolean succeed, File modFolder)
{
if(succeed){
mModsAdapter.modInstalled(mod, modFolder);
}
}
@Override
public void downloadProgress(String... progress)
{
if(progress.length > 0)
{
mModsAdapter.downloadProgress(mod, progress[0]);
}
}
}
}

View File

@ -1,53 +0,0 @@
package eu.vcmi.vcmi;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import android.view.ViewStub;
/**
* @author F
*/
public abstract class ActivityWithToolbar extends ActivityBase
{
@Override
public void setContentView(final int layoutResId)
{
super.setContentView(R.layout.activity_toolbar_wrapper);
final ViewStub contentStub = (ViewStub) findViewById(R.id.toolbar_wrapper_content_stub);
contentStub.setLayoutResource(layoutResId);
contentStub.inflate();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
if (item.getItemId() == android.R.id.home)
{
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
protected void initToolbar(final int textResId)
{
initToolbar(textResId, false);
}
protected void initToolbar(final int textResId, final boolean isTopLevelActivity)
{
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
toolbar.setTitle(textResId);
if (!isTopLevelActivity)
{
final ActionBar bar = getSupportActionBar();
if (bar != null)
{
bar.setDisplayHomeAsUpEnabled(true);
}
}
}
}

View File

@ -1,217 +0,0 @@
package eu.vcmi.vcmi;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public class Config
{
public static final String DEFAULT_LANGUAGE = "english";
public static final int DEFAULT_MUSIC_VALUE = 5;
public static final int DEFAULT_SOUND_VALUE = 5;
public String mLanguage;
public int mScreenScale;
public int mVolumeSound;
public int mVolumeMusic;
private String adventureAi;
private double mPointerSpeedMultiplier;
private boolean mUseRelativePointer;
private JSONObject mRawObject;
private boolean mIsModified;
private static JSONObject accessNode(final JSONObject baseObj, String type)
{
if (baseObj == null)
{
return null;
}
return baseObj.optJSONObject(type);
}
private static JSONObject accessResolutionNode(final JSONObject baseObj)
{
if (baseObj == null)
{
return null;
}
final JSONObject video = baseObj.optJSONObject("video");
if (video != null)
{
return video.optJSONObject("resolution");
}
return null;
}
private static double loadDouble(final JSONObject node, final String key, final double fallback)
{
if (node == null)
{
return fallback;
}
return node.optDouble(key, fallback);
}
@SuppressWarnings("unchecked")
private static <T> T loadEntry(final JSONObject node, final String key, final T fallback)
{
if (node == null)
{
return fallback;
}
final Object value = node.opt(key);
return value == null ? fallback : (T) value;
}
public static Config load(final JSONObject obj)
{
Log.v("loading config from json: " + obj.toString());
final Config config = new Config();
final JSONObject general = accessNode(obj, "general");
final JSONObject server = accessNode(obj, "server");
final JSONObject resolution = accessResolutionNode(obj);
config.mLanguage = loadEntry(general, "language", DEFAULT_LANGUAGE);
config.mScreenScale = loadEntry(resolution, "scaling", -1);
config.mVolumeSound = loadEntry(general, "sound", DEFAULT_SOUND_VALUE);
config.mVolumeMusic = loadEntry(general, "music", DEFAULT_MUSIC_VALUE);
config.adventureAi = loadEntry(server, "playerAI", "Nullkiller");
config.mUseRelativePointer = loadEntry(general, "userRelativePointer", false);
config.mPointerSpeedMultiplier = loadDouble(general, "relativePointerSpeedMultiplier", 1.0);
config.mRawObject = obj;
return config;
}
public void updateLanguage(final String s)
{
mLanguage = s;
mIsModified = true;
}
public void updateScreenScale(final int scale)
{
mScreenScale = scale;
mIsModified = true;
}
public void updateSound(final int i)
{
mVolumeSound = i;
mIsModified = true;
}
public void updateMusic(final int i)
{
mVolumeMusic = i;
mIsModified = true;
}
public void setAdventureAi(String ai)
{
adventureAi = ai;
mIsModified = true;
}
public String getAdventureAi()
{
return this.adventureAi == null ? "Nullkiller" : this.adventureAi;
}
public void setPointerSpeedMultiplier(float speedMultiplier)
{
mPointerSpeedMultiplier = speedMultiplier;
mIsModified = true;
}
public float getPointerSpeedMultiplier()
{
return (float)mPointerSpeedMultiplier;
}
public void setPointerMode(boolean isRelative)
{
mUseRelativePointer = isRelative;
mIsModified = true;
}
public boolean getPointerModeIsRelative()
{
return mUseRelativePointer;
}
public void save(final File location) throws IOException, JSONException
{
if (!needsSaving(location))
{
Log.d(this, "Config doesn't need saving");
return;
}
try
{
final String configString = toJson();
FileUtil.write(location, configString);
Log.v(this, "Saved config: " + configString);
}
catch (final Exception e)
{
Log.e(this, "Could not save config", e);
throw e;
}
}
private boolean needsSaving(final File location)
{
return mIsModified || !location.exists();
}
private String toJson() throws JSONException
{
final JSONObject generalNode = accessNode(mRawObject, "general");
final JSONObject serverNode = accessNode(mRawObject, "server");
final JSONObject resolutionNode = accessResolutionNode(mRawObject);
final JSONObject root = mRawObject == null ? new JSONObject() : mRawObject;
final JSONObject general = generalNode == null ? new JSONObject() : generalNode;
final JSONObject video = new JSONObject();
final JSONObject resolution = resolutionNode == null ? new JSONObject() : resolutionNode;
final JSONObject server = serverNode == null ? new JSONObject() : serverNode;
if (mLanguage != null)
{
general.put("language", mLanguage);
}
general.put("music", mVolumeMusic);
general.put("sound", mVolumeSound);
general.put("userRelativePointer", mUseRelativePointer);
general.put("relativePointerSpeedMultiplier", mPointerSpeedMultiplier);
root.put("general", general);
if(this.adventureAi != null)
{
server.put("playerAI", this.adventureAi);
root.put("server", server);
}
if (mScreenScale > 0)
{
resolution.put("scaling", mScreenScale);
video.put("resolution", resolution);
root.put("video", video);
}
return root.toString();
}
}

View File

@ -1,15 +1,6 @@
package eu.vcmi.vcmi;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
/**
* @author F
@ -17,8 +8,6 @@ import java.io.OutputStreamWriter;
public class Const
{
public static final String JNI_METHOD_SUPPRESS = "unused"; // jni methods are marked as unused, because IDE doesn't understand jni calls
// used to disable lint errors about try-with-resources being unsupported on api <19 (it is supported, because retrolambda backports it)
public static final int SUPPRESS_TRY_WITH_RESOURCES_WARNING = Build.VERSION_CODES.KITKAT;
public static final String VCMI_DATA_ROOT_FOLDER_NAME = "vcmi-data";
}

View File

@ -1,14 +1,8 @@
package eu.vcmi.vcmi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.VibrationEffect;
import android.os.Vibrator;
@ -17,9 +11,6 @@ import org.libsdl.app.SDLActivity;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.Date;
import java.util.Locale;
import java.text.SimpleDateFormat;
import eu.vcmi.vcmi.util.Log;
@ -35,7 +26,7 @@ public class NativeMethods
}
public static native void initClassloader();
public static native void heroesDataUpdate();
public static native boolean tryToSaveTheGame();
public static void setupMsg(final Messenger msg)

View File

@ -1,33 +1,14 @@
package eu.vcmi.vcmi;
import android.content.Context;
import java.io.File;
import java.io.IOException;
import eu.vcmi.vcmi.util.FileUtil;
public class Storage
{
public static File getVcmiDataDir(Context context)
{
File root = context.getExternalFilesDir(null);
return new File(root, Const.VCMI_DATA_ROOT_FOLDER_NAME);
}
public static boolean testH3DataFolder(Context context)
{
return testH3DataFolder(getVcmiDataDir(context));
}
public static boolean testH3DataFolder(final File baseDir)
{
final File testH3Data = new File(baseDir, "Data");
final File testH3data = new File(baseDir, "data");
final File testH3DATA = new File(baseDir, "DATA");
return testH3Data.exists() || testH3data.exists() || testH3DATA.exists();
}
public static String getH3DataFolder(Context context){
return getVcmiDataDir(context).getAbsolutePath();
}
}

View File

@ -84,9 +84,7 @@ public class VcmiSDLActivity extends SDLActivity
@Override
protected String getMainSharedObject() {
String library = "libvcmiclient.so";
return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
return String.format("%s/lib%s.so", getContext().getApplicationInfo().nativeLibraryDir, LibsLoader.CLIENT_LIB);
}
@Override
@ -100,9 +98,6 @@ public class VcmiSDLActivity extends SDLActivity
{
super.onCreate(savedInstanceState);
if(mBrokenLibraries)
return;
final View outerLayout = getLayoutInflater().inflate(R.layout.activity_game, null, false);
final ViewGroup layout = (ViewGroup) outerLayout.findViewById(R.id.game_outer_frame);
mProgressBar = outerLayout.findViewById(R.id.game_progress);
@ -182,4 +177,4 @@ public class VcmiSDLActivity extends SDLActivity
mCallback = callback;
}
}
}
}

View File

@ -1,171 +0,0 @@
package eu.vcmi.vcmi.content;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import androidx.core.app.ActivityCompat;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.Const;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.Storage;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
import eu.vcmi.vcmi.util.SharedPrefs;
/**
* @author F
*/
public class AsyncLauncherInitialization extends AsyncTask<Void, Void, AsyncLauncherInitialization.InitResult>
{
private final WeakReference<ILauncherCallbacks> mCallbackRef;
public AsyncLauncherInitialization(final ILauncherCallbacks callback)
{
mCallbackRef = new WeakReference<>(callback);
}
private InitResult init()
{
InitResult initResult = handleDataFoldersInitialization();
if (!initResult.mSuccess)
{
return initResult;
}
Log.d(this, "Folders check passed");
return initResult;
}
private InitResult handleDataFoldersInitialization()
{
final ILauncherCallbacks callbacks = mCallbackRef.get();
if (callbacks == null)
{
return InitResult.failure("Internal error");
}
final Context ctx = callbacks.ctx();
final File vcmiDir = Storage.getVcmiDataDir(ctx);
final File internalDir = ctx.getFilesDir();
final File vcmiInternalDir = new File(internalDir, Const.VCMI_DATA_ROOT_FOLDER_NAME);
Log.i(this, "Using " + vcmiDir.getAbsolutePath() + " as root vcmi dir");
if(!vcmiInternalDir.exists()) vcmiInternalDir.mkdir();
if(!vcmiDir.exists()) vcmiDir.mkdir();
if (!Storage.testH3DataFolder(ctx))
{
// no h3 data present -> instruct user where to put it
return InitResult.failure(
ctx.getString(
R.string.launcher_error_h3_data_missing,
Storage.getVcmiDataDir(ctx)));
}
final File testVcmiData = new File(vcmiInternalDir, "Mods/vcmi/mod.json");
final boolean internalVcmiDataExisted = testVcmiData.exists();
if (!internalVcmiDataExisted && !FileUtil.unpackVcmiDataToInternalDir(vcmiInternalDir, ctx.getAssets()))
{
// no h3 data present -> instruct user where to put it
return InitResult.failure(ctx.getString(R.string.launcher_error_vcmi_data_internal_missing));
}
final String previousInternalDataHash = callbacks.prefs().load(SharedPrefs.KEY_CURRENT_INTERNAL_ASSET_HASH, null);
final String currentInternalDataHash = FileUtil.readAssetsStream(ctx.getAssets(), "internalDataHash.txt");
if (currentInternalDataHash == null || previousInternalDataHash == null || !currentInternalDataHash.equals(previousInternalDataHash))
{
// we should update the data only if it existed previously (hash is bound to be empty if we have just created the data)
if (internalVcmiDataExisted)
{
Log.i(this, "Internal data needs to be created/updated; old hash=" + previousInternalDataHash
+ ", new hash=" + currentInternalDataHash);
if (!FileUtil.reloadVcmiDataToInternalDir(vcmiInternalDir, ctx.getAssets()))
{
return InitResult.failure(ctx.getString(R.string.launcher_error_vcmi_data_internal_update));
}
}
callbacks.prefs().save(SharedPrefs.KEY_CURRENT_INTERNAL_ASSET_HASH, currentInternalDataHash);
}
return InitResult.success();
}
@Override
protected InitResult doInBackground(final Void... params)
{
return init();
}
@Override
protected void onPostExecute(final InitResult initResult)
{
final ILauncherCallbacks callbacks = mCallbackRef.get();
if (callbacks == null)
{
return;
}
if (initResult.mSuccess)
{
callbacks.onInitSuccess();
}
else
{
callbacks.onInitFailure(initResult);
}
}
public interface ILauncherCallbacks
{
Activity ctx();
SharedPrefs prefs();
void onInitSuccess();
void onInitFailure(InitResult result);
}
public static final class InitResult
{
public final boolean mSuccess;
public final String mMessage;
public boolean mFailSilently;
public InitResult(final boolean success, final String message)
{
mSuccess = success;
mMessage = message;
}
@Override
public String toString()
{
return String.format("success: %s (%s)", mSuccess, mMessage);
}
public static InitResult failure(String message)
{
return new InitResult(false, message);
}
public static InitResult success()
{
return new InitResult(true, "");
}
}
}

View File

@ -1,52 +0,0 @@
package eu.vcmi.vcmi.content;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import java.io.IOException;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public class DialogAuthors extends DialogFragment
{
@NonNull
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState)
{
final LayoutInflater inflater = LayoutInflater.from(getActivity());
@SuppressLint("InflateParams") final View inflated = inflater.inflate(R.layout.dialog_authors, null, false);
final TextView vcmiAuthorsView = (TextView) inflated.findViewById(R.id.dialog_authors_vcmi);
final TextView launcherAuthorsView = (TextView) inflated.findViewById(R.id.dialog_authors_launcher);
loadAuthorsContent(vcmiAuthorsView, launcherAuthorsView);
return new AlertDialog.Builder(getActivity())
.setView(inflated)
.create();
}
private void loadAuthorsContent(final TextView vcmiAuthorsView, final TextView launcherAuthorsView)
{
try
{
// to be checked if this should be converted to async load (not really a file operation so it should be okay)
final String authorsContent = "See ingame credits";
vcmiAuthorsView.setText(authorsContent);
launcherAuthorsView.setText("Fay"); // TODO hardcoded for now
}
catch (final Exception e)
{
Log.e(this, "Could not load authors content", e);
}
}
}

View File

@ -1,36 +0,0 @@
package eu.vcmi.vcmi.content;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class ModBaseViewHolder extends RecyclerView.ViewHolder
{
final View mModNesting;
final TextView mModName;
ModBaseViewHolder(final View parentView)
{
this(
LayoutInflater.from(parentView.getContext()).inflate(
R.layout.mod_base_adapter_item,
(ViewGroup) parentView,
false),
true);
}
protected ModBaseViewHolder(final View v, final boolean internal)
{
super(v);
mModNesting = itemView.findViewById(R.id.mods_adapter_item_nesting);
mModName = (TextView) itemView.findViewById(R.id.mods_adapter_item_name);
}
}

View File

@ -1,254 +0,0 @@
package eu.vcmi.vcmi.content;
import android.content.Context;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.mods.VCMIMod;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public class ModsAdapter extends RecyclerView.Adapter<ModBaseViewHolder>
{
private static final int NESTING_WIDTH_PER_LEVEL = 16;
private static final int VIEWTYPE_MOD = 0;
private static final int VIEWTYPE_FAILED_MOD = 1;
private final List<ModItem> mDataset = new ArrayList<>();
private final IOnItemAction mItemListener;
public ModsAdapter(final IOnItemAction itemListener)
{
mItemListener = itemListener;
}
@Override
public ModBaseViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType)
{
switch (viewType)
{
case VIEWTYPE_MOD:
return new ModsViewHolder(parent);
case VIEWTYPE_FAILED_MOD:
return new ModBaseViewHolder(parent);
default:
Log.e(this, "Unhandled view type: " + viewType);
return null;
}
}
@Override
public void onBindViewHolder(final ModBaseViewHolder holder, final int position)
{
final ModItem item = mDataset.get(position);
final int viewType = getItemViewType(position);
final Context ctx = holder.itemView.getContext();
holder.mModNesting.getLayoutParams().width = item.mNestingLevel * NESTING_WIDTH_PER_LEVEL;
switch (viewType)
{
case VIEWTYPE_MOD:
final ModsViewHolder modHolder = (ModsViewHolder) holder;
modHolder.mModName.setText(item.mMod.mName + ", " + item.mMod.mVersion);
modHolder.mModType.setText(item.mMod.mModType);
if (item.mMod.mSize > 0)
{
modHolder.mModSize.setVisibility(View.VISIBLE);
// TODO unit conversion
modHolder.mModSize.setText(String.format(Locale.getDefault(), "%.1f kB", item.mMod.mSize / 1024.0f));
}
else
{
modHolder.mModSize.setVisibility(View.GONE);
}
modHolder.mModAuthor.setText(ctx.getString(R.string.mods_item_author_template, item.mMod.mAuthor));
modHolder.mStatusIcon.setImageResource(selectModStatusIcon(item.mMod.mActive));
modHolder.mDownloadBtn.setVisibility(View.GONE);
modHolder.mDownloadProgress.setVisibility(View.GONE);
modHolder.mUninstall.setVisibility(View.GONE);
if(!item.mMod.mSystem)
{
if (item.mDownloadProgress != null)
{
modHolder.mDownloadProgress.setText(item.mDownloadProgress);
modHolder.mDownloadProgress.setVisibility(View.VISIBLE);
}
else if (!item.mMod.mInstalled)
{
modHolder.mDownloadBtn.setVisibility(View.VISIBLE);
}
else if (item.mMod.installationFolder != null)
{
modHolder.mUninstall.setVisibility(View.VISIBLE);
}
modHolder.itemView.setOnClickListener(v -> mItemListener.onItemPressed(item, holder));
modHolder.mStatusIcon.setOnClickListener(v -> mItemListener.onTogglePressed(item, holder));
modHolder.mDownloadBtn.setOnClickListener(v -> mItemListener.onDownloadPressed(item, holder));
modHolder.mUninstall.setOnClickListener(v -> mItemListener.onUninstall(item, holder));
}
break;
case VIEWTYPE_FAILED_MOD:
holder.mModName.setText(ctx.getString(R.string.mods_failed_mod_loading, item.mMod.mName));
break;
default:
Log.e(this, "Unhandled view type: " + viewType);
break;
}
}
private int selectModStatusIcon(final boolean active)
{
// TODO distinguishing mods that aren't downloaded or have an update available
if (active)
{
return R.drawable.ic_star_full;
}
return R.drawable.ic_star_empty;
}
@Override
public int getItemViewType(final int position)
{
return mDataset.get(position).mMod.mLoadedCorrectly ? VIEWTYPE_MOD : VIEWTYPE_FAILED_MOD;
}
@Override
public int getItemCount()
{
return mDataset.size();
}
public void attachSubmods(final ModItem mod, final RecyclerView.ViewHolder vh)
{
int adapterPosition = vh.getAdapterPosition();
final List<ModItem> submods = new ArrayList<>();
for (VCMIMod v : mod.mMod.submods())
{
ModItem modItem = new ModItem(v, mod.mNestingLevel + 1);
submods.add(modItem);
}
mDataset.addAll(adapterPosition + 1, submods);
notifyItemRangeInserted(adapterPosition + 1, submods.size());
}
public void detachSubmods(final ModItem mod, final RecyclerView.ViewHolder vh)
{
final int adapterPosition = vh.getAdapterPosition();
final int checkedPosition = adapterPosition + 1;
int detachedElements = 0;
while (checkedPosition < mDataset.size() && mDataset.get(checkedPosition).mNestingLevel > mod.mNestingLevel)
{
++detachedElements;
mDataset.remove(checkedPosition);
}
notifyItemRangeRemoved(checkedPosition, detachedElements);
}
public void updateModsList(List<VCMIMod> mods)
{
mDataset.clear();
List<ModItem> list = new ArrayList<>();
for (VCMIMod mod : mods)
{
ModItem modItem = new ModItem(mod);
list.add(modItem);
}
mDataset.addAll(list);
notifyDataSetChanged();
}
public void modInstalled(ModItem mod, File modFolder)
{
try
{
mod.mMod.updateFromModInfo(modFolder);
mod.mMod.mLoadedCorrectly = true;
mod.mMod.mActive = true; // active by default
mod.mMod.mInstalled = true;
mod.mMod.installationFolder = modFolder;
mod.mDownloadProgress = null;
notifyItemChanged(mDataset.indexOf(mod));
}
catch (Exception ex)
{
Log.e("Failed to install mod", ex);
}
}
public void downloadProgress(ModItem mod, String progress)
{
mod.mDownloadProgress = progress;
notifyItemChanged(mDataset.indexOf(mod));
}
public void modRemoved(ModItem item)
{
int itemIndex = mDataset.indexOf(item);
if(item.mMod.mArchiveUrl != null && item.mMod.mArchiveUrl != "")
{
item.mMod.mInstalled = false;
item.mMod.installationFolder = null;
notifyItemChanged(itemIndex);
}
else
{
mDataset.remove(item);
notifyItemRemoved(itemIndex);
}
}
public interface IOnItemAction
{
void onItemPressed(final ModItem mod, final RecyclerView.ViewHolder vh);
void onDownloadPressed(final ModItem mod, final RecyclerView.ViewHolder vh);
void onTogglePressed(ModItem item, ModBaseViewHolder holder);
void onUninstall(ModItem item, ModBaseViewHolder holder);
}
public static class ModItem
{
public final VCMIMod mMod;
public int mNestingLevel;
public boolean mExpanded;
public String mDownloadProgress;
public ModItem(final VCMIMod mod)
{
this(mod, 0);
}
public ModItem(final VCMIMod mod, final int nestingLevel)
{
mMod = mod;
mNestingLevel = nestingLevel;
mExpanded = false;
}
@Override
public String toString()
{
return mMod.toString();
}
}
}

View File

@ -1,35 +0,0 @@
package eu.vcmi.vcmi.content;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class ModsViewHolder extends ModBaseViewHolder
{
final TextView mModAuthor;
final TextView mModType;
final TextView mModSize;
final ImageView mStatusIcon;
final View mDownloadBtn;
final TextView mDownloadProgress;
final View mUninstall;
ModsViewHolder(final View parentView)
{
super(LayoutInflater.from(parentView.getContext()).inflate(R.layout.mods_adapter_item, (ViewGroup) parentView, false), true);
mModAuthor = (TextView) itemView.findViewById(R.id.mods_adapter_item_author);
mModType = (TextView) itemView.findViewById(R.id.mods_adapter_item_modtype);
mModSize = (TextView) itemView.findViewById(R.id.mods_adapter_item_size);
mDownloadBtn = itemView.findViewById(R.id.mods_adapter_item_btn_download);
mStatusIcon = (ImageView) itemView.findViewById(R.id.mods_adapter_item_status);
mDownloadProgress = (TextView) itemView.findViewById(R.id.mods_adapter_item_install_progress);
mUninstall = itemView.findViewById(R.id.mods_adapter_item_btn_uninstall);
}
}

View File

@ -1,258 +0,0 @@
package eu.vcmi.vcmi.mods;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import eu.vcmi.vcmi.BuildConfig;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public class VCMIMod
{
protected final Map<String, VCMIMod> mSubmods;
public String mId;
public String mName;
public String mDesc;
public String mVersion;
public String mAuthor;
public String mContact;
public String mModType;
public String mArchiveUrl;
public long mSize;
public File installationFolder;
// config values
public boolean mActive;
public boolean mInstalled;
public boolean mValidated;
public String mChecksum;
// internal
public boolean mLoadedCorrectly;
public boolean mSystem;
protected VCMIMod()
{
mSubmods = new HashMap<>();
}
public static VCMIMod buildFromRepoJson(final String id,
final JSONObject obj,
JSONObject modDownloadData)
{
final VCMIMod mod = new VCMIMod();
mod.mId = id.toLowerCase(Locale.US);
mod.mName = obj.optString("name");
mod.mDesc = obj.optString("description");
mod.mVersion = obj.optString("version");
mod.mAuthor = obj.optString("author");
mod.mContact = obj.optString("contact");
mod.mModType = obj.optString("modType");
mod.mArchiveUrl = modDownloadData.optString("download");
mod.mSize = obj.optLong("size");
mod.mLoadedCorrectly = true;
return mod;
}
public static VCMIMod buildFromConfigJson(final String id, final JSONObject obj) throws JSONException
{
final VCMIMod mod = new VCMIMod();
mod.updateFromConfigJson(id, obj);
mod.mInstalled = true;
return mod;
}
public static VCMIMod buildFromModInfo(final File modPath) throws IOException, JSONException
{
final VCMIMod mod = new VCMIMod();
if (!mod.updateFromModInfo(modPath))
{
return mod;
}
mod.mLoadedCorrectly = true;
mod.mActive = true; // active by default
mod.mInstalled = true;
mod.installationFolder = modPath;
return mod;
}
protected static Map<String, VCMIMod> loadSubmods(final List<File> modsList) throws IOException, JSONException
{
final Map<String, VCMIMod> submods = new HashMap<>();
for (final File f : modsList)
{
if (!f.isDirectory())
{
Log.w("VCMI", "Non-directory encountered in mods dir: " + f.getName());
continue;
}
final VCMIMod submod = buildFromModInfo(f);
if (submod == null)
{
Log.w(null, "Could not build mod in folder " + f + "; ignoring");
continue;
}
submods.put(submod.mId, submod);
}
return submods;
}
public void updateFromConfigJson(final String id, final JSONObject obj) throws JSONException
{
if(mSystem)
{
return;
}
mId = id.toLowerCase(Locale.US);
mActive = obj.optBoolean("active");
mValidated = obj.optBoolean("validated");
mChecksum = obj.optString("checksum");
final JSONObject submods = obj.optJSONObject("mods");
if (submods != null)
{
updateChildrenFromConfigJson(submods);
}
}
protected void updateChildrenFromConfigJson(final JSONObject submods) throws JSONException
{
final JSONArray names = submods.names();
for (int i = 0; i < names.length(); ++i)
{
final String modId = names.getString(i);
final String normalizedModId = modId.toLowerCase(Locale.US);
if (!mSubmods.containsKey(normalizedModId))
{
Log.w(this, "Mod present in config but not found in /Mods; ignoring: " + modId);
continue;
}
mSubmods.get(normalizedModId).updateFromConfigJson(modId, submods.getJSONObject(modId));
}
}
public boolean updateFromModInfo(final File modPath) throws IOException, JSONException
{
final File modInfoFile = new File(modPath, "mod.json");
if (!modInfoFile.exists())
{
Log.w(this, "Mod info doesn't exist");
mName = modPath.getAbsolutePath();
return false;
}
try
{
final JSONObject modInfoContent = new JSONObject(FileUtil.read(modInfoFile));
mId = modPath.getName().toLowerCase(Locale.US);
mName = modInfoContent.optString("name");
mDesc = modInfoContent.optString("description");
mVersion = modInfoContent.optString("version");
mAuthor = modInfoContent.optString("author");
mContact = modInfoContent.optString("contact");
mModType = modInfoContent.optString("modType");
mSystem = mId.equals("vcmi");
final File submodsDir = new File(modPath, "Mods");
if (submodsDir.exists())
{
final List<File> submodsFiles = new ArrayList<>();
Collections.addAll(submodsFiles, submodsDir.listFiles());
mSubmods.putAll(loadSubmods(submodsFiles));
}
return true;
}
catch (final JSONException ex)
{
mName = modPath.getAbsolutePath();
return false;
}
}
@Override
public String toString()
{
if (!BuildConfig.DEBUG)
{
return "";
}
return String.format("mod:[id:%s,active:%s,submods:[%s]]", mId, mActive, TextUtils.join(",", mSubmods.values()));
}
protected void submodsToJson(final JSONObject modsRoot) throws JSONException
{
for (final VCMIMod submod : mSubmods.values())
{
final JSONObject submodEntry = new JSONObject();
submod.toJsonInternal(submodEntry);
modsRoot.put(submod.mId, submodEntry);
}
}
protected void toJsonInternal(final JSONObject root) throws JSONException
{
root.put("active", mActive);
root.put("validated", mValidated);
if (!TextUtils.isEmpty(mChecksum))
{
root.put("checksum", mChecksum);
}
if (!mSubmods.isEmpty())
{
JSONObject submods = new JSONObject();
submodsToJson(submods);
root.put("mods", submods);
}
}
public boolean hasSubmods()
{
return !mSubmods.isEmpty();
}
public List<VCMIMod> submods()
{
final ArrayList<VCMIMod> ret = new ArrayList<>();
ret.addAll(mSubmods.values());
Collections.sort(ret, new Comparator<VCMIMod>()
{
@Override
public int compare(VCMIMod left, VCMIMod right)
{
return left.mName.compareTo(right.mName);
}
});
return ret;
}
protected void updateFrom(VCMIMod other)
{
this.mModType = other.mModType;
this.mAuthor = other.mAuthor;
this.mDesc = other.mDesc;
this.mArchiveUrl = other.mArchiveUrl;
}
}

View File

@ -1,106 +0,0 @@
package eu.vcmi.vcmi.mods;
import android.text.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import eu.vcmi.vcmi.BuildConfig;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public class VCMIModContainer extends VCMIMod
{
private VCMIMod mCoreStatus; // kept here to correctly save core object to modSettings
private VCMIModContainer()
{
}
public static VCMIModContainer createContainer(final List<File> modsList) throws IOException, JSONException
{
final VCMIModContainer mod = new VCMIModContainer();
mod.mSubmods.putAll(loadSubmods(modsList));
return mod;
}
public void updateContainerFromConfigJson(final JSONObject modsList, final JSONObject coreStatus) throws JSONException
{
updateChildrenFromConfigJson(modsList);
if (coreStatus != null)
{
mCoreStatus = VCMIMod.buildFromConfigJson("core", coreStatus);
}
}
public void updateFromRepo(List<VCMIMod> repoMods){
for (VCMIMod mod : repoMods)
{
final String normalizedModId = mod.mId.toLowerCase(Locale.US);
if(mSubmods.containsKey(normalizedModId)){
VCMIMod existing = mSubmods.get(normalizedModId);
existing.updateFrom(mod);
}
else{
mSubmods.put(normalizedModId, mod);
}
}
}
@Override
public String toString()
{
if (!BuildConfig.DEBUG)
{
return "";
}
return String.format("mods:[%s]", TextUtils.join(",", mSubmods.values()));
}
public void saveToFile(final File location)
{
try
{
FileUtil.write(location, toJson());
}
catch (Exception e)
{
Log.e(this, "Could not save mod settings", e);
}
}
protected String toJson() throws JSONException
{
final JSONObject root = new JSONObject();
final JSONObject activeMods = new JSONObject();
final JSONObject coreStatus = new JSONObject();
root.put("activeMods", activeMods);
submodsToJson(activeMods);
coreStatusToJson(coreStatus);
root.put("core", coreStatus);
return root.toString();
}
private void coreStatusToJson(final JSONObject coreStatus) throws JSONException
{
if (mCoreStatus == null)
{
mCoreStatus = new VCMIMod();
mCoreStatus.mId = "core";
mCoreStatus.mActive = true;
}
mCoreStatus.toJsonInternal(coreStatus);
}
}

View File

@ -1,108 +0,0 @@
package eu.vcmi.vcmi.mods;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.util.AsyncRequest;
import eu.vcmi.vcmi.util.Log;
import eu.vcmi.vcmi.util.ServerResponse;
/**
* @author F
*/
public class VCMIModsRepo
{
private final List<VCMIMod> mModsList;
private IOnModsRepoDownloaded mCallback;
public VCMIModsRepo()
{
mModsList = new ArrayList<>();
}
public void init(final String url, final IOnModsRepoDownloaded callback)
{
mCallback = callback;
new AsyncLoadRepo().execute(url);
}
public interface IOnModsRepoDownloaded
{
void onSuccess(ServerResponse<List<VCMIMod>> response);
void onError(final int code);
}
private class AsyncLoadRepo extends AsyncRequest<List<VCMIMod>>
{
@Override
protected ServerResponse<List<VCMIMod>> doInBackground(final String... params)
{
ServerResponse<List<VCMIMod>> serverResponse = sendRequest(params[0]);
if (serverResponse.isValid())
{
final List<VCMIMod> mods = new ArrayList<>();
try
{
JSONObject jsonContent = new JSONObject(serverResponse.mRawContent);
final JSONArray names = jsonContent.names();
for (int i = 0; i < names.length(); ++i)
{
try
{
String name = names.getString(i);
JSONObject modDownloadData = jsonContent.getJSONObject(name);
if(modDownloadData.has("mod"))
{
String modFileAddress = modDownloadData.getString("mod");
ServerResponse<List<VCMIMod>> modFile = sendRequest(modFileAddress);
if (!modFile.isValid())
{
continue;
}
JSONObject modJson = new JSONObject(modFile.mRawContent);
mods.add(VCMIMod.buildFromRepoJson(name, modJson, modDownloadData));
}
else
{
mods.add(VCMIMod.buildFromRepoJson(name, modDownloadData, modDownloadData));
}
}
catch (JSONException e)
{
Log.e(this, "Could not parse the response as json", e);
}
}
serverResponse.mContent = mods;
}
catch (JSONException e)
{
Log.e(this, "Could not parse the response as json", e);
serverResponse.mCode = ServerResponse.LOCAL_ERROR_PARSING;
}
}
return serverResponse;
}
@Override
protected void onPostExecute(final ServerResponse<List<VCMIMod>> response)
{
if (response.isValid())
{
mModsList.clear();
mModsList.addAll(response.mContent);
mCallback.onSuccess(response);
}
else
{
mCallback.onError(response.mCode);
}
}
}
}

View File

@ -1,46 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class AdventureAiController extends LauncherSettingWithDialogController<String, Config>
{
public AdventureAiController(final AppCompatActivity activity)
{
super(activity);
}
@Override
protected LauncherSettingDialog<String> dialog()
{
return new AdventureAiSelectionDialog();
}
@Override
public void onItemChosen(final String item)
{
mConfig.setAdventureAi(item);
updateContent();
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_adventure_ai_title);
}
@Override
protected String subText()
{
if (mConfig == null)
{
return "";
}
return mConfig.getAdventureAi();
}
}

View File

@ -1,37 +0,0 @@
package eu.vcmi.vcmi.settings;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class AdventureAiSelectionDialog extends LauncherSettingDialog<String>
{
private static final List<String> AVAILABLE_AI = new ArrayList<>();
static
{
AVAILABLE_AI.add("VCAI");
AVAILABLE_AI.add("Nullkiller");
}
public AdventureAiSelectionDialog()
{
super(AVAILABLE_AI);
}
@Override
protected int dialogTitleResId()
{
return R.string.launcher_btn_adventure_ai_title;
}
@Override
protected CharSequence itemName(final String item)
{
return item;
}
}

View File

@ -1,193 +0,0 @@
package eu.vcmi.vcmi.settings;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.view.View;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import androidx.appcompat.app.AppCompatActivity;
import androidx.documentfile.provider.DocumentFile;
import androidx.loader.content.AsyncTaskLoader;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.Storage;
import eu.vcmi.vcmi.util.FileUtil;
public class CopyDataController extends LauncherSettingController<Void, Void>
{
public static final int PICK_EXTERNAL_VCMI_DATA_TO_COPY = 3;
private String progress;
public CopyDataController(final AppCompatActivity act)
{
super(act);
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_import_title);
}
@Override
protected String subText()
{
if (progress != null)
{
return progress;
}
return mActivity.getString(R.string.launcher_btn_import_description);
}
@Override
public void onClick(final View v)
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "vcmi-data")));
mActivity.startActivityForResult(intent, PICK_EXTERNAL_VCMI_DATA_TO_COPY);
}
public void copyData(Uri folderToCopy)
{
AsyncCopyData copyTask = new AsyncCopyData(mActivity, folderToCopy);
copyTask.execute();
}
private class AsyncCopyData extends AsyncTask<String, String, Boolean>
{
private Activity owner;
private Uri folderToCopy;
public AsyncCopyData(Activity owner, Uri folderToCopy)
{
this.owner = owner;
this.folderToCopy = folderToCopy;
}
@Override
protected Boolean doInBackground(final String... params)
{
File targetDir = Storage.getVcmiDataDir(owner);
DocumentFile sourceDir = DocumentFile.fromTreeUri(owner, folderToCopy);
ArrayList<String> allowedFolders = new ArrayList<String>();
allowedFolders.add("Data");
allowedFolders.add("Mp3");
allowedFolders.add("Maps");
allowedFolders.add("Saves");
allowedFolders.add("Mods");
allowedFolders.add("config");
return copyDirectory(targetDir, sourceDir, allowedFolders);
}
@Override
protected void onPostExecute(Boolean result)
{
super.onPostExecute(result);
if (result)
{
CopyDataController.this.progress = null;
CopyDataController.this.updateContent();
}
}
@Override
protected void onProgressUpdate(String... values)
{
CopyDataController.this.progress = values[0];
CopyDataController.this.updateContent();
}
private boolean copyDirectory(File targetDir, DocumentFile sourceDir, List<String> allowed)
{
if (!targetDir.exists())
{
targetDir.mkdir();
}
for (DocumentFile child : sourceDir.listFiles())
{
if (allowed != null)
{
boolean fileAllowed = false;
for (String str : allowed)
{
if (str.equalsIgnoreCase(child.getName()))
{
fileAllowed = true;
break;
}
}
if (!fileAllowed)
continue;
}
File exported = new File(targetDir, child.getName());
if (child.isFile())
{
publishProgress(owner.getString(R.string.launcher_progress_copy,
child.getName()));
if (!exported.exists())
{
try
{
exported.createNewFile();
}
catch (IOException e)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
}
try (
final OutputStream targetStream = new FileOutputStream(exported, false);
final InputStream sourceStream = owner.getContentResolver()
.openInputStream(child.getUri()))
{
FileUtil.copyStream(sourceStream, targetStream);
}
catch (IOException e)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
}
if (child.isDirectory() && !copyDirectory(exported, child, null))
{
return false;
}
}
return true;
}
}
}

View File

@ -1,19 +0,0 @@
package eu.vcmi.vcmi.settings;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.util.SharedPrefs;
/**
* @author F
*/
public class DoubleConfig
{
public final Config mConfig;
public final SharedPrefs mPrefs;
public DoubleConfig(final Config config, final SharedPrefs prefs)
{
mConfig = config;
mPrefs = prefs;
}
}

View File

@ -1,174 +0,0 @@
package eu.vcmi.vcmi.settings;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.view.View;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import androidx.appcompat.app.AppCompatActivity;
import androidx.documentfile.provider.DocumentFile;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.Storage;
import eu.vcmi.vcmi.util.FileUtil;
public class ExportDataController extends LauncherSettingController<Void, Void>
{
public static final int PICK_DIRECTORY_TO_EXPORT = 4;
private String progress;
public ExportDataController(final AppCompatActivity act)
{
super(act);
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_export_title);
}
@Override
protected String subText()
{
if (progress != null)
{
return progress;
}
return mActivity.getString(R.string.launcher_btn_export_description);
}
@Override
public void onClick(final View v)
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "vcmi-data")));
mActivity.startActivityForResult(intent, PICK_DIRECTORY_TO_EXPORT);
}
public void copyData(Uri targetFolder)
{
AsyncCopyData copyTask = new AsyncCopyData(mActivity, targetFolder);
copyTask.execute();
}
private class AsyncCopyData extends AsyncTask<String, String, Boolean>
{
private Activity owner;
private Uri targetFolder;
public AsyncCopyData(Activity owner, Uri targetFolder)
{
this.owner = owner;
this.targetFolder = targetFolder;
}
@Override
protected Boolean doInBackground(final String... params)
{
File targetDir = Storage.getVcmiDataDir(owner);
DocumentFile sourceDir = DocumentFile.fromTreeUri(owner, targetFolder);
return copyDirectory(targetDir, sourceDir);
}
@Override
protected void onPostExecute(Boolean result)
{
super.onPostExecute(result);
if (result)
{
ExportDataController.this.progress = null;
ExportDataController.this.updateContent();
}
}
@Override
protected void onProgressUpdate(String... values)
{
ExportDataController.this.progress = values[0];
ExportDataController.this.updateContent();
}
private boolean copyDirectory(File sourceDir, DocumentFile targetDir)
{
for (File child : sourceDir.listFiles())
{
DocumentFile exported = targetDir.findFile(child.getName());
if (child.isFile())
{
publishProgress(owner.getString(R.string.launcher_progress_copy,
child.getName()));
if (exported == null)
{
try
{
exported = targetDir.createFile(
"application/octet-stream",
child.getName());
}
catch (UnsupportedOperationException e)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
}
if (exported == null)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
try(
final OutputStream targetStream = owner.getContentResolver()
.openOutputStream(exported.getUri());
final InputStream sourceStream = new FileInputStream(child))
{
FileUtil.copyStream(sourceStream, targetStream);
}
catch (IOException e)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
}
if (child.isDirectory())
{
if (exported == null)
{
exported = targetDir.createDirectory(child.getName());
}
if(!copyDirectory(child, exported))
{
return false;
}
}
}
return true;
}
}
}

View File

@ -1,48 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class LanguageSettingController extends LauncherSettingWithDialogController<String, Config>
{
public LanguageSettingController(final AppCompatActivity activity)
{
super(activity);
}
@Override
protected LauncherSettingDialog<String> dialog()
{
return new LanguageSettingDialog();
}
@Override
public void onItemChosen(final String item)
{
mConfig.updateLanguage(item);
updateContent();
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_language_title);
}
@Override
protected String subText()
{
if (mConfig == null)
{
return "";
}
return mConfig.mLanguage == null || mConfig.mLanguage.isEmpty()
? mActivity.getString(R.string.launcher_btn_language_subtitle_unknown)
: mActivity.getString(R.string.launcher_btn_language_subtitle, mConfig.mLanguage);
}
}

View File

@ -1,55 +0,0 @@
package eu.vcmi.vcmi.settings;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class LanguageSettingDialog extends LauncherSettingDialog<String>
{
private static final List<String> AVAILABLE_LANGUAGES = new ArrayList<>();
static
{
AVAILABLE_LANGUAGES.add("english");
AVAILABLE_LANGUAGES.add("czech");
AVAILABLE_LANGUAGES.add("chinese");
AVAILABLE_LANGUAGES.add("finnish");
AVAILABLE_LANGUAGES.add("french");
AVAILABLE_LANGUAGES.add("german");
AVAILABLE_LANGUAGES.add("hungarian");
AVAILABLE_LANGUAGES.add("italian");
AVAILABLE_LANGUAGES.add("korean");
AVAILABLE_LANGUAGES.add("polish");
AVAILABLE_LANGUAGES.add("portuguese");
AVAILABLE_LANGUAGES.add("russian");
AVAILABLE_LANGUAGES.add("spanish");
AVAILABLE_LANGUAGES.add("swedish");
AVAILABLE_LANGUAGES.add("turkish");
AVAILABLE_LANGUAGES.add("ukrainian");
AVAILABLE_LANGUAGES.add("vietnamese");
AVAILABLE_LANGUAGES.add("other_cp1250");
AVAILABLE_LANGUAGES.add("other_cp1251");
AVAILABLE_LANGUAGES.add("other_cp1252");
}
public LanguageSettingDialog()
{
super(AVAILABLE_LANGUAGES);
}
@Override
protected int dialogTitleResId()
{
return R.string.launcher_btn_language_title;
}
@Override
protected CharSequence itemName(final String item)
{
return item;
}
}

View File

@ -1,75 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public abstract class LauncherSettingController<TSetting, TConf> implements View.OnClickListener
{
protected AppCompatActivity mActivity;
protected TConf mConfig;
private View mSettingViewRoot;
private TextView mSettingsTextMain;
private TextView mSettingsTextSub;
LauncherSettingController(final AppCompatActivity act)
{
mActivity = act;
}
public final LauncherSettingController<TSetting, TConf> init(final int rootViewResId)
{
return init(rootViewResId, null);
}
public final LauncherSettingController<TSetting, TConf> init(final int rootViewResId, final TConf config)
{
mSettingViewRoot = mActivity.findViewById(rootViewResId);
mSettingViewRoot.setOnClickListener(this);
mSettingsTextMain = (TextView) mSettingViewRoot.findViewById(R.id.inc_launcher_btn_main);
mSettingsTextSub = (TextView) mSettingViewRoot.findViewById(R.id.inc_launcher_btn_sub);
childrenInit(mSettingViewRoot);
updateConfig(config);
updateContent();
return this;
}
protected void childrenInit(final View root)
{
}
public void updateConfig(final TConf conf)
{
mConfig = conf;
updateContent();
}
public void updateContent()
{
mSettingsTextMain.setText(mainText());
if (mSettingsTextSub != null)
{
mSettingsTextSub.setText(subText());
}
}
protected abstract String mainText();
protected abstract String subText();
public void show()
{
mSettingViewRoot.setVisibility(View.VISIBLE);
}
public void hide()
{
mSettingViewRoot.setVisibility(View.GONE);
}
}

View File

@ -1,73 +0,0 @@
package eu.vcmi.vcmi.settings;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public abstract class LauncherSettingDialog<T> extends DialogFragment
{
protected final List<T> mDataset;
private IOnItemChosen<T> mObserver;
protected LauncherSettingDialog(final List<T> dataset)
{
mDataset = dataset;
}
public void observe(final IOnItemChosen<T> observer)
{
mObserver = observer;
}
protected abstract CharSequence itemName(T item);
protected abstract int dialogTitleResId();
@NonNull
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState)
{
List<CharSequence> list = new ArrayList<>();
for (T t : mDataset)
{
CharSequence charSequence = itemName(t);
list.add(charSequence);
}
return new AlertDialog.Builder(getActivity())
.setTitle(dialogTitleResId())
.setItems(
list.toArray(new CharSequence[0]),
this::onItemChosenInternal)
.create();
}
private void onItemChosenInternal(final DialogInterface dialog, final int index)
{
final T chosenItem = mDataset.get(index);
Log.d(this, "Chosen item: " + chosenItem);
dialog.dismiss();
if (mObserver != null)
{
mObserver.onItemChosen(chosenItem);
}
}
public interface IOnItemChosen<V>
{
void onItemChosen(V item);
}
}

View File

@ -1,31 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public abstract class LauncherSettingWithDialogController<T, Conf> extends LauncherSettingController<T, Conf>
implements LauncherSettingDialog.IOnItemChosen<T>
{
public static final String SETTING_DIALOG_ID = "settings.dialog";
protected LauncherSettingWithDialogController(final AppCompatActivity act)
{
super(act);
}
@Override
public void onClick(final View v)
{
Log.i(this, "Showing dialog");
final LauncherSettingDialog<T> dialog = dialog();
dialog.observe(this); // TODO rebinding dialogs on activity config changes
dialog.show(mActivity.getSupportFragmentManager(), SETTING_DIALOG_ID);
}
protected abstract LauncherSettingDialog<T> dialog();
}

View File

@ -1,83 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatSeekBar;
import android.view.View;
import android.widget.SeekBar;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public abstract class LauncherSettingWithSliderController<T, Conf> extends LauncherSettingController<T, Conf>
{
private AppCompatSeekBar mSlider;
private final int mSliderMin;
private final int mSliderMax;
protected LauncherSettingWithSliderController(final AppCompatActivity act, final int min, final int max)
{
super(act);
mSliderMin = min;
mSliderMax = max;
}
@Override
protected void childrenInit(final View root)
{
mSlider = (AppCompatSeekBar) root.findViewById(R.id.inc_launcher_btn_slider);
if (mSliderMax <= mSliderMin)
{
throw new IllegalArgumentException("slider min>=max");
}
mSlider.setMax(mSliderMax - mSliderMin);
mSlider.setOnSeekBarChangeListener(new OnValueChangedListener());
}
protected abstract void onValueChanged(final int v);
protected abstract int currentValue();
@Override
public void updateContent()
{
super.updateContent();
mSlider.setProgress(currentValue() + mSliderMin);
}
@Override
protected String subText()
{
return null; // not used with slider settings
}
@Override
public void onClick(final View v)
{
// not used with slider settings
}
private class OnValueChangedListener implements SeekBar.OnSeekBarChangeListener
{
@Override
public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser)
{
if (fromUser)
{
onValueChanged(progress);
}
}
@Override
public void onStartTrackingTouch(final SeekBar seekBar)
{
}
@Override
public void onStopTrackingTouch(final SeekBar seekBar)
{
}
}
}

View File

@ -1,38 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class ModsBtnController extends LauncherSettingController<Void, Void>
{
private View.OnClickListener mOnSelectedAction;
public ModsBtnController(final AppCompatActivity act, final View.OnClickListener onSelectedAction)
{
super(act);
mOnSelectedAction = onSelectedAction;
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_mods_title);
}
@Override
protected String subText()
{
return mActivity.getString(R.string.launcher_btn_mods_subtitle);
}
@Override
public void onClick(final View v)
{
mOnSelectedAction.onClick(v);
}
}

View File

@ -1,40 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class MusicSettingController extends LauncherSettingWithSliderController<Integer, Config>
{
public MusicSettingController(final AppCompatActivity act)
{
super(act, 0, 100);
}
@Override
protected void onValueChanged(final int v)
{
mConfig.updateMusic(v);
updateContent();
}
@Override
protected int currentValue()
{
if (mConfig == null)
{
return Config.DEFAULT_MUSIC_VALUE;
}
return mConfig.mVolumeMusic;
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_music_title);
}
}

View File

@ -1,63 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class PointerModeSettingController
extends LauncherSettingWithDialogController<PointerModeSettingController.PointerMode, Config>
{
public PointerModeSettingController(final AppCompatActivity activity)
{
super(activity);
}
@Override
protected LauncherSettingDialog<PointerMode> dialog()
{
return new PointerModeSettingDialog();
}
@Override
public void onItemChosen(final PointerMode item)
{
mConfig.setPointerMode(item == PointerMode.RELATIVE);
updateContent();
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_pointermode_title);
}
@Override
protected String subText()
{
if (mConfig == null)
{
return "";
}
return mActivity.getString(R.string.launcher_btn_pointermode_subtitle,
PointerModeSettingDialog.pointerModeToUserString(mActivity, getPointerMode()));
}
private PointerMode getPointerMode()
{
if(mConfig.getPointerModeIsRelative())
{
return PointerMode.RELATIVE;
}
return PointerMode.NORMAL;
}
public enum PointerMode
{
NORMAL,
RELATIVE;
}
}

View File

@ -1,55 +0,0 @@
package eu.vcmi.vcmi.settings;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class PointerModeSettingDialog extends LauncherSettingDialog<PointerModeSettingController.PointerMode>
{
private static final List<PointerModeSettingController.PointerMode> POINTER_MODES = new ArrayList<>();
static
{
POINTER_MODES.add(PointerModeSettingController.PointerMode.NORMAL);
POINTER_MODES.add(PointerModeSettingController.PointerMode.RELATIVE);
}
public PointerModeSettingDialog()
{
super(POINTER_MODES);
}
public static String pointerModeToUserString(
final Context ctx,
final PointerModeSettingController.PointerMode pointerMode)
{
switch (pointerMode)
{
default:
return "";
case NORMAL:
return ctx.getString(R.string.misc_pointermode_normal);
case RELATIVE:
return ctx.getString(R.string.misc_pointermode_relative);
}
}
@Override
protected int dialogTitleResId()
{
return R.string.launcher_btn_pointermode_title;
}
@Override
protected CharSequence itemName(final PointerModeSettingController.PointerMode item)
{
return pointerModeToUserString(getContext(), item);
}
}

View File

@ -1,51 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class PointerMultiplierSettingController
extends LauncherSettingWithDialogController<Float, Config>
{
public PointerMultiplierSettingController(final AppCompatActivity activity)
{
super(activity);
}
@Override
protected LauncherSettingDialog<Float> dialog()
{
return new PointerMultiplierSettingDialog();
}
@Override
public void onItemChosen(final Float item)
{
mConfig.setPointerSpeedMultiplier(item);
updateContent();
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_pointermulti_title);
}
@Override
protected String subText()
{
if (mConfig == null)
{
return "";
}
String pointerModeString = PointerMultiplierSettingDialog.pointerMultiplierToUserString(
mConfig.getPointerSpeedMultiplier());
return mActivity.getString(R.string.launcher_btn_pointermulti_subtitle, pointerModeString);
}
}

View File

@ -1,48 +0,0 @@
package eu.vcmi.vcmi.settings;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class PointerMultiplierSettingDialog extends LauncherSettingDialog<Float>
{
private static final List<Float> AVAILABLE_MULTIPLIERS = new ArrayList<>();
static
{
AVAILABLE_MULTIPLIERS.add(1.0f);
AVAILABLE_MULTIPLIERS.add(1.25f);
AVAILABLE_MULTIPLIERS.add(1.5f);
AVAILABLE_MULTIPLIERS.add(1.75f);
AVAILABLE_MULTIPLIERS.add(2.0f);
AVAILABLE_MULTIPLIERS.add(2.5f);
AVAILABLE_MULTIPLIERS.add(3.0f);
}
public PointerMultiplierSettingDialog()
{
super(AVAILABLE_MULTIPLIERS);
}
@Override
protected int dialogTitleResId()
{
return R.string.launcher_btn_pointermode_title;
}
@Override
protected CharSequence itemName(final Float item)
{
return pointerMultiplierToUserString(item);
}
public static String pointerMultiplierToUserString(final float multiplier)
{
return String.format(Locale.US, "%.2fx", multiplier);
}
}

View File

@ -1,64 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class ScreenScaleSettingController extends LauncherSettingWithDialogController<ScreenScaleSettingController.ScreenScale, Config>
{
public ScreenScaleSettingController(final AppCompatActivity activity)
{
super(activity);
}
@Override
protected LauncherSettingDialog<ScreenScale> dialog()
{
return new ScreenScaleSettingDialog(mActivity);
}
@Override
public void onItemChosen(final ScreenScale item)
{
mConfig.updateScreenScale(item.mScreenScale);
updateContent();
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_scale_title);
}
@Override
protected String subText()
{
if (mConfig == null)
{
return "";
}
return mConfig.mScreenScale <= 0
? mActivity.getString(R.string.launcher_btn_scale_subtitle_unknown)
: mActivity.getString(R.string.launcher_btn_scale_subtitle, mConfig.mScreenScale);
}
public static class ScreenScale
{
public int mScreenScale;
public ScreenScale(final int scale)
{
mScreenScale = scale;
}
@Override
public String toString()
{
return mScreenScale + "%";
}
}
}

View File

@ -1,98 +0,0 @@
package eu.vcmi.vcmi.settings;
import android.app.Activity;
import android.graphics.Point;
import android.view.WindowMetrics;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.Storage;
import eu.vcmi.vcmi.util.FileUtil;
/**
* @author F
*/
public class ScreenScaleSettingDialog extends LauncherSettingDialog<ScreenScaleSettingController.ScreenScale>
{
public ScreenScaleSettingDialog(Activity mActivity)
{
super(loadScales(mActivity));
}
@Override
protected int dialogTitleResId()
{
return R.string.launcher_btn_scale_title;
}
@Override
protected CharSequence itemName(final ScreenScaleSettingController.ScreenScale item)
{
return item.toString();
}
public static int[] getSupportedScalingRange(Activity activity) {
Point screenRealSize = new Point();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
WindowMetrics windowMetrics = activity.getWindowManager().getCurrentWindowMetrics();
screenRealSize.x = windowMetrics.getBounds().width();
screenRealSize.y = windowMetrics.getBounds().height();
} else {
activity.getWindowManager().getDefaultDisplay().getRealSize(screenRealSize);
}
if (screenRealSize.x < screenRealSize.y) {
int tmp = screenRealSize.x;
screenRealSize.x = screenRealSize.y;
screenRealSize.y = tmp;
}
// H3 resolution, any resolution smaller than that is not correctly supported
Point minResolution = new Point(800, 600);
// arbitrary limit on *downscaling*. Allow some downscaling, if requested by user. Should be generally limited to 100+ for all but few devices
double minimalScaling = 50;
Point renderResolution = screenRealSize;
double maximalScalingWidth = 100.0 * renderResolution.x / minResolution.x;
double maximalScalingHeight = 100.0 * renderResolution.y / minResolution.y;
double maximalScaling = Math.min(maximalScalingWidth, maximalScalingHeight);
return new int[] { (int)minimalScaling, (int)maximalScaling };
}
private static List<ScreenScaleSettingController.ScreenScale> loadScales(Activity activity)
{
List<ScreenScaleSettingController.ScreenScale> availableScales = new ArrayList<>();
try
{
int[] supportedScalingRange = getSupportedScalingRange(activity);
for (int i = 0; i <= supportedScalingRange[1] + 10 - 1; i += 10)
{
if (i >= supportedScalingRange[0])
availableScales.add(new ScreenScaleSettingController.ScreenScale(i));
}
if(availableScales.isEmpty())
{
availableScales.add(new ScreenScaleSettingController.ScreenScale(100));
}
}
catch(Exception ex)
{
ex.printStackTrace();
availableScales.clear();
availableScales.add(new ScreenScaleSettingController.ScreenScale(100));
}
return availableScales;
}
}

View File

@ -1,40 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class SoundSettingController extends LauncherSettingWithSliderController<Integer, Config>
{
public SoundSettingController(final AppCompatActivity act)
{
super(act, 0, 100);
}
@Override
protected void onValueChanged(final int v)
{
mConfig.updateSound(v);
updateContent();
}
@Override
protected int currentValue()
{
if (mConfig == null)
{
return Config.DEFAULT_SOUND_VALUE;
}
return mConfig.mVolumeSound;
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_sound_title);
}
}

View File

@ -1,39 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.util.GeneratedVersion;
/**
* @author F
*/
public class StartGameController extends LauncherSettingController<Void, Void>
{
private View.OnClickListener mOnSelectedAction;
public StartGameController(final AppCompatActivity act, final View.OnClickListener onSelectedAction)
{
super(act);
mOnSelectedAction = onSelectedAction;
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_start_title);
}
@Override
protected String subText()
{
return mActivity.getString(R.string.launcher_btn_start_subtitle, GeneratedVersion.VCMI_VERSION);
}
@Override
public void onClick(final View v)
{
mOnSelectedAction.onClick(v);
}
}

View File

@ -1,49 +0,0 @@
package eu.vcmi.vcmi.util;
import android.annotation.TargetApi;
import android.os.AsyncTask;
import android.os.Build;
import androidx.annotation.RequiresApi;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;
import eu.vcmi.vcmi.Const;
/**
* @author F
*/
public abstract class AsyncRequest<T> extends AsyncTask<String, Void, ServerResponse<T>>
{
@TargetApi(Const.SUPPRESS_TRY_WITH_RESOURCES_WARNING)
protected ServerResponse<T> sendRequest(final String url)
{
try
{
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
final int responseCode = conn.getResponseCode();
if (!ServerResponse.isResponseCodeValid(responseCode))
{
return new ServerResponse<>(responseCode, null);
}
try (Scanner s = new java.util.Scanner(conn.getInputStream()).useDelimiter("\\A"))
{
final String response = s.hasNext() ? s.next() : "";
return new ServerResponse<>(responseCode, response);
}
catch (final Exception e)
{
Log.e(this, "Request failed: ", e);
}
}
catch (final Exception e)
{
Log.e(this, "Request failed: ", e);
}
return new ServerResponse<>(ServerResponse.LOCAL_ERROR_IO, null);
}
}

View File

@ -1,177 +1,101 @@
package eu.vcmi.vcmi.util;
import android.annotation.TargetApi;
import android.content.res.AssetManager;
import android.os.Environment;
import android.text.TextUtils;
import android.app.Activity;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.List;
import eu.vcmi.vcmi.Const;
import eu.vcmi.vcmi.Storage;
/**
* @author F
*/
@TargetApi(Const.SUPPRESS_TRY_WITH_RESOURCES_WARNING)
public class FileUtil
{
private static final int BUFFER_SIZE = 4096;
public static String read(final InputStream stream) throws IOException
public static boolean copyData(Uri folderToCopy, Activity activity)
{
try (InputStreamReader reader = new InputStreamReader(stream))
{
return readInternal(reader);
}
File targetDir = Storage.getVcmiDataDir(activity);
DocumentFile sourceDir = DocumentFile.fromTreeUri(activity, folderToCopy);
return copyDirectory(targetDir, sourceDir, List.of("Data", "Maps", "Mp3"), activity);
}
public static String read(final File file) throws IOException
private static boolean copyDirectory(File targetDir, DocumentFile sourceDir, @Nullable List<String> allowed, Activity activity)
{
try (FileReader reader = new FileReader(file))
if (!targetDir.exists())
{
return readInternal(reader);
}
catch (final FileNotFoundException ignored)
{
Log.w("Could not load file: " + file);
return null;
}
}
private static String readInternal(final InputStreamReader reader) throws IOException
{
final char[] buffer = new char[BUFFER_SIZE];
int currentRead;
final StringBuilder content = new StringBuilder();
while ((currentRead = reader.read(buffer, 0, BUFFER_SIZE)) >= 0)
{
content.append(buffer, 0, currentRead);
}
return content.toString();
}
public static void write(final File file, final String data) throws IOException
{
if (!ensureWriteable(file))
{
Log.e("Couldn't write " + data + " to " + file);
return;
}
try (final FileWriter fw = new FileWriter(file, false))
{
Log.v(null, "Saving data: " + data + " to " + file.getAbsolutePath());
fw.write(data);
}
}
private static boolean ensureWriteable(final File file)
{
if (file == null)
{
Log.e("Broken path given to fileutil::ensureWriteable");
return false;
targetDir.mkdir();
}
final File dir = file.getParentFile();
if (dir.exists() || dir.mkdirs())
for (DocumentFile child : sourceDir.listFiles())
{
return true;
}
Log.e("Couldn't create dir " + dir);
return false;
}
public static boolean clearDirectory(final File dir)
{
if (dir == null || dir.listFiles() == null)
{
Log.e("Broken path given to fileutil::clearDirectory");
return false;
}
for (final File f : dir.listFiles())
{
if (f.isDirectory() && !clearDirectory(f))
if (allowed != null)
{
return false;
boolean fileAllowed = false;
for (String str : allowed)
{
if (str.equalsIgnoreCase(child.getName()))
{
fileAllowed = true;
break;
}
}
if (!fileAllowed)
continue;
}
if (!f.delete())
File exported = new File(targetDir, child.getName());
if (child.isFile())
{
if (!exported.exists())
{
try
{
exported.createNewFile();
}
catch (IOException e)
{
Log.e(activity, "createNewFile failed: " + e);
return false;
}
}
try (
final OutputStream targetStream = new FileOutputStream(exported, false);
final InputStream sourceStream = activity.getContentResolver()
.openInputStream(child.getUri()))
{
copyStream(sourceStream, targetStream);
}
catch (IOException e)
{
Log.e(activity, "copyStream failed: " + e);
return false;
}
}
if (child.isDirectory() && !copyDirectory(exported, child, null, activity))
{
return false;
}
}
return true;
}
public static void copyDir(final File srcFile, final File dstFile)
{
File[] files = srcFile.listFiles();
if(!dstFile.exists()) dstFile.mkdir();
if(files == null)
return;
for (File child : files){
File childTarget = new File(dstFile, child.getName());
if(child.isDirectory()){
copyDir(child, childTarget);
}
else{
copyFile(child, childTarget);
}
}
}
public static boolean copyFile(final File srcFile, final File dstFile)
{
if (!srcFile.exists())
{
return false;
}
final File dstDir = dstFile.getParentFile();
if (!dstDir.exists())
{
if (!dstDir.mkdirs())
{
Log.w("Couldn't create dir to copy file: " + dstFile);
return false;
}
}
try (final FileInputStream input = new FileInputStream(srcFile);
final FileOutputStream output = new FileOutputStream(dstFile))
{
copyStream(input, output);
return true;
}
catch (final Exception ex)
{
Log.e("Couldn't copy " + srcFile + " to " + dstFile, ex);
return false;
}
}
public static void copyStream(InputStream source, OutputStream target) throws IOException
private static void copyStream(InputStream source, OutputStream target) throws IOException
{
final byte[] buffer = new byte[BUFFER_SIZE];
int read;
@ -180,171 +104,4 @@ public class FileUtil
target.write(buffer, 0, read);
}
}
// (when internal data have changed)
public static boolean reloadVcmiDataToInternalDir(final File vcmiInternalDir, final AssetManager assets)
{
return clearDirectory(vcmiInternalDir) && unpackVcmiDataToInternalDir(vcmiInternalDir, assets);
}
public static boolean unpackVcmiDataToInternalDir(final File vcmiInternalDir, final AssetManager assets)
{
try
{
final InputStream inputStream = assets.open("internalData.zip");
final boolean success = unpackZipFile(inputStream, vcmiInternalDir, null);
inputStream.close();
return success;
}
catch (final Exception e)
{
Log.e("Couldn't extract vcmi data to internal dir", e);
return false;
}
}
public static boolean unpackZipFile(
final File inputFile,
final File destDir,
IZipProgressReporter progressReporter)
{
try
{
final InputStream inputStream = new FileInputStream(inputFile);
final boolean success = unpackZipFile(
inputStream,
destDir,
progressReporter);
inputStream.close();
return success;
}
catch (final Exception e)
{
Log.e("Couldn't extract file to " + destDir, e);
return false;
}
}
public static int countFilesInZip(final File zipFile)
{
int totalEntries = 0;
try
{
final InputStream inputStream = new FileInputStream(zipFile);
ZipInputStream is = new ZipInputStream(inputStream);
ZipEntry zipEntry;
while ((zipEntry = is.getNextEntry()) != null)
{
totalEntries++;
}
is.closeEntry();
is.close();
inputStream.close();
}
catch (final Exception e)
{
Log.e("Couldn't count items in zip", e);
}
return totalEntries;
}
public static boolean unpackZipFile(
final InputStream inputStream,
final File destDir,
final IZipProgressReporter progressReporter)
{
try
{
int unpackedEntries = 0;
final byte[] buffer = new byte[BUFFER_SIZE];
ZipInputStream is = new ZipInputStream(inputStream);
ZipEntry zipEntry;
while ((zipEntry = is.getNextEntry()) != null)
{
final String fileName = zipEntry.getName();
final File newFile = new File(destDir, fileName);
if (newFile.exists())
{
Log.d("Already exists: " + newFile.getName());
continue;
}
else if (zipEntry.isDirectory())
{
Log.v("Creating new dir: " + zipEntry);
if (!newFile.mkdirs())
{
Log.e("Couldn't create directory " + newFile.getAbsolutePath());
return false;
}
continue;
}
final File parentFile = new File(newFile.getParent());
if (!parentFile.exists() && !parentFile.mkdirs())
{
Log.e("Couldn't create directory " + parentFile.getAbsolutePath());
return false;
}
final FileOutputStream fos = new FileOutputStream(newFile, false);
int currentRead;
while ((currentRead = is.read(buffer)) > 0)
{
fos.write(buffer, 0, currentRead);
}
fos.flush();
fos.close();
++unpackedEntries;
if(progressReporter != null)
{
progressReporter.onUnpacked(newFile);
}
}
Log.d("Unpacked data (" + unpackedEntries + " entries)");
is.closeEntry();
is.close();
return true;
}
catch (final Exception e)
{
Log.e("Couldn't extract vcmi data to " + destDir, e);
return false;
}
}
public static String configFileLocation(File filesDir)
{
return filesDir + "/config/settings.json";
}
public static String readAssetsStream(final AssetManager assets, final String assetPath)
{
if (assets == null || TextUtils.isEmpty(assetPath))
{
return null;
}
try (java.util.Scanner s = new java.util.Scanner(assets.open(assetPath), "UTF-8").useDelimiter("\\A"))
{
return s.hasNext() ? s.next() : null;
}
catch (final IOException e)
{
Log.e("Couldn't read stream data", e);
return null;
}
}
}

View File

@ -1,8 +0,0 @@
package eu.vcmi.vcmi.util;
import java.io.File;
public interface IZipProgressReporter
{
void onUnpacked(File newFile);
}

View File

@ -1,198 +0,0 @@
package eu.vcmi.vcmi.util;
import android.content.Context;
import android.nfc.FormatException;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
public class InstallModAsync
extends AsyncTask<String, String, Boolean>
implements IZipProgressReporter
{
private static final String TAG = "DOWNLOADFILE";
private static final int DOWNLOAD_PERCENT = 70;
private PostDownload callback;
private File downloadLocation;
private File extractLocation;
private Context context;
private int totalFiles;
private int unpackedFiles;
public InstallModAsync(File extractLocation, Context context, PostDownload callback)
{
this.context = context;
this.callback = callback;
this.extractLocation = extractLocation;
}
@Override
protected Boolean doInBackground(String... args)
{
int count;
try
{
File modsFolder = extractLocation.getParentFile();
if (!modsFolder.exists()) modsFolder.mkdir();
this.downloadLocation = File.createTempFile("tmp", ".zip", modsFolder);
URL url = new URL(args[0]);
URLConnection connection = url.openConnection();
connection.connect();
long lengthOfFile = -1;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
{
lengthOfFile = connection.getContentLengthLong();
}
if(lengthOfFile == -1)
{
try
{
lengthOfFile = Long.parseLong(connection.getHeaderField("Content-Length"));
Log.d(TAG, "Length of the file: " + lengthOfFile);
} catch (NumberFormatException ex)
{
Log.d(TAG, "Failed to parse content length", ex);
}
}
if(lengthOfFile == -1)
{
lengthOfFile = 100000000;
Log.d(TAG, "Using dummy length of file");
}
InputStream input = new BufferedInputStream(url.openStream());
FileOutputStream output = new FileOutputStream(downloadLocation); //context.openFileOutput("content.zip", Context.MODE_PRIVATE);
Log.d(TAG, "file saved at " + downloadLocation.getAbsolutePath());
byte data[] = new byte[1024];
long total = 0;
while ((count = input.read(data)) != -1)
{
total += count;
output.write(data, 0, count);
this.publishProgress((int) ((total * DOWNLOAD_PERCENT) / lengthOfFile) + "%");
}
output.flush();
output.close();
input.close();
File tempDir = File.createTempFile("tmp", "", modsFolder);
tempDir.delete();
tempDir.mkdir();
if (!extractLocation.exists()) extractLocation.mkdir();
try
{
totalFiles = FileUtil.countFilesInZip(downloadLocation);
unpackedFiles = 0;
FileUtil.unpackZipFile(downloadLocation, tempDir, this);
return moveModToExtractLocation(tempDir);
}
finally
{
downloadLocation.delete();
FileUtil.clearDirectory(tempDir);
tempDir.delete();
}
} catch (Exception e)
{
Log.e(TAG, "Unhandled exception while installing mod", e);
}
return false;
}
@Override
protected void onProgressUpdate(String... values)
{
callback.downloadProgress(values);
}
@Override
protected void onPostExecute(Boolean result)
{
if (callback != null) callback.downloadDone(result, extractLocation);
}
private boolean moveModToExtractLocation(File tempDir)
{
return moveModToExtractLocation(tempDir, 0);
}
private boolean moveModToExtractLocation(File tempDir, int level)
{
File[] modJson = tempDir.listFiles(new FileFilter()
{
@Override
public boolean accept(File file)
{
return file.getName().equalsIgnoreCase("Mod.json");
}
});
if (modJson != null && modJson.length > 0)
{
File modFolder = modJson[0].getParentFile();
if (!modFolder.renameTo(extractLocation))
{
FileUtil.copyDir(modFolder, extractLocation);
}
return true;
}
if (level <= 1)
{
for (File child : tempDir.listFiles())
{
if (child.isDirectory() && moveModToExtractLocation(child, level + 1))
{
return true;
}
}
}
return false;
}
@Override
public void onUnpacked(File newFile)
{
unpackedFiles++;
int progress = DOWNLOAD_PERCENT
+ (unpackedFiles * (100 - DOWNLOAD_PERCENT) / totalFiles);
publishProgress(progress + "%");
}
public interface PostDownload
{
void downloadDone(Boolean succeed, File modFolder);
void downloadProgress(String... progress);
}
}

View File

@ -12,9 +12,12 @@ import eu.vcmi.vcmi.NativeMethods;
*/
public final class LibsLoader
{
public static final String CLIENT_LIB = "vcmiclient_"
+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_ABIS[0] : Build.CPU_ABI);
public static void loadClientLibs(Context ctx)
{
SDL.loadLibrary("vcmiclient");
SDL.loadLibrary(CLIENT_LIB);
SDL.setContext(ctx);
}

View File

@ -1,15 +1,6 @@
package eu.vcmi.vcmi.util;
import android.os.Environment;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;
import eu.vcmi.vcmi.BuildConfig;
import eu.vcmi.vcmi.Const;
/**
* @author F
@ -18,8 +9,6 @@ import eu.vcmi.vcmi.Const;
public class Log
{
private static final boolean LOGGING_ENABLED_CONSOLE = BuildConfig.DEBUG;
private static final boolean LOGGING_ENABLED_FILE = true;
private static final String FILELOG_PATH = "/" + Const.VCMI_DATA_ROOT_FOLDER_NAME + "/cache/VCMI_launcher.log";
private static final String TAG_PREFIX = "VCMI/";
private static final String STATIC_TAG = "static";
@ -34,19 +23,6 @@ public class Log
{
android.util.Log.println(priority, TAG_PREFIX + tagString, msg);
}
if (LOGGING_ENABLED_FILE) // this is probably very inefficient, but should be enough for now...
{
try
{
final BufferedWriter fileWriter = new BufferedWriter(new FileWriter(Environment.getExternalStorageDirectory() + FILELOG_PATH, true));
fileWriter.write(String.format("[%s] %s: %s\n", formatPriority(priority), tagString, msg));
fileWriter.flush();
fileWriter.close();
}
catch (IOException ignored)
{
}
}
}
private static String formatPriority(final int priority)
@ -77,23 +53,6 @@ public class Log
return obj.getClass().getSimpleName();
}
public static void init()
{
if (LOGGING_ENABLED_FILE) // clear previous log
{
try
{
final BufferedWriter fileWriter = new BufferedWriter(new FileWriter(Environment.getExternalStorageDirectory() + FILELOG_PATH, false));
fileWriter.write("Starting VCMI launcher log, " + DateFormat.getDateTimeInstance().format(new Date()) + "\n");
fileWriter.flush();
fileWriter.close();
}
catch (IOException ignored)
{
}
}
}
public static void v(final String msg)
{
logInternal(android.util.Log.VERBOSE, STATIC_TAG, msg);

View File

@ -1,30 +0,0 @@
package eu.vcmi.vcmi.util;
/**
* @author F
*/
public class ServerResponse<T>
{
public static final int LOCAL_ERROR_IO = -1;
public static final int LOCAL_ERROR_PARSING = -2;
public int mCode;
public String mRawContent;
public T mContent;
public ServerResponse(final int code, final String content)
{
mCode = code;
mRawContent = content;
}
public static boolean isResponseCodeValid(final int responseCode)
{
return responseCode >= 200 && responseCode < 300;
}
public boolean isValid()
{
return isResponseCodeValid(mCode);
}
}

View File

@ -1,92 +0,0 @@
package eu.vcmi.vcmi.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
/**
* simple shared preferences wrapper
*
* @author F
*/
public class SharedPrefs
{
public static final String KEY_CURRENT_INTERNAL_ASSET_HASH = "KEY_CURRENT_INTERNAL_ASSET_HASH"; // [string]
private static final String VCMI_PREFS_NAME = "VCMIPrefs";
private final SharedPreferences mPrefs;
public SharedPrefs(final Context ctx)
{
mPrefs = ctx.getSharedPreferences(VCMI_PREFS_NAME, Context.MODE_PRIVATE);
}
public void save(final String name, final String value)
{
mPrefs.edit().putString(name, value).apply();
log(name, value, true);
}
public String load(final String name, final String defaultValue)
{
return log(name, mPrefs.getString(name, defaultValue), false);
}
public void save(final String name, final int value)
{
mPrefs.edit().putInt(name, value).apply();
log(name, value, true);
}
public int load(final String name, final int defaultValue)
{
return log(name, mPrefs.getInt(name, defaultValue), false);
}
public void save(final String name, final float value)
{
mPrefs.edit().putFloat(name, value).apply();
log(name, value, true);
}
public float load(final String name, final float defaultValue)
{
return log(name, mPrefs.getFloat(name, defaultValue), false);
}
public void save(final String name, final boolean value)
{
mPrefs.edit().putBoolean(name, value).apply();
log(name, value, true);
}
public boolean load(final String name, final boolean defaultValue)
{
return log(name, mPrefs.getBoolean(name, defaultValue), false);
}
public <T extends Enum<T>> void saveEnum(final String name, final T value)
{
mPrefs.edit().putInt(name, value.ordinal()).apply();
log(name, value, true);
}
@SuppressWarnings("unchecked")
public <T extends Enum<T>> T loadEnum(final String name, @NonNull final T defaultValue)
{
final int rawValue = mPrefs.getInt(name, defaultValue.ordinal());
return (T) log(name, defaultValue.getClass().getEnumConstants()[rawValue], false);
}
private <T> T log(final String key, final T value, final boolean saving)
{
if (saving)
{
Log.v(this, "[prefs saving] " + key + " => " + value);
}
else
{
Log.v(this, "[prefs loading] " + key + " => " + value);
}
return value;
}
}

View File

@ -1,58 +0,0 @@
package eu.vcmi.vcmi.util;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.util.DisplayMetrics;
/**
* @author F
*/
public final class Utils
{
private static String sAppVersionCache;
private Utils()
{
}
public static String appVersionName(final Context ctx)
{
if (sAppVersionCache == null)
{
final PackageManager pm = ctx.getPackageManager();
try
{
final PackageInfo info = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_META_DATA);
return sAppVersionCache = info.versionName;
}
catch (final PackageManager.NameNotFoundException e)
{
Log.e(ctx, "Couldn't resolve app version", e);
}
}
return sAppVersionCache;
}
public static float convertDpToPx(final Context ctx, final float dp)
{
return convertDpToPx(ctx.getResources(), dp);
}
public static float convertDpToPx(final Resources res, final float dp)
{
return dp * res.getDisplayMetrics().density;
}
public static float convertPxToDp(final Context ctx, final float px)
{
return convertPxToDp(ctx.getResources(), px);
}
public static float convertPxToDp(final Resources res, final float px)
{
return px / res.getDisplayMetrics().density;
}
}

View File

@ -1,52 +0,0 @@
package eu.vcmi.vcmi.viewmodels;
import android.view.View;
import androidx.lifecycle.ViewModel;
import androidx.databinding.PropertyChangeRegistry;
import androidx.databinding.Observable;
/**
* @author F
*/
public class ObservableViewModel extends ViewModel implements Observable
{
private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();
@Override
public void addOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback)
{
callbacks.add(callback);
}
@Override
public void removeOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback)
{
callbacks.remove(callback);
}
public int visible(boolean isVisible)
{
return isVisible ? View.VISIBLE : View.GONE;
}
/**
* Notifies observers that all properties of this instance have changed.
*/
void notifyChange() {
callbacks.notifyCallbacks(this, 0, null);
}
/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the @Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* @param fieldId The generated BR id for the Bindable field.
*/
void notifyPropertyChanged(int fieldId) {
callbacks.notifyCallbacks(this, fieldId, null);
}
}

View File

@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
private static final String TAG = "SDL";
private static final int SDL_MAJOR_VERSION = 2;
private static final int SDL_MINOR_VERSION = 26;
private static final int SDL_MICRO_VERSION = 1;
private static final int SDL_MICRO_VERSION = 5;
/*
// Display InputType.SOURCE/CLASS of events and devices
//
@ -241,7 +241,14 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
* It can be overridden by derived classes.
*/
protected String getMainSharedObject() {
return null;
String library;
String[] libraries = SDLActivity.mSingleton.getLibraries();
if (libraries.length > 0) {
library = "lib" + libraries[libraries.length - 1] + ".so";
} else {
library = "libmain.so";
}
return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
}
/**

View File

@ -168,6 +168,32 @@ class SDLJoystickHandler_API16 extends SDLJoystickHandler {
arg1Axis = MotionEvent.AXIS_GAS;
}
// Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
// This is because the usual pairing are:
// - AXIS_X + AXIS_Y (left stick).
// - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
// - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
// This sorts the axes in the above order, which tends to be correct
// for Xbox-ish game pads that have the right stick on RX/RY and the
// triggers on Z/RZ.
//
// Gamepads that don't have AXIS_Z/AXIS_RZ but use
// AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
//
// References:
// - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
// - https://www.kernel.org/doc/html/latest/input/gamepad.html
if (arg0Axis == MotionEvent.AXIS_Z) {
arg0Axis = MotionEvent.AXIS_RZ - 1;
} else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
--arg0Axis;
}
if (arg1Axis == MotionEvent.AXIS_Z) {
arg1Axis = MotionEvent.AXIS_RZ - 1;
} else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
--arg1Axis;
}
return arg0Axis - arg1Axis;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 B

View File

@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android:startColor="#000000" android:endColor="#00000000" android:angle="270"/>
</shape>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z" />
</vector>

View File

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" />
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4V6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" />
</vector>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<solid android:color="#A0000000" />
<stroke android:color="@color/accent" android:width="1dp" />
</shape>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size
android:width="1px"
android:height="1px" />
<solid android:color="@color/accent" />
</shape>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout>
<com.google.android.material.appbar.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/bgMain">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/bgMain"
android:elevation="6dp"
app:elevation="6dp"
app:title="@string/launcher_title" />
</com.google.android.material.appbar.AppBarLayout>
</layout>

View File

@ -1,71 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar_include">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/side_margin">
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.Header"
android:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_version_app"
style="@style/VCMI.Text"
android:text="@string/about_version_app" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_version_launcher"
style="@style/VCMI.Text"
android:text="@string/about_version_launcher" />
</LinearLayout>
<include layout="@layout/inc_separator" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:text="@string/about_section_project" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_link_portal"
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
android:text="@string/about_links_main" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_link_repo_main"
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
android:text="@string/about_links_repo" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_link_repo_launcher"
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
android:text="@string/about_links_repo_launcher" />
<include layout="@layout/inc_separator" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:text="@string/about_section_legal" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_btn_authors"
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
android:text="@string/about_btn_authors" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_btn_privacy"
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
android:text="@string/about_btn_privacy" />
</LinearLayout>
</ScrollView>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/error_message"
style="@style/VCMI.Text"
android:layout_margin="@dimen/side_margin"
android:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/error_btn_try_again"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_margin="@dimen/side_margin"
android:layout_marginTop="20dp"
android:text="@string/misc_try_again" />
</LinearLayout>

View File

@ -16,4 +16,4 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
</FrameLayout>

View File

@ -1,106 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar_include"
android:clipChildren="false"
android:clipToPadding="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/launcher_version_info"
style="@style/VCMI.Text"
android:padding="@dimen/side_margin"
android:text="@string/app_name" />
<include layout="@layout/inc_separator" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="2dp"
android:text="@string/launcher_section_init"
app:elevation="2dp" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/launcher_btn_start"
layout="@layout/inc_launcher_btn" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/launcher_error"
style="@style/VCMI.Text"
android:drawableLeft="@drawable/ic_error"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:minHeight="80dp"
android:padding="@dimen/side_margin"
android:text="@string/app_name" />
<ProgressBar
android:id="@+id/launcher_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
<include
android:id="@+id/launcher_btn_copy"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_export"
layout="@layout/inc_launcher_btn" />
<include layout="@layout/inc_separator" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/launcher_section_settings" />
<include
android:id="@+id/launcher_btn_mods"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_scale"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_adventure_ai"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_cp"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_pointer_mode"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_pointer_multi"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_volume_sound"
layout="@layout/inc_launcher_slider" />
<include
android:id="@+id/launcher_btn_volume_music"
layout="@layout/inc_launcher_slider" />
</LinearLayout>
</ScrollView>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:id="@+id/mods_data_root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar_include"
android:clipChildren="false"
android:clipToPadding="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mods_recycler"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/mods_adapter_item" />
<TextView
android:id="@+id/mods_error_text"
style="@style/VCMI.Text"
android:layout_marginTop="30dp"
android:gravity="center" />
<ProgressBar
android:id="@+id/mods_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/toolbar_include"
layout="@layout/inc_toolbar" />
<ViewStub
android:id="@+id/toolbar_wrapper_content_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar_include" />
</RelativeLayout>
</layout>

View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:text="@string/dialog_authors_vcmi" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialog_authors_vcmi"
style="@style/VCMI.Text"
android:padding="@dimen/side_margin" />
<include layout="@layout/inc_separator" />
<!-- TODO should this be separate or just merged with vcmi authors? -->
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:text="@string/dialog_authors_launcher" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialog_authors_launcher"
style="@style/VCMI.Text"
android:padding="@dimen/side_margin" />
</LinearLayout>
</ScrollView>

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="title"
type="java.lang.String" />
<variable
name="description"
type="java.lang.String" />
</data>
<RelativeLayout
style="@style/VCMI.Entry.Clickable"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:id="@+id/inc_launcher_btn_main"
style="@style/VCMI.Text.LauncherEntry"
android:text="@{title}" />
<TextView
android:id="@+id/inc_launcher_btn_sub"
style="@style/VCMI.Text.LauncherEntry.Sub"
android:text="@{description}" />
</LinearLayout>
</RelativeLayout>
</layout>

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
style="@style/VCMI.Entry"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:id="@+id/inc_launcher_btn_main"
style="@style/VCMI.Text.LauncherEntry"
android:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/inc_launcher_btn_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
</LinearLayout>
</RelativeLayout>
</layout>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/separator" />
</layout>

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:clipChildren="false"
android:clipToPadding="false">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/bgMain"
android:elevation="6dp"
app:elevation="6dp"
app:title="@string/launcher_title" />
<ImageView
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="-4dp"
android:background="@drawable/compat_toolbar_shadow" />
</RelativeLayout>
</layout>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<View
android:id="@+id/mods_adapter_item_nesting"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="@color/accent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/mods_adapter_item_name"
style="@style/VCMI.Text"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_marginLeft="8dp"
android:drawableLeft="@drawable/ic_error"
android:drawablePadding="18dp"
android:gravity="center_vertical"
android:padding="@dimen/side_margin"
android:text="@string/mods_failed_mod_loading" />
</LinearLayout>

View File

@ -1,101 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/selectableItemBackground"
android:minHeight="@dimen/entry_min_height"
android:orientation="horizontal">
<View
android:id="@+id/mods_adapter_item_nesting"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="@color/accent" />
<LinearLayout
style="@style/VCMI.Entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:elevation="4dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingBottom="5dp"
android:paddingLeft="@dimen/side_margin"
android:paddingTop="5dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/mods_adapter_item_status"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginRight="10dp"
android:src="@drawable/ic_star_full" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:layout_weight="1"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/mods_adapter_item_name"
style="@style/VCMI.Text.ModName"
android:ellipsize="end"
android:lines="1"
android:text="mod name, v1.0" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/mods_adapter_item_author"
style="@style/VCMI.Text.ModAuthor"
android:ellipsize="end"
android:lines="1"
android:text="by mod author" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/mods_adapter_item_modtype"
style="@style/VCMI.Text.ModType"
android:layout_width="wrap_content"
android:layout_gravity="right"
android:text="tools" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/mods_adapter_item_size"
style="@style/VCMI.Text.ModSize"
android:layout_width="wrap_content"
android:layout_gravity="right"
android:text="1000 MB" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/mods_adapter_item_btn_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/side_margin"
android:src="@android:drawable/stat_sys_download" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/mods_adapter_item_btn_uninstall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/side_margin"
android:src="@android:drawable/ic_menu_delete" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/mods_adapter_item_install_progress"
android:padding="@dimen/side_margin" />
</LinearLayout>
</LinearLayout>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_launcher_about"
android:icon="@drawable/ic_info"
android:title="@string/menu_launcher_about"
app:showAsAction="always" />
</menu>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_mods_download_repo"
android:icon="@android:drawable/stat_sys_download"
android:title="@string/menu_mods_download_repo"
app:showAsAction="ifRoom" />
</menu>

View File

@ -1,71 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="url_project_page" translatable="false">https://vcmi.eu</string>
<string name="url_project_repo" translatable="false">https://github.com/vcmi/vcmi</string>
<string name="url_launcher_repo" translatable="false">https://github.com/vcmi/vcmi-android</string>
<string name="url_launcher_privacy" translatable="false">https://github.com/vcmi/vcmi/blob/master/docs/players/Privacy_Policy.md</string>
<string name="app_name">VCMI</string>
<string name="server_name">Server VCMI</string>
<string name="launcher_title">Spouštěč VCMI</string>
<string name="launcher_btn_scale_title">Škálování herního rozlišení</string>
<string name="launcher_btn_scale_subtitle_unknown">Současné: neznámé</string>
<string name="launcher_btn_scale_subtitle">Současné: %1$d%%</string>
<string name="launcher_btn_start_title">Spustit VCMI</string>
<string name="launcher_btn_start_subtitle">Současná verze VCMI: %1$s</string>
<string name="launcher_btn_mods_title">Modifikace</string>
<string name="launcher_btn_mods_subtitle">Nainstalovat nové frakce, přeměty a bonusy</string>
<string name="launcher_btn_language_title">Jazyk</string>
<string name="launcher_btn_language_subtitle_unknown">Současný: neznámý</string>
<string name="launcher_btn_language_subtitle">Současný: %1$s</string>
<string name="launcher_btn_pointermode_title">Změnit režim ukazatele</string>
<string name="launcher_btn_pointermode_subtitle">Současný: %1$s</string>
<string name="launcher_btn_pointermulti_title">Násobitel rychlosti relativního ukazatele</string>
<string name="launcher_btn_pointermulti_subtitle">Současný: %1$s</string>
<string name="launcher_btn_sound_title">Hlasitost zvuků</string>
<string name="launcher_btn_music_title">Hlasitost hudby</string>
<string name="launcher_btn_adventure_ai">AI světa</string>
<string name="launcher_btn_adventure_ai_title">Změnit AI světa</string>
<string name="launcher_btn_import_title">Importovat data VCMI</string>
<string name="launcher_btn_import_description">Zkopírovat soubury VCMI do vestavěného úložiště. Můžete importovat starou složku dat vcmi z vydání 0.99 nebo soubory HOMM3</string>
<string name="launcher_btn_export_title">Exportovat data VCMI</string>
<string name="launcher_btn_export_description">Udělat kopii dat VCMI před odinstalací nebo pro synchronizaci s desktopovou verzí. Též můžete přímo přistoupit k interním datům.</string>
<string name="launcher_progress_copy">Kopírování %1$s</string>
<string name="launcher_version">Současná verze spouštěče: %1$s</string>
<string name="launcher_error_vcmi_data_root_failed">Nelze vytvořit datovou složku VCMI v %1$s.</string>
<string name="launcher_error_h3_data_missing">Nelze najít datovou složku v \'%1$s\'. Vložte do ní své datové soubory HoMM3 nebo použijte tlačítko níže. Možná budete muset také restartovat aplikaci.</string>
<string name="launcher_error_vcmi_data_internal_missing">Nelze najít nebo rozbalit data vcmi ze zdrojů aplikace. Zkuste přeinstalovat aplikaci.</string>
<string name="launcher_error_vcmi_data_internal_update">Nelze aktualizovat data vcmi ze zdrojů aplikace. Zkuste přeinstalovat aplikaci.</string>
<string name="launcher_error_permissions">Tato aplikace potřebuje oprávnění k zápisu pro použití obsahu na externím úložišti</string>
<string name="launcher_error_permission_broken">Nelze správně vyřešit oprávnění</string>
<string name="mods_item_author_template">od %1$s</string>
<string name="misc_try_again">Zkusit znovu</string>
<string name="launcher_section_init">Inicializae hry</string>
<string name="launcher_section_settings">Nastavení</string>
<string name="menu_mods_download_repo">Stáhnout data repozitáře</string>
<string name="misc_pointermode_normal">Normální</string>
<string name="misc_pointermode_relative">Relativní</string>
<string name="menu_launcher_about">O spouštěči</string>
<string name="mods_title">Nalezené modifikace</string>
<string name="mods_failed_mod_loading">Nelze načíst modifikaci ve složce \'%1$s\'</string>
<string name="mods_removal_title">Odebírání %1$s</string>
<string name="mods_removal_confirmation">Jste si jisti odebráním %1$s?</string>
<string name="about_title">O aplikaci</string>
<string name="about_version_app">Verze aplikace: %1$s</string>
<string name="about_version_launcher">Verze spouštěče: %1$s</string>
<string name="about_section_project">Projekt</string>
<string name="about_section_legal">Právní záležitosti</string>
<string name="about_links_main">Hlavní stránka: %1$s</string>
<string name="about_links_repo">Repozitář projektu: %1$s</string>
<string name="about_links_repo_launcher">Repozitář spouštěče: %1$s</string>
<string name="about_btn_authors">Autoři</string>
<string name="about_btn_privacy">Zásady ochrany osobních údajů: %1$s</string>
<string name="about_error_opening_url">Nebylo možné otevřít webovou stránku (nenalezena patřičná aplikace)</string>
<string name="dialog_authors_vcmi">Autoři VCMI</string>
<string name="dialog_authors_launcher">Autoři spouštěče</string>
<string name="launcher_error_config_saving_failed">Nelze uložit konfigurační soubor VCMI; důvod: %1$s</string>
</resources>

View File

@ -1,62 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">VCMI</string>
<string name="launcher_btn_start_title">VCMI starten</string>
<string name="launcher_title">VCMI-Starter</string>
<string name="launcher_btn_scale_title">Skalierung der Spielauflösung</string>
<string name="launcher_btn_scale_subtitle_unknown">Aktuell: unbekannt</string>
<string name="launcher_btn_scale_subtitle">Aktuell: %1$d%%</string>
<string name="server_name">VCMI-Server</string>
<string name="launcher_btn_start_subtitle">Aktuelle VCMI-Version: %1$s</string>
<string name="launcher_btn_mods_title">Mods</string>
<string name="launcher_btn_mods_subtitle">Neue Burgen, Kreaturen, Objekte und Erweiterungen hinzufügen</string>
<string name="launcher_btn_language_title">Sprache</string>
<string name="launcher_btn_language_subtitle_unknown">Aktuell: unbekannt</string>
<string name="launcher_btn_language_subtitle">Aktuell: %1$s</string>
<string name="launcher_btn_pointermode_title">Zeigermodus ändern</string>
<string name="launcher_btn_pointermode_subtitle">Aktuell: %1$s</string>
<string name="launcher_btn_pointermulti_subtitle">Aktuell: %1$s</string>
<string name="launcher_version">Aktuelle Version des Starters: %1$s</string>
<string name="launcher_error_h3_data_missing">Der Datenordner in \'%1$s\' konnte nicht gefunden werden. Legen Sie Ihre HoMM3-Datendateien dort ab oder verwenden Sie die Schaltfläche unten, um diese zu importieren. Möglicherweise müssen Sie die Anwendung neu starten.</string>
<string name="launcher_btn_pointermulti_title">Multiplikator der relativen Zeigergeschwindigkeit</string>
<string name="launcher_btn_sound_title">Lautstärke der Geräusche</string>
<string name="launcher_btn_music_title">Lautstärke der Musik</string>
<string name="launcher_btn_adventure_ai">Abenteuer KI</string>
<string name="launcher_btn_adventure_ai_title">Abenteuer KI ändern</string>
<string name="launcher_error_vcmi_data_root_failed">VCMI-Datenordner in %1$s konnte nicht erstellt werden.</string>
<string name="launcher_error_vcmi_data_internal_missing">Ressourcendateien konnten nicht extrahiert werden. Versuchen Sie, die Anwendung neu zu installieren.</string>
<string name="launcher_error_vcmi_data_internal_update">Die Ressourcendateien konnten nicht aktualisiert werden. Versuchen Sie, die Anwendung neu zu installieren.</string>
<string name="launcher_error_permissions">Die Anwendung benötigt Rechte, um Inhalte auf externen Speicher zu schreiben.</string>
<string name="launcher_error_permission_broken">Keine Berechtigung erhalten.</string>
<string name="launcher_error_config_saving_failed">Einstellungen konnten nicht gespeichert werden; Grund: %1$s</string>
<string name="mods_item_author_template">Autor %1$s</string>
<string name="misc_try_again">Erneut versuchen</string>
<string name="launcher_section_init">Initialisierung</string>
<string name="launcher_section_settings">Einstellungen</string>
<string name="menu_mods_download_repo">Liste der Mods herunterladen</string>
<string name="misc_pointermode_normal">Normal</string>
<string name="misc_pointermode_relative">Relativ</string>
<string name="menu_launcher_about">Über</string>
<string name="mods_title">Installierte Mods</string>
<string name="mods_failed_mod_loading">Die Mod in dem Ordner \'%1$s\' kann nicht geladen werden.</string>
<string name="mods_removal_title">%1$s löschen</string>
<string name="mods_removal_confirmation">Sind Sie sicher, dass Sie %1$s löschen wollen?</string>
<string name="about_title">Über die Anwendung</string>
<string name="about_version_app">Version der Anwendung: %1$s</string>
<string name="about_version_launcher">Version des Startprogramms: %1$s</string>
<string name="about_section_project">Projekt</string>
<string name="about_section_legal">Rechtliches</string>
<string name="about_links_main">Website: %1$s</string>
<string name="about_links_repo">Projekt-Repository: %1$s</string>
<string name="about_links_repo_launcher">Starter-Repository: %1$s</string>
<string name="about_btn_authors">Autoren</string>
<string name="about_error_opening_url">Die Seite kann nicht geöffnet werden (wahrscheinlich konnte kein Browser gefunden werden)</string>
<string name="dialog_authors_vcmi">VCMI-Autoren</string>
<string name="dialog_authors_launcher">Autoren des Starters</string>
<string name="about_btn_privacy">Datenschutzbestimmungen: %1$s</string>
<string name="launcher_btn_export_title">VCMI-Daten exportieren</string>
<string name="launcher_btn_export_description">Erstellen Sie eine Kopie der internen VCMI-Daten vor der Deinstallation oder zur Synchronisierung der Speicherstände mit der Desktop-Version. Sie können auch direkt auf den internen Speicher zugreifen.</string>
<string name="launcher_btn_import_title">VCMI-Daten importieren</string>
<string name="launcher_btn_import_description">Kopieren Sie VCMI-Dateien in den internen Speicher. Sie können den alten vcmi-data-Ordner von Version 0.99 oder HoMM3-Dateien importieren</string>
<string name="launcher_progress_copy">Kopiere %1$s</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More