mirror of
https://github.com/vcmi/vcmi.git
synced 2025-05-31 22:59:54 +02:00
add existing android files
This commit is contained in:
parent
cc966902cd
commit
7d4f8ab70d
9
android/.gitignore
vendored
Normal file
9
android/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
9
android/GeneratedVersion.java.in
Normal file
9
android/GeneratedVersion.java.in
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package eu.vcmi.vcmi.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated via cmake (./project/vcmi-app/cmake-scripts/versions.cmake)
|
||||||
|
*/
|
||||||
|
public class GeneratedVersion
|
||||||
|
{
|
||||||
|
public static final String VCMI_VERSION = "@VCMI_VERSION@";
|
||||||
|
}
|
21
android/build.gradle
Normal file
21
android/build.gradle
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
apply from: rootProject.file("defs.gradle")
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
27
android/defs.gradle
Normal file
27
android/defs.gradle
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import groovy.json.JsonSlurper
|
||||||
|
|
||||||
|
ext {
|
||||||
|
final def jsonFile = rootProject.file("../vcmiconf.json")
|
||||||
|
final def rawConf = new JsonSlurper().parseText(jsonFile.text)
|
||||||
|
|
||||||
|
PROJECT_PATH_BASE = jsonFile.getParentFile().getAbsolutePath().replace('\\', '/')
|
||||||
|
VCMI_PATH_EXT = "${PROJECT_PATH_BASE}/ext"
|
||||||
|
VCMI_PATH_MAIN = "${PROJECT_PATH_BASE}/project"
|
||||||
|
|
||||||
|
VCMI_PATH_VCMI = "${VCMI_PATH_EXT}/vcmi"
|
||||||
|
|
||||||
|
// can be 16 if building only for armeabi-v7a, definitely needs to be 21+ to build arm64 and x86_64
|
||||||
|
VCMI_PLATFORM = rawConf.androidApi
|
||||||
|
// we should be able to use the newest version to compile, but it seems that gradle-experimental is somehow broken and doesn't compile native libs correctly for apis older than this setting...
|
||||||
|
VCMI_COMPILE_SDK = 26
|
||||||
|
VCMI_ABIS = rawConf.abis.split(" ")
|
||||||
|
|
||||||
|
VCMI_STL_VERSION = "c++_shared"
|
||||||
|
VCMI_BUILD_TOOLS = "25.0.2"
|
||||||
|
|
||||||
|
// these values will be retrieved during gradle build
|
||||||
|
gitInfoLauncher = "none"
|
||||||
|
gitInfoVcmi = "none"
|
||||||
|
|
||||||
|
//logger.info("Base path = ${PROJECT_PATH_BASE}")
|
||||||
|
}
|
0
android/defs.gradle.properties
Normal file
0
android/defs.gradle.properties
Normal file
19
android/gradle.properties
Normal file
19
android/gradle.properties
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# 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
|
||||||
|
# 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
|
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#Tue Sep 27 21:00:27 EEST 2022
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
185
android/gradlew
vendored
Executable file
185
android/gradlew
vendored
Executable file
@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
89
android/gradlew.bat
vendored
Normal file
89
android/gradlew.bat
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
9
android/settings.gradle
Normal file
9
android/settings.gradle
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "VCMI"
|
||||||
|
include ':vcmi-app'
|
1
android/vcmi-app/.gitignore
vendored
Normal file
1
android/vcmi-app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
214
android/vcmi-app/build.gradle
Normal file
214
android/vcmi-app/build.gradle
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk 31
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "is.xyz.vcmi"
|
||||||
|
minSdk 19
|
||||||
|
targetSdk 31
|
||||||
|
versionCode 1103
|
||||||
|
versionName "1.1"
|
||||||
|
setProperty("archivesBaseName", "vcmi")
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
version "3.18+"
|
||||||
|
arguments "-DANDROID_STL=${VCMI_STL_VERSION}",
|
||||||
|
"-DANDROID_NATIVE_API_LEVEL=${VCMI_PLATFORM}",
|
||||||
|
"-DANDROID_TOOLCHAIN=clang",
|
||||||
|
"-DVCMI_ROOT=${PROJECT_PATH_BASE}"
|
||||||
|
cppFlags "-frtti", "-fexceptions", "-Wno-switch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ndk {
|
||||||
|
abiFilters = new HashSet<>()
|
||||||
|
abiFilters.addAll(VCMI_ABIS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
releaseSigning
|
||||||
|
LoadSigningConfig(PROJECT_PATH_BASE)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
jniLibs.srcDirs = ["${PROJECT_PATH_BASE}/ext-output"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
zipAlignEnabled true
|
||||||
|
signingConfig signingConfigs.releaseSigning
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all { variant -> RenameOutput(project.archivesBaseName, variant) }
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile) {
|
||||||
|
options.compilerArgs += ["-Xlint:deprecation"]
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
version "3.18.0+"
|
||||||
|
path file('cmake-scripts/CMakeLists.txt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
dataBinding true
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions "vcmi"
|
||||||
|
productFlavors {
|
||||||
|
VcmiOnly {
|
||||||
|
dimension "vcmi"
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
version "3.18+"
|
||||||
|
targets "vcmi",
|
||||||
|
"vcmiserver",
|
||||||
|
"vcmiclient"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LibsOnly {
|
||||||
|
dimension "vcmi"
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
version "3.18+"
|
||||||
|
targets "boost-datetime",
|
||||||
|
"boost-system",
|
||||||
|
"boost-filesystem",
|
||||||
|
"boost-locale",
|
||||||
|
"boost-program-options",
|
||||||
|
"boost-thread",
|
||||||
|
"fl-shared",
|
||||||
|
"minizip"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AllTargets {
|
||||||
|
dimension "vcmi"
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
version "3.18+"
|
||||||
|
targets "boost-datetime",
|
||||||
|
"boost-system",
|
||||||
|
"boost-filesystem",
|
||||||
|
"boost-locale",
|
||||||
|
"boost-program-options",
|
||||||
|
"boost-thread",
|
||||||
|
"fl-shared",
|
||||||
|
"minizip",
|
||||||
|
"vcmi",
|
||||||
|
"vcmiserver",
|
||||||
|
"vcmiclient"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def RenameOutput(final baseName, final variant) {
|
||||||
|
final def buildTaskId = System.getenv("GITHUB_RUN_ID")
|
||||||
|
|
||||||
|
ResolveGitInfo()
|
||||||
|
|
||||||
|
def name = baseName + "-" + ext.gitInfoLauncher + "-" + 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("."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def CommandOutput(final cmd, final arguments, final cwd) {
|
||||||
|
try {
|
||||||
|
new ByteArrayOutputStream().withStream { final os ->
|
||||||
|
exec {
|
||||||
|
executable cmd
|
||||||
|
args arguments
|
||||||
|
workingDir cwd
|
||||||
|
standardOutput os
|
||||||
|
}
|
||||||
|
return os.toString().trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final Exception ex) {
|
||||||
|
print("Broken: " + cmd + " " + arguments + " in " + cwd + " :: " + ex.toString())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def ResolveGitInfo() {
|
||||||
|
if (ext.gitInfoLauncher != "none" && ext.gitInfoVcmi != "none") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ext.gitInfoLauncher = CommandOutput("git", ["describe", "--match=", "--always", "--abbrev=7"], PROJECT_PATH_BASE)
|
||||||
|
ext.gitInfoVcmi =
|
||||||
|
CommandOutput("git", ["log", "-1", "--pretty=%D", "--decorate-refs=refs/remotes/origin/*"], PROJECT_PATH_BASE + "/ext/vcmi").replace("origin/", "").replace(", HEAD", "").replaceAll("[^a-zA-Z0-9\\-_]", "_") +
|
||||||
|
"-" +
|
||||||
|
CommandOutput("git", ["describe", "--match=", "--always", "--abbrev=7"], PROJECT_PATH_BASE + "/ext/vcmi")
|
||||||
|
}
|
||||||
|
|
||||||
|
def SigningPropertiesPath(final basePath) {
|
||||||
|
return file(basePath + "/.github/CI/signing.properties")
|
||||||
|
}
|
||||||
|
|
||||||
|
def SigningKeystorePath(final basePath, final keystoreFileName) {
|
||||||
|
return file(basePath + "/.github/CI/" + keystoreFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
def LoadSigningConfig(final basePath) {
|
||||||
|
final def props = new Properties()
|
||||||
|
final def propFile = SigningPropertiesPath(basePath)
|
||||||
|
if (propFile.canRead()) {
|
||||||
|
props.load(new FileInputStream(propFile))
|
||||||
|
|
||||||
|
if (props != null
|
||||||
|
&& props.containsKey('STORE_FILE')
|
||||||
|
&& props.containsKey('STORE_PASSWORD')
|
||||||
|
&& props.containsKey('KEY_ALIAS')
|
||||||
|
&& props.containsKey('KEY_PASSWORD')) {
|
||||||
|
|
||||||
|
android.signingConfigs.releaseSigning.storeFile = SigningKeystorePath(basePath, props['STORE_FILE'])
|
||||||
|
android.signingConfigs.releaseSigning.storePassword = props['STORE_PASSWORD']
|
||||||
|
android.signingConfigs.releaseSigning.keyAlias = props['KEY_ALIAS']
|
||||||
|
android.signingConfigs.releaseSigning.keyPassword = props['KEY_PASSWORD']
|
||||||
|
} else {
|
||||||
|
println("Some props from signing file are missing")
|
||||||
|
android.buildTypes.release.signingConfig = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println("file with signing properties is missing")
|
||||||
|
android.buildTypes.release.signingConfig = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
|
}
|
21
android/vcmi-app/proguard-rules.pro
vendored
Normal file
21
android/vcmi-app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
53
android/vcmi-app/src/main/AndroidManifest.xml
Normal file
53
android/vcmi-app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?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" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:extractNativeLibs="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:hasFragileUserData="true"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:installLocation="auto"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
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>
|
105
android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityAbout.java
Normal file
105
android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityAbout.java
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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);
|
||||||
|
initControlBtn(R.id.about_btn_libs, this::onBtnLibsPressed);
|
||||||
|
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 onBtnLibsPressed(final View v)
|
||||||
|
{
|
||||||
|
// TODO 3rd party libs view (dialog?)
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onBtnPrivacyPressed(final View v)
|
||||||
|
{
|
||||||
|
// TODO tbd if we even need this in app
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,303 @@
|
|||||||
|
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 eu.vcmi.vcmi.content.AsyncLauncherInitialization;
|
||||||
|
import eu.vcmi.vcmi.settings.AdventureAiController;
|
||||||
|
import eu.vcmi.vcmi.settings.CodepageSettingController;
|
||||||
|
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.ScreenResSettingController;
|
||||||
|
import eu.vcmi.vcmi.settings.SoundSettingController;
|
||||||
|
import eu.vcmi.vcmi.settings.StartGameController;
|
||||||
|
import eu.vcmi.vcmi.util.FileUtil;
|
||||||
|
import eu.vcmi.vcmi.util.Log;
|
||||||
|
import eu.vcmi.vcmi.util.SharedPrefs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author F
|
||||||
|
*/
|
||||||
|
public class ActivityLauncher extends ActivityWithToolbar
|
||||||
|
{
|
||||||
|
public static final int PERMISSIONS_REQ_CODE = 123;
|
||||||
|
|
||||||
|
private final List<LauncherSettingController<?, ?>> mActualSettings = new ArrayList<>();
|
||||||
|
private View mProgress;
|
||||||
|
private TextView mErrorMessage;
|
||||||
|
private Config mConfig;
|
||||||
|
private LauncherSettingController<ScreenResSettingController.ScreenRes, Config> mCtrlScreenRes;
|
||||||
|
private LauncherSettingController<String, Config> mCtrlCodepage;
|
||||||
|
private LauncherSettingController<PointerModeSettingController.PointerMode, Config> mCtrlPointerMode;
|
||||||
|
private LauncherSettingController<Void, Void> mCtrlStart;
|
||||||
|
private LauncherSettingController<Float, Config> mCtrlPointerMulti;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(final Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
if (savedInstanceState == null) // only clear the log if this is initial onCreate and not config change
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onActivityResult(requestCode, resultCode, resultData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestStoragePermissions()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||||
|
{
|
||||||
|
requestPermissions(
|
||||||
|
new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||||
|
PERMISSIONS_REQ_CODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSettingsGui()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
mCtrlScreenRes = new ScreenResSettingController(this).init(R.id.launcher_btn_res, mConfig);
|
||||||
|
mCtrlCodepage = new CodepageSettingController(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);
|
||||||
|
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(mCtrlCodepage);
|
||||||
|
mActualSettings.add(mCtrlScreenRes);
|
||||||
|
mActualSettings.add(mCtrlPointerMode);
|
||||||
|
mActualSettings.add(mCtrlPointerMulti);
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
updateCtrlConfig(mCtrlScreenRes, mConfig);
|
||||||
|
updateCtrlConfig(mCtrlCodepage, mConfig);
|
||||||
|
updateCtrlConfig(mCtrlPointerMode, mConfig);
|
||||||
|
updateCtrlConfig(mCtrlPointerMulti, 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);
|
||||||
|
}
|
||||||
|
}
|
331
android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java
Normal file
331
android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
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 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/github.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
233
android/vcmi-app/src/main/java/eu/vcmi/vcmi/Config.java
Normal file
233
android/vcmi-app/src/main/java/eu/vcmi/vcmi/Config.java
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
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_CODEPAGE = "CP1250";
|
||||||
|
public static final int DEFAULT_MUSIC_VALUE = 5;
|
||||||
|
public static final int DEFAULT_SOUND_VALUE = 5;
|
||||||
|
public static final int DEFAULT_SCREEN_RES_W = 800;
|
||||||
|
public static final int DEFAULT_SCREEN_RES_H = 600;
|
||||||
|
|
||||||
|
public String mCodepage;
|
||||||
|
public int mResolutionWidth;
|
||||||
|
public int mResolutionHeight;
|
||||||
|
public boolean mSwipeEnabled;
|
||||||
|
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 accessScreenResNode(final JSONObject baseObj)
|
||||||
|
{
|
||||||
|
if (baseObj == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final JSONObject video = baseObj.optJSONObject("video");
|
||||||
|
if (video != null)
|
||||||
|
{
|
||||||
|
return video.optJSONObject("screenRes");
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
config.mCodepage = loadEntry(general, "encoding", DEFAULT_CODEPAGE);
|
||||||
|
config.mVolumeSound = loadEntry(general, "sound", DEFAULT_SOUND_VALUE);
|
||||||
|
config.mVolumeMusic = loadEntry(general, "music", DEFAULT_MUSIC_VALUE);
|
||||||
|
config.mSwipeEnabled = loadEntry(general, "swipe", true);
|
||||||
|
config.adventureAi = loadEntry(server, "playerAI", "VCAI");
|
||||||
|
config.mUseRelativePointer = loadEntry(general, "userRelativePointer", false);
|
||||||
|
config.mPointerSpeedMultiplier = loadDouble(general, "relativePointerSpeedMultiplier", 1.0);
|
||||||
|
|
||||||
|
final JSONObject screenRes = accessScreenResNode(obj);
|
||||||
|
config.mResolutionWidth = loadEntry(screenRes, "width", DEFAULT_SCREEN_RES_W);
|
||||||
|
config.mResolutionHeight = loadEntry(screenRes, "height", DEFAULT_SCREEN_RES_H);
|
||||||
|
|
||||||
|
config.mRawObject = obj;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateCodepage(final String s)
|
||||||
|
{
|
||||||
|
mCodepage = s;
|
||||||
|
mIsModified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateResolution(final int x, final int y)
|
||||||
|
{
|
||||||
|
mResolutionWidth = x;
|
||||||
|
mResolutionHeight = y;
|
||||||
|
mIsModified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSwipe(final boolean b)
|
||||||
|
{
|
||||||
|
mSwipeEnabled = b;
|
||||||
|
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 ? "VCAI" : 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 screenResNode = accessScreenResNode(mRawObject);
|
||||||
|
|
||||||
|
final JSONObject root = mRawObject == null ? new JSONObject() : mRawObject;
|
||||||
|
final JSONObject general = generalNode == null ? new JSONObject() : generalNode;
|
||||||
|
final JSONObject video = new JSONObject();
|
||||||
|
final JSONObject screenRes = screenResNode == null ? new JSONObject() : screenResNode;
|
||||||
|
final JSONObject server = serverNode == null ? new JSONObject() : serverNode;
|
||||||
|
|
||||||
|
if (mCodepage != null)
|
||||||
|
{
|
||||||
|
general.put("encoding", mCodepage);
|
||||||
|
}
|
||||||
|
|
||||||
|
general.put("swipe", mSwipeEnabled);
|
||||||
|
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 (mResolutionHeight > 0 && mResolutionWidth > 0)
|
||||||
|
{
|
||||||
|
screenRes.put("width", mResolutionWidth);
|
||||||
|
screenRes.put("height", mResolutionHeight);
|
||||||
|
video.put("screenRes", screenRes);
|
||||||
|
root.put("video", video);
|
||||||
|
}
|
||||||
|
|
||||||
|
return root.toString();
|
||||||
|
}
|
||||||
|
}
|
24
android/vcmi-app/src/main/java/eu/vcmi/vcmi/Const.java
Normal file
24
android/vcmi-app/src/main/java/eu/vcmi/vcmi/Const.java
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
|
*/
|
||||||
|
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";
|
||||||
|
}
|
163
android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java
Normal file
163
android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
package eu.vcmi.vcmi;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.Messenger;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
|
||||||
|
import org.libsdl.app.SDL;
|
||||||
|
import org.libsdl.app.SDLActivity;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
import eu.vcmi.vcmi.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author F
|
||||||
|
*/
|
||||||
|
public class NativeMethods
|
||||||
|
{
|
||||||
|
private static WeakReference<Messenger> serverMessengerRef;
|
||||||
|
|
||||||
|
public NativeMethods()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native void initClassloader();
|
||||||
|
|
||||||
|
public static native void clientSetupJNI();
|
||||||
|
|
||||||
|
public static native void createServer();
|
||||||
|
|
||||||
|
public static native void notifyServerReady();
|
||||||
|
|
||||||
|
public static native void notifyServerClosed();
|
||||||
|
|
||||||
|
public static native boolean tryToSaveTheGame();
|
||||||
|
|
||||||
|
public static void setupMsg(final Messenger msg)
|
||||||
|
{
|
||||||
|
serverMessengerRef = new WeakReference<>(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
|
||||||
|
public static String dataRoot()
|
||||||
|
{
|
||||||
|
final Context ctx = SDL.getContext();
|
||||||
|
String root = Storage.getVcmiDataDir(ctx).getAbsolutePath();
|
||||||
|
|
||||||
|
Log.i("Accessing data root: " + root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this path is visible only to this application; we can store base vcmi configs etc. there
|
||||||
|
@SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
|
||||||
|
public static String internalDataRoot()
|
||||||
|
{
|
||||||
|
final Context ctx = SDL.getContext();
|
||||||
|
String root = new File(ctx.getFilesDir(), Const.VCMI_DATA_ROOT_FOLDER_NAME).getAbsolutePath();
|
||||||
|
Log.i("Accessing internal data root: " + root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
|
||||||
|
public static String nativePath()
|
||||||
|
{
|
||||||
|
final Context ctx = SDL.getContext();
|
||||||
|
Log.i("Accessing ndk path: " + ctx.getApplicationInfo().nativeLibraryDir);
|
||||||
|
return ctx.getApplicationInfo().nativeLibraryDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
|
||||||
|
public static void startServer()
|
||||||
|
{
|
||||||
|
Log.i("Got server create request");
|
||||||
|
final Context ctx = SDL.getContext();
|
||||||
|
|
||||||
|
if (!(ctx instanceof VcmiSDLActivity))
|
||||||
|
{
|
||||||
|
Log.e("Unexpected context... " + ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent intent = new Intent(ctx, SDLActivity.class);
|
||||||
|
intent.setAction(VcmiSDLActivity.NATIVE_ACTION_CREATE_SERVER);
|
||||||
|
// I probably do something incorrectly, but sending new intent to the activity "normally" breaks SDL events handling (probably detaches jnienv?)
|
||||||
|
// so instead let's call onNewIntent directly, as out context SHOULD be SDLActivity anyway
|
||||||
|
((VcmiSDLActivity) ctx).hackCallNewIntentDirectly(intent);
|
||||||
|
// ctx.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
|
||||||
|
public static void killServer()
|
||||||
|
{
|
||||||
|
Log.i("Got server close request");
|
||||||
|
|
||||||
|
final Context ctx = SDL.getContext();
|
||||||
|
ctx.stopService(new Intent(ctx, ServerService.class));
|
||||||
|
|
||||||
|
Messenger messenger = requireServerMessenger();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// we need to actually inform client about killing the server, beacuse it needs to unbind service connection before server gets destroyed
|
||||||
|
messenger.send(Message.obtain(null, VcmiSDLActivity.SERVER_MESSAGE_SERVER_KILLED));
|
||||||
|
}
|
||||||
|
catch (RemoteException e)
|
||||||
|
{
|
||||||
|
Log.w("Connection with client process broken?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
|
||||||
|
public static void onServerReady()
|
||||||
|
{
|
||||||
|
Log.i("Got server ready msg");
|
||||||
|
Messenger messenger = requireServerMessenger();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
messenger.send(Message.obtain(null, VcmiSDLActivity.SERVER_MESSAGE_SERVER_READY));
|
||||||
|
}
|
||||||
|
catch (RemoteException e)
|
||||||
|
{
|
||||||
|
Log.w("Connection with client process broken?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
|
||||||
|
public static void showProgress()
|
||||||
|
{
|
||||||
|
internalProgressDisplay(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
|
||||||
|
public static void hideProgress()
|
||||||
|
{
|
||||||
|
internalProgressDisplay(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void internalProgressDisplay(final boolean show)
|
||||||
|
{
|
||||||
|
final Context ctx = SDL.getContext();
|
||||||
|
if (!(ctx instanceof VcmiSDLActivity))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
((SDLActivity) ctx).runOnUiThread(() -> ((VcmiSDLActivity) ctx).displayProgress(show));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Messenger requireServerMessenger()
|
||||||
|
{
|
||||||
|
Messenger msg = serverMessengerRef.get();
|
||||||
|
if (msg == null)
|
||||||
|
{
|
||||||
|
throw new RuntimeException("Broken server messenger");
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
107
android/vcmi-app/src/main/java/eu/vcmi/vcmi/ServerService.java
Normal file
107
android/vcmi-app/src/main/java/eu/vcmi/vcmi/ServerService.java
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package eu.vcmi.vcmi;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.Messenger;
|
||||||
|
|
||||||
|
import org.libsdl.app.SDL;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
import eu.vcmi.vcmi.util.LibsLoader;
|
||||||
|
import eu.vcmi.vcmi.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author F
|
||||||
|
*/
|
||||||
|
public class ServerService extends Service
|
||||||
|
{
|
||||||
|
public static final int CLIENT_MESSAGE_CLIENT_REGISTERED = 1;
|
||||||
|
public static final String INTENT_ACTION_KILL_SERVER = "ServerService.Action.Kill";
|
||||||
|
final Messenger mMessenger = new Messenger(new IncomingClientMessageHandler(new OnClientRegisteredCallback()));
|
||||||
|
private Messenger mClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent)
|
||||||
|
{
|
||||||
|
return mMessenger.getBinder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId)
|
||||||
|
{
|
||||||
|
SDL.setContext(ServerService.this);
|
||||||
|
LibsLoader.loadServerLibs();
|
||||||
|
if (INTENT_ACTION_KILL_SERVER.equals(intent.getAction()))
|
||||||
|
{
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy()
|
||||||
|
{
|
||||||
|
super.onDestroy();
|
||||||
|
Log.i(this, "destroyed");
|
||||||
|
// we need to kill the process to ensure all server data is cleaned up; this isn't a good solution (as we mess with system's
|
||||||
|
// memory management stuff), but clearing all native data manually would be a pain and we can't force close the server "gracefully", because
|
||||||
|
// even after onDestroy call, the system can postpone actually finishing the process -- this would break CVCMIServer initialization
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface IncomingClientMessageHandlerCallback
|
||||||
|
{
|
||||||
|
void onClientRegistered(Messenger client);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ServerStartThread extends Thread
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
NativeMethods.createServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class IncomingClientMessageHandler extends Handler
|
||||||
|
{
|
||||||
|
private WeakReference<IncomingClientMessageHandlerCallback> mCallbackRef;
|
||||||
|
|
||||||
|
IncomingClientMessageHandler(final IncomingClientMessageHandlerCallback callback)
|
||||||
|
{
|
||||||
|
mCallbackRef = new WeakReference<>(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg)
|
||||||
|
{
|
||||||
|
switch (msg.what)
|
||||||
|
{
|
||||||
|
case CLIENT_MESSAGE_CLIENT_REGISTERED:
|
||||||
|
final IncomingClientMessageHandlerCallback callback = mCallbackRef.get();
|
||||||
|
if (callback != null)
|
||||||
|
{
|
||||||
|
callback.onClientRegistered(msg.replyTo);
|
||||||
|
}
|
||||||
|
NativeMethods.setupMsg(msg.replyTo);
|
||||||
|
new ServerStartThread().start();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
super.handleMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OnClientRegisteredCallback implements IncomingClientMessageHandlerCallback
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onClientRegistered(final Messenger client)
|
||||||
|
{
|
||||||
|
mClient = client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java
Normal file
31
android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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");
|
||||||
|
return testH3Data.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getH3DataFolder(Context context){
|
||||||
|
return getVcmiDataDir(context).getAbsolutePath();
|
||||||
|
}
|
||||||
|
}
|
206
android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java
Normal file
206
android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
package eu.vcmi.vcmi;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.Messenger;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.libsdl.app.SDLActivity;
|
||||||
|
|
||||||
|
import eu.vcmi.vcmi.util.LibsLoader;
|
||||||
|
import eu.vcmi.vcmi.util.Log;
|
||||||
|
|
||||||
|
public class VcmiSDLActivity extends SDLActivity
|
||||||
|
{
|
||||||
|
public static final int SERVER_MESSAGE_SERVER_READY = 1000;
|
||||||
|
public static final int SERVER_MESSAGE_SERVER_KILLED = 1001;
|
||||||
|
public static final String NATIVE_ACTION_CREATE_SERVER = "SDLActivity.Action.CreateServer";
|
||||||
|
protected static final int COMMAND_USER = 0x8000;
|
||||||
|
|
||||||
|
final Messenger mClientMessenger = new Messenger(
|
||||||
|
new IncomingServerMessageHandler(
|
||||||
|
new OnServerRegisteredCallback()));
|
||||||
|
Messenger mServiceMessenger = null;
|
||||||
|
boolean mIsServerServiceBound;
|
||||||
|
private View mProgressBar;
|
||||||
|
|
||||||
|
private ServiceConnection mServerServiceConnection = new ServiceConnection()
|
||||||
|
{
|
||||||
|
public void onServiceConnected(ComponentName className,
|
||||||
|
IBinder service)
|
||||||
|
{
|
||||||
|
Log.i(this, "Service connection");
|
||||||
|
mServiceMessenger = new Messenger(service);
|
||||||
|
mIsServerServiceBound = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Message msg = Message.obtain(null, ServerService.CLIENT_MESSAGE_CLIENT_REGISTERED);
|
||||||
|
msg.replyTo = mClientMessenger;
|
||||||
|
mServiceMessenger.send(msg);
|
||||||
|
}
|
||||||
|
catch (RemoteException ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className)
|
||||||
|
{
|
||||||
|
Log.i(this, "Service disconnection");
|
||||||
|
mServiceMessenger = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public void hackCallNewIntentDirectly(final Intent intent)
|
||||||
|
{
|
||||||
|
onNewIntent(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void displayProgress(final boolean show)
|
||||||
|
{
|
||||||
|
if (mProgressBar != null)
|
||||||
|
{
|
||||||
|
mProgressBar.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadLibraries()
|
||||||
|
{
|
||||||
|
LibsLoader.loadClientLibs(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getMainSharedObject() {
|
||||||
|
String library = "libvcmi-client.so";
|
||||||
|
|
||||||
|
return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(final Intent intent)
|
||||||
|
{
|
||||||
|
Log.i(this, "Got new intent with action " + intent.getAction());
|
||||||
|
if (NATIVE_ACTION_CREATE_SERVER.equals(intent.getAction()))
|
||||||
|
{
|
||||||
|
initService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
mLayout.removeView(mSurface);
|
||||||
|
layout.addView(mSurface);
|
||||||
|
mLayout = layout;
|
||||||
|
|
||||||
|
setContentView(outerLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// since android can kill the activity unexpectedly (e.g. memory is low or device is inactive for some time), let's try creating
|
||||||
|
// an autosave so user might be able to resume the game; this isn't a very good impl (we shouldn't really sleep here and hope that the
|
||||||
|
// save is created, but for now it might suffice
|
||||||
|
// (better solution: listen for game's confirmation that the save has been created -- this would allow us to inform the users
|
||||||
|
// on the next app launch that there is an automatic save that they can use)
|
||||||
|
if (NativeMethods.tryToSaveTheGame())
|
||||||
|
{
|
||||||
|
Thread.sleep(1000L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final InterruptedException ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindServer();
|
||||||
|
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initService()
|
||||||
|
{
|
||||||
|
unbindServer();
|
||||||
|
startService(new Intent(this, ServerService.class));
|
||||||
|
bindService(
|
||||||
|
new Intent(VcmiSDLActivity.this, ServerService.class),
|
||||||
|
mServerServiceConnection,
|
||||||
|
Context.BIND_AUTO_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unbindServer()
|
||||||
|
{
|
||||||
|
Log.d(this, "Unbinding server " + mIsServerServiceBound);
|
||||||
|
if (mIsServerServiceBound)
|
||||||
|
{
|
||||||
|
unbindService(mServerServiceConnection);
|
||||||
|
mIsServerServiceBound = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface IncomingServerMessageHandlerCallback
|
||||||
|
{
|
||||||
|
void unbindServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OnServerRegisteredCallback implements IncomingServerMessageHandlerCallback
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void unbindServer()
|
||||||
|
{
|
||||||
|
VcmiSDLActivity.this.unbindServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class IncomingServerMessageHandler extends Handler
|
||||||
|
{
|
||||||
|
private VcmiSDLActivity.IncomingServerMessageHandlerCallback mCallback;
|
||||||
|
|
||||||
|
IncomingServerMessageHandler(
|
||||||
|
final VcmiSDLActivity.IncomingServerMessageHandlerCallback callback)
|
||||||
|
{
|
||||||
|
mCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg)
|
||||||
|
{
|
||||||
|
Log.i(this, "Got server msg " + msg);
|
||||||
|
switch (msg.what)
|
||||||
|
{
|
||||||
|
case SERVER_MESSAGE_SERVER_READY:
|
||||||
|
NativeMethods.notifyServerReady();
|
||||||
|
break;
|
||||||
|
case SERVER_MESSAGE_SERVER_KILLED:
|
||||||
|
if (mCallback != null)
|
||||||
|
{
|
||||||
|
mCallback.unbindServer();
|
||||||
|
}
|
||||||
|
NativeMethods.notifyServerClosed();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
super.handleMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
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.LegacyConfigReader;
|
||||||
|
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, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
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 = FileUtil.read(getResources().openRawResource(R.raw.authors));
|
||||||
|
vcmiAuthorsView.setText(authorsContent);
|
||||||
|
launcherAuthorsView.setText("Fay"); // TODO hardcoded for now
|
||||||
|
}
|
||||||
|
catch (final IOException e)
|
||||||
|
{
|
||||||
|
Log.e(this, "Could not load authors content", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,254 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
258
android/vcmi-app/src/main/java/eu/vcmi/vcmi/mods/VCMIMod.java
Normal file
258
android/vcmi-app/src/main/java/eu/vcmi/vcmi/mods/VCMIMod.java
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package eu.vcmi.vcmi.settings;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import eu.vcmi.vcmi.Config;
|
||||||
|
import eu.vcmi.vcmi.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author F
|
||||||
|
*/
|
||||||
|
public class CodepageSettingController extends LauncherSettingWithDialogController<String, Config>
|
||||||
|
{
|
||||||
|
public CodepageSettingController(final AppCompatActivity activity)
|
||||||
|
{
|
||||||
|
super(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected LauncherSettingDialog<String> dialog()
|
||||||
|
{
|
||||||
|
return new CodepageSettingDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemChosen(final String item)
|
||||||
|
{
|
||||||
|
mConfig.updateCodepage(item);
|
||||||
|
updateContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String mainText()
|
||||||
|
{
|
||||||
|
return mActivity.getString(R.string.launcher_btn_cp_title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String subText()
|
||||||
|
{
|
||||||
|
if (mConfig == null)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return mConfig.mCodepage == null || mConfig.mCodepage.isEmpty()
|
||||||
|
? mActivity.getString(R.string.launcher_btn_cp_subtitle_unknown)
|
||||||
|
: mActivity.getString(R.string.launcher_btn_cp_subtitle, mConfig.mCodepage);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package eu.vcmi.vcmi.settings;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import eu.vcmi.vcmi.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author F
|
||||||
|
*/
|
||||||
|
public class CodepageSettingDialog extends LauncherSettingDialog<String>
|
||||||
|
{
|
||||||
|
private static final List<String> AVAILABLE_CODEPAGES = new ArrayList<>();
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
AVAILABLE_CODEPAGES.add("CP1250");
|
||||||
|
AVAILABLE_CODEPAGES.add("CP1251");
|
||||||
|
AVAILABLE_CODEPAGES.add("CP1252");
|
||||||
|
AVAILABLE_CODEPAGES.add("GBK");
|
||||||
|
AVAILABLE_CODEPAGES.add("GB2312");
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodepageSettingDialog()
|
||||||
|
{
|
||||||
|
super(AVAILABLE_CODEPAGES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int dialogTitleResId()
|
||||||
|
{
|
||||||
|
return R.string.launcher_btn_cp_title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected CharSequence itemName(final String item)
|
||||||
|
{
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,181 @@
|
|||||||
|
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 && !allowed.contains(child.getName()))
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,168 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
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();
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
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)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
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);
|
||||||
|
mConfig.updateSwipe(item.supportsSwipe());
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mConfig.mSwipeEnabled)
|
||||||
|
{
|
||||||
|
return PointerMode.NORMAL_WITH_SWIPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PointerMode.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PointerMode
|
||||||
|
{
|
||||||
|
NORMAL,
|
||||||
|
NORMAL_WITH_SWIPE,
|
||||||
|
RELATIVE;
|
||||||
|
|
||||||
|
public boolean supportsSwipe()
|
||||||
|
{
|
||||||
|
return this == NORMAL_WITH_SWIPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
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.NORMAL_WITH_SWIPE);
|
||||||
|
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 NORMAL_WITH_SWIPE:
|
||||||
|
return ctx.getString(R.string.misc_pointermode_swipe);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package eu.vcmi.vcmi.settings;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import eu.vcmi.vcmi.Config;
|
||||||
|
import eu.vcmi.vcmi.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author F
|
||||||
|
*/
|
||||||
|
public class ScreenResSettingController extends LauncherSettingWithDialogController<ScreenResSettingController.ScreenRes, Config>
|
||||||
|
{
|
||||||
|
public ScreenResSettingController(final AppCompatActivity activity)
|
||||||
|
{
|
||||||
|
super(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected LauncherSettingDialog<ScreenRes> dialog()
|
||||||
|
{
|
||||||
|
return new ScreenResSettingDialog(mActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemChosen(final ScreenRes item)
|
||||||
|
{
|
||||||
|
mConfig.updateResolution(item.mWidth, item.mHeight);
|
||||||
|
updateContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String mainText()
|
||||||
|
{
|
||||||
|
return mActivity.getString(R.string.launcher_btn_res_title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String subText()
|
||||||
|
{
|
||||||
|
if (mConfig == null)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return mConfig.mResolutionWidth <= 0 || mConfig.mResolutionHeight <= 0
|
||||||
|
? mActivity.getString(R.string.launcher_btn_res_subtitle_unknown)
|
||||||
|
: mActivity.getString(R.string.launcher_btn_res_subtitle, mConfig.mResolutionWidth, mConfig.mResolutionHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ScreenRes
|
||||||
|
{
|
||||||
|
public int mWidth;
|
||||||
|
public int mHeight;
|
||||||
|
|
||||||
|
public ScreenRes(final int width, final int height)
|
||||||
|
{
|
||||||
|
mWidth = width;
|
||||||
|
mHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return mWidth + "x" + mHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
package eu.vcmi.vcmi.settings;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
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 ScreenResSettingDialog extends LauncherSettingDialog<ScreenResSettingController.ScreenRes>
|
||||||
|
{
|
||||||
|
public ScreenResSettingDialog(Activity mActivity)
|
||||||
|
{
|
||||||
|
super(loadResolutions(mActivity));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int dialogTitleResId()
|
||||||
|
{
|
||||||
|
return R.string.launcher_btn_res_title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected CharSequence itemName(final ScreenResSettingController.ScreenRes item)
|
||||||
|
{
|
||||||
|
return item.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ScreenResSettingController.ScreenRes> loadResolutions(Activity activity)
|
||||||
|
{
|
||||||
|
List<ScreenResSettingController.ScreenRes> availableResolutions = new ArrayList<>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File modsFolder = new File(Storage.getVcmiDataDir(activity), "Mods");
|
||||||
|
Queue<File> folders = new ArrayDeque<File>();
|
||||||
|
folders.offer(modsFolder);
|
||||||
|
|
||||||
|
while (!folders.isEmpty())
|
||||||
|
{
|
||||||
|
File folder = folders.poll();
|
||||||
|
File[] children = folder.listFiles();
|
||||||
|
|
||||||
|
if(children == null) continue;
|
||||||
|
|
||||||
|
for (File child : children)
|
||||||
|
{
|
||||||
|
if (child.isDirectory())
|
||||||
|
{
|
||||||
|
folders.add(child);
|
||||||
|
}
|
||||||
|
else if (child.getName().equals("resolutions.json"))
|
||||||
|
{
|
||||||
|
JSONArray resolutions = new JSONObject(FileUtil.read(child))
|
||||||
|
.getJSONArray("GUISettings");
|
||||||
|
|
||||||
|
for(int index = 0; index < resolutions.length(); index++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JSONObject resolution = resolutions
|
||||||
|
.getJSONObject(index)
|
||||||
|
.getJSONObject("resolution");
|
||||||
|
|
||||||
|
availableResolutions.add(new ScreenResSettingController.ScreenRes(
|
||||||
|
resolution.getInt("x"),
|
||||||
|
resolution.getInt("y")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(availableResolutions.isEmpty())
|
||||||
|
{
|
||||||
|
availableResolutions.add(new ScreenResSettingController.ScreenRes(800, 600));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
ex.printStackTrace();
|
||||||
|
|
||||||
|
availableResolutions.clear();
|
||||||
|
|
||||||
|
availableResolutions.add(new ScreenResSettingController.ScreenRes(800, 600));
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableResolutions;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
344
android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java
Normal file
344
android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
package eu.vcmi.vcmi.util;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.res.AssetManager;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
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 eu.vcmi.vcmi.Const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
{
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(stream))
|
||||||
|
{
|
||||||
|
return readInternal(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String read(final File file) throws IOException
|
||||||
|
{
|
||||||
|
try (FileReader reader = new FileReader(file))
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final File dir = file.getParentFile();
|
||||||
|
|
||||||
|
if (dir.exists() || dir.mkdirs())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e("Couldn't create dir " + dir);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean clearDirectory(final File dir)
|
||||||
|
{
|
||||||
|
for (final File f : dir.listFiles())
|
||||||
|
{
|
||||||
|
if (f.isDirectory() && !clearDirectory(f))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!f.delete())
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
final byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
int read;
|
||||||
|
while ((read = source.read(buffer)) != -1)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package eu.vcmi.vcmi.util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public interface IZipProgressReporter
|
||||||
|
{
|
||||||
|
void onUnpacked(File newFile);
|
||||||
|
}
|
@ -0,0 +1,198 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
package eu.vcmi.vcmi.util;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
|
||||||
|
import eu.vcmi.vcmi.Const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* helper used to retrieve old vcmi config (currently only needed for h3 data path in order to migrate the data to the new location)
|
||||||
|
*
|
||||||
|
* @author F
|
||||||
|
*/
|
||||||
|
public final class LegacyConfigReader
|
||||||
|
{
|
||||||
|
private static void skipBools(final ObjectInputStream stream, final int num) throws IOException
|
||||||
|
{
|
||||||
|
for (int i = 0; i < num; ++i)
|
||||||
|
{
|
||||||
|
stream.readBoolean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void skipInts(final ObjectInputStream stream, final int num) throws IOException
|
||||||
|
{
|
||||||
|
for (int i = 0; i < num; ++i)
|
||||||
|
{
|
||||||
|
stream.readInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Const.SUPPRESS_TRY_WITH_RESOURCES_WARNING)
|
||||||
|
public static Config load(final File basePath)
|
||||||
|
{
|
||||||
|
final File settingsFile = new File(basePath, "/libsdl-settings.cfg");
|
||||||
|
if (!settingsFile.exists())
|
||||||
|
{
|
||||||
|
Log.i("Legacy config file doesn't exist");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (final ObjectInputStream stream = new ObjectInputStream(new FileInputStream(settingsFile)))
|
||||||
|
{
|
||||||
|
if (stream.readInt() != 5)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
skipBools(stream, 5);
|
||||||
|
skipInts(stream, 9);
|
||||||
|
skipBools(stream, 2);
|
||||||
|
skipInts(stream, 2);
|
||||||
|
stream.readBoolean();
|
||||||
|
skipInts(stream, 2);
|
||||||
|
skipInts(stream, stream.readInt());
|
||||||
|
stream.readInt();
|
||||||
|
skipInts(stream, 6);
|
||||||
|
stream.readInt();
|
||||||
|
skipBools(stream, 8);
|
||||||
|
stream.readInt();
|
||||||
|
stream.readInt();
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
stream.readInt();
|
||||||
|
stream.readBoolean();
|
||||||
|
}
|
||||||
|
skipInts(stream, 5);
|
||||||
|
final StringBuilder b = new StringBuilder();
|
||||||
|
final int len = stream.readInt();
|
||||||
|
for (int i = 0; i < len; i++)
|
||||||
|
{
|
||||||
|
b.append(stream.readChar());
|
||||||
|
}
|
||||||
|
|
||||||
|
final Config config = new Config();
|
||||||
|
config.mDataPath = b.toString();
|
||||||
|
Log.v("Retrieved legacy data folder name: " + config.mDataPath);
|
||||||
|
if (!TextUtils.isEmpty(config.mDataPath) && new File(config.mDataPath).exists())
|
||||||
|
{
|
||||||
|
// return config only if there actually is a chance of retrieving old data
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
Log.i("Couldn't find valid data in legacy config");
|
||||||
|
}
|
||||||
|
catch (final Exception e)
|
||||||
|
{
|
||||||
|
Log.i("Couldn't load legacy config");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Config
|
||||||
|
{
|
||||||
|
public String mDataPath;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package eu.vcmi.vcmi.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import org.libsdl.app.SDL;
|
||||||
|
|
||||||
|
import eu.vcmi.vcmi.NativeMethods;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author F
|
||||||
|
*/
|
||||||
|
public final class LibsLoader
|
||||||
|
{
|
||||||
|
private static void loadLib(final String libName, final boolean onlyForOldApis)
|
||||||
|
{
|
||||||
|
if (!onlyForOldApis || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
{
|
||||||
|
Log.v("Loading native lib: " + libName);
|
||||||
|
SDL.loadLibrary(libName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void loadCommon()
|
||||||
|
{
|
||||||
|
loadLib("c++_shared", true);
|
||||||
|
loadLib("iconv", true);
|
||||||
|
loadLib("boost-system", true);
|
||||||
|
loadLib("boost-datetime", true);
|
||||||
|
loadLib("boost-locale", true);
|
||||||
|
loadLib("boost-filesystem", true);
|
||||||
|
loadLib("boost-program-options", true);
|
||||||
|
loadLib("boost-thread", true);
|
||||||
|
loadLib("SDL2", false);
|
||||||
|
loadLib("x264", true);
|
||||||
|
loadLib("avutil", true);
|
||||||
|
loadLib("swscale", true);
|
||||||
|
loadLib("swresample", true);
|
||||||
|
loadLib("postproc", true);
|
||||||
|
loadLib("avcodec", true);
|
||||||
|
loadLib("avformat", true);
|
||||||
|
loadLib("avfilter", true);
|
||||||
|
loadLib("avdevice", true);
|
||||||
|
loadLib("minizip", true);
|
||||||
|
loadLib("vcmi-fuzzylite", true);
|
||||||
|
loadLib("vcmi-lib", true);
|
||||||
|
loadLib("SDL2_image", false);
|
||||||
|
loadLib("SDL2_mixer", false);
|
||||||
|
loadLib("SDL2_ttf", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadClientLibs(Context ctx)
|
||||||
|
{
|
||||||
|
loadCommon();
|
||||||
|
loadLib("vcmi-client", false);
|
||||||
|
SDL.setContext(ctx);
|
||||||
|
NativeMethods.clientSetupJNI();
|
||||||
|
NativeMethods.initClassloader();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadServerLibs()
|
||||||
|
{
|
||||||
|
loadCommon();
|
||||||
|
loadLib("vcmi-server", false);
|
||||||
|
NativeMethods.initClassloader();
|
||||||
|
}
|
||||||
|
}
|
156
android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/Log.java
Normal file
156
android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/Log.java
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
private static void log(final int priority, final Object obj, final String msg)
|
||||||
|
{
|
||||||
|
logInternal(priority, tag(obj), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void logInternal(final int priority, final String tagString, final String msg)
|
||||||
|
{
|
||||||
|
if (LOGGING_ENABLED_CONSOLE)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
switch (priority)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
return "?";
|
||||||
|
case android.util.Log.VERBOSE:
|
||||||
|
return "V";
|
||||||
|
case android.util.Log.DEBUG:
|
||||||
|
return "D";
|
||||||
|
case android.util.Log.INFO:
|
||||||
|
return "I";
|
||||||
|
case android.util.Log.WARN:
|
||||||
|
return "W";
|
||||||
|
case android.util.Log.ERROR:
|
||||||
|
return "E";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String tag(final Object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void d(final String msg)
|
||||||
|
{
|
||||||
|
logInternal(android.util.Log.DEBUG, STATIC_TAG, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void i(final String msg)
|
||||||
|
{
|
||||||
|
logInternal(android.util.Log.INFO, STATIC_TAG, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void w(final String msg)
|
||||||
|
{
|
||||||
|
logInternal(android.util.Log.WARN, STATIC_TAG, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void e(final String msg)
|
||||||
|
{
|
||||||
|
logInternal(android.util.Log.ERROR, STATIC_TAG, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void v(final Object obj, final String msg)
|
||||||
|
{
|
||||||
|
log(android.util.Log.VERBOSE, obj, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void d(final Object obj, final String msg)
|
||||||
|
{
|
||||||
|
log(android.util.Log.DEBUG, obj, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void i(final Object obj, final String msg)
|
||||||
|
{
|
||||||
|
log(android.util.Log.INFO, obj, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void w(final Object obj, final String msg)
|
||||||
|
{
|
||||||
|
log(android.util.Log.WARN, obj, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void e(final Object obj, final String msg)
|
||||||
|
{
|
||||||
|
log(android.util.Log.ERROR, obj, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void e(final Object obj, final String msg, final Throwable e)
|
||||||
|
{
|
||||||
|
log(android.util.Log.ERROR, obj, msg + "\n" + android.util.Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void e(final String msg, final Throwable e)
|
||||||
|
{
|
||||||
|
logInternal(android.util.Log.ERROR, STATIC_TAG, msg + "\n" + android.util.Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
58
android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/Utils.java
Normal file
58
android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/Utils.java
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
74
android/vcmi-app/src/main/java/org/libsdl/app/DummyEdit.java
Normal file
74
android/vcmi-app/src/main/java/org/libsdl/app/DummyEdit.java
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.text.InputType;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputConnection;
|
||||||
|
|
||||||
|
/* This is a fake invisible editor view that receives the input and defines the
|
||||||
|
* pan&scan region
|
||||||
|
*/
|
||||||
|
public class DummyEdit extends View implements View.OnKeyListener
|
||||||
|
{
|
||||||
|
InputConnection ic;
|
||||||
|
|
||||||
|
public DummyEdit(Context context)
|
||||||
|
{
|
||||||
|
super(context);
|
||||||
|
setFocusableInTouchMode(true);
|
||||||
|
setFocusable(true);
|
||||||
|
setOnKeyListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCheckIsTextEditor()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKey(View v, int keyCode, KeyEvent event)
|
||||||
|
{
|
||||||
|
return SDLActivity.handleKeyEvent(v, keyCode, event, ic);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
@Override
|
||||||
|
public boolean onKeyPreIme(int keyCode, KeyEvent event)
|
||||||
|
{
|
||||||
|
// As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
|
||||||
|
// FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
|
||||||
|
// FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
|
||||||
|
// FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
|
||||||
|
// FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
|
||||||
|
// FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
|
||||||
|
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK)
|
||||||
|
{
|
||||||
|
if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE)
|
||||||
|
{
|
||||||
|
SDLActivity.onNativeKeyboardFocusLost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onKeyPreIme(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs)
|
||||||
|
{
|
||||||
|
ic = new SDLInputConnection(this, true);
|
||||||
|
|
||||||
|
outAttrs.inputType = InputType.TYPE_CLASS_TEXT |
|
||||||
|
InputType.TYPE_TEXT_FLAG_MULTI_LINE;
|
||||||
|
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI |
|
||||||
|
EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
|
||||||
|
|
||||||
|
return ic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputConnection getInputConnection()
|
||||||
|
{
|
||||||
|
return ic;
|
||||||
|
}
|
||||||
|
}
|
22
android/vcmi-app/src/main/java/org/libsdl/app/HIDDevice.java
Normal file
22
android/vcmi-app/src/main/java/org/libsdl/app/HIDDevice.java
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
|
||||||
|
interface HIDDevice
|
||||||
|
{
|
||||||
|
public int getId();
|
||||||
|
public int getVendorId();
|
||||||
|
public int getProductId();
|
||||||
|
public String getSerialNumber();
|
||||||
|
public int getVersion();
|
||||||
|
public String getManufacturerName();
|
||||||
|
public String getProductName();
|
||||||
|
public UsbDevice getDevice();
|
||||||
|
public boolean open();
|
||||||
|
public int sendFeatureReport(byte[] report);
|
||||||
|
public int sendOutputReport(byte[] report);
|
||||||
|
public boolean getFeatureReport(byte[] report);
|
||||||
|
public void setFrozen(boolean frozen);
|
||||||
|
public void close();
|
||||||
|
public void shutdown();
|
||||||
|
}
|
@ -0,0 +1,650 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothGatt;
|
||||||
|
import android.bluetooth.BluetoothGattCallback;
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
import android.bluetooth.BluetoothGattDescriptor;
|
||||||
|
import android.bluetooth.BluetoothManager;
|
||||||
|
import android.bluetooth.BluetoothProfile;
|
||||||
|
import android.bluetooth.BluetoothGattService;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.os.*;
|
||||||
|
|
||||||
|
//import com.android.internal.util.HexDump;
|
||||||
|
|
||||||
|
import java.lang.Runnable;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
|
||||||
|
|
||||||
|
private static final String TAG = "hidapi";
|
||||||
|
private HIDDeviceManager mManager;
|
||||||
|
private BluetoothDevice mDevice;
|
||||||
|
private int mDeviceId;
|
||||||
|
private BluetoothGatt mGatt;
|
||||||
|
private boolean mIsRegistered = false;
|
||||||
|
private boolean mIsConnected = false;
|
||||||
|
private boolean mIsChromebook = false;
|
||||||
|
private boolean mIsReconnecting = false;
|
||||||
|
private boolean mFrozen = false;
|
||||||
|
private LinkedList<GattOperation> mOperations;
|
||||||
|
GattOperation mCurrentOperation = null;
|
||||||
|
private Handler mHandler;
|
||||||
|
|
||||||
|
private static final int TRANSPORT_AUTO = 0;
|
||||||
|
private static final int TRANSPORT_BREDR = 1;
|
||||||
|
private static final int TRANSPORT_LE = 2;
|
||||||
|
|
||||||
|
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
|
||||||
|
|
||||||
|
static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
|
||||||
|
static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
|
||||||
|
static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
|
||||||
|
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
|
||||||
|
|
||||||
|
static class GattOperation {
|
||||||
|
private enum Operation {
|
||||||
|
CHR_READ,
|
||||||
|
CHR_WRITE,
|
||||||
|
ENABLE_NOTIFICATION
|
||||||
|
}
|
||||||
|
|
||||||
|
Operation mOp;
|
||||||
|
UUID mUuid;
|
||||||
|
byte[] mValue;
|
||||||
|
BluetoothGatt mGatt;
|
||||||
|
boolean mResult = true;
|
||||||
|
|
||||||
|
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
|
||||||
|
mGatt = gatt;
|
||||||
|
mOp = operation;
|
||||||
|
mUuid = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
|
||||||
|
mGatt = gatt;
|
||||||
|
mOp = operation;
|
||||||
|
mUuid = uuid;
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
// This is executed in main thread
|
||||||
|
BluetoothGattCharacteristic chr;
|
||||||
|
|
||||||
|
switch (mOp) {
|
||||||
|
case CHR_READ:
|
||||||
|
chr = getCharacteristic(mUuid);
|
||||||
|
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
|
||||||
|
if (!mGatt.readCharacteristic(chr)) {
|
||||||
|
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
|
||||||
|
mResult = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mResult = true;
|
||||||
|
break;
|
||||||
|
case CHR_WRITE:
|
||||||
|
chr = getCharacteristic(mUuid);
|
||||||
|
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
|
||||||
|
chr.setValue(mValue);
|
||||||
|
if (!mGatt.writeCharacteristic(chr)) {
|
||||||
|
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
|
||||||
|
mResult = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mResult = true;
|
||||||
|
break;
|
||||||
|
case ENABLE_NOTIFICATION:
|
||||||
|
chr = getCharacteristic(mUuid);
|
||||||
|
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
|
||||||
|
if (chr != null) {
|
||||||
|
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
|
||||||
|
if (cccd != null) {
|
||||||
|
int properties = chr.getProperties();
|
||||||
|
byte[] value;
|
||||||
|
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
|
||||||
|
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
|
||||||
|
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
|
||||||
|
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Unable to start notifications on input characteristic");
|
||||||
|
mResult = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mGatt.setCharacteristicNotification(chr, true);
|
||||||
|
cccd.setValue(value);
|
||||||
|
if (!mGatt.writeDescriptor(cccd)) {
|
||||||
|
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
|
||||||
|
mResult = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mResult = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean finish() {
|
||||||
|
return mResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
|
||||||
|
BluetoothGattService valveService = mGatt.getService(steamControllerService);
|
||||||
|
if (valveService == null)
|
||||||
|
return null;
|
||||||
|
return valveService.getCharacteristic(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
|
||||||
|
return new GattOperation(gatt, Operation.CHR_READ, uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
|
||||||
|
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
|
||||||
|
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
|
||||||
|
mManager = manager;
|
||||||
|
mDevice = device;
|
||||||
|
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
|
||||||
|
mIsRegistered = false;
|
||||||
|
mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
|
||||||
|
mOperations = new LinkedList<GattOperation>();
|
||||||
|
mHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
mGatt = connectGatt();
|
||||||
|
// final HIDDeviceBLESteamController finalThis = this;
|
||||||
|
// mHandler.postDelayed(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public void run() {
|
||||||
|
// finalThis.checkConnectionForChromebookIssue();
|
||||||
|
// }
|
||||||
|
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdentifier() {
|
||||||
|
return String.format("SteamController.%s", mDevice.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
public BluetoothGatt getGatt() {
|
||||||
|
return mGatt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
|
||||||
|
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
|
||||||
|
private BluetoothGatt connectGatt(boolean managed) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
|
try {
|
||||||
|
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return mDevice.connectGatt(mManager.getContext(), managed, this);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return mDevice.connectGatt(mManager.getContext(), managed, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BluetoothGatt connectGatt() {
|
||||||
|
return connectGatt(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getConnectionState() {
|
||||||
|
|
||||||
|
Context context = mManager.getContext();
|
||||||
|
if (context == null) {
|
||||||
|
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
|
||||||
|
return BluetoothProfile.STATE_DISCONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||||
|
if (btManager == null) {
|
||||||
|
// This device doesn't support Bluetooth. We should never be here, because how did
|
||||||
|
// we instantiate a device to start with?
|
||||||
|
return BluetoothProfile.STATE_DISCONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reconnect() {
|
||||||
|
|
||||||
|
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
|
||||||
|
mGatt.disconnect();
|
||||||
|
mGatt = connectGatt();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void checkConnectionForChromebookIssue() {
|
||||||
|
if (!mIsChromebook) {
|
||||||
|
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
|
||||||
|
// over and over.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int connectionState = getConnectionState();
|
||||||
|
|
||||||
|
switch (connectionState) {
|
||||||
|
case BluetoothProfile.STATE_CONNECTED:
|
||||||
|
if (!mIsConnected) {
|
||||||
|
// We are in the Bad Chromebook Place. We can force a disconnect
|
||||||
|
// to try to recover.
|
||||||
|
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
|
||||||
|
mIsReconnecting = true;
|
||||||
|
mGatt.disconnect();
|
||||||
|
mGatt = connectGatt(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (!isRegistered()) {
|
||||||
|
if (mGatt.getServices().size() > 0) {
|
||||||
|
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
|
||||||
|
probeService(this);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
|
||||||
|
mIsReconnecting = true;
|
||||||
|
mGatt.disconnect();
|
||||||
|
mGatt = connectGatt(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BluetoothProfile.STATE_DISCONNECTED:
|
||||||
|
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
|
||||||
|
|
||||||
|
mIsReconnecting = true;
|
||||||
|
mGatt.disconnect();
|
||||||
|
mGatt = connectGatt(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BluetoothProfile.STATE_CONNECTING:
|
||||||
|
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final HIDDeviceBLESteamController finalThis = this;
|
||||||
|
mHandler.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
finalThis.checkConnectionForChromebookIssue();
|
||||||
|
}
|
||||||
|
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRegistered() {
|
||||||
|
return mIsRegistered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setRegistered() {
|
||||||
|
mIsRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean probeService(HIDDeviceBLESteamController controller) {
|
||||||
|
|
||||||
|
if (isRegistered()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mIsConnected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v(TAG, "probeService controller=" + controller);
|
||||||
|
|
||||||
|
for (BluetoothGattService service : mGatt.getServices()) {
|
||||||
|
if (service.getUuid().equals(steamControllerService)) {
|
||||||
|
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
|
||||||
|
|
||||||
|
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
|
||||||
|
if (chr.getUuid().equals(inputCharacteristic)) {
|
||||||
|
Log.v(TAG, "Found input characteristic");
|
||||||
|
// Start notifications
|
||||||
|
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
|
||||||
|
if (cccd != null) {
|
||||||
|
enableNotification(chr.getUuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
|
||||||
|
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
|
||||||
|
mIsConnected = false;
|
||||||
|
mIsReconnecting = true;
|
||||||
|
mGatt.disconnect();
|
||||||
|
mGatt = connectGatt(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void finishCurrentGattOperation() {
|
||||||
|
GattOperation op = null;
|
||||||
|
synchronized (mOperations) {
|
||||||
|
if (mCurrentOperation != null) {
|
||||||
|
op = mCurrentOperation;
|
||||||
|
mCurrentOperation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (op != null) {
|
||||||
|
boolean result = op.finish(); // TODO: Maybe in main thread as well?
|
||||||
|
|
||||||
|
// Our operation failed, let's add it back to the beginning of our queue.
|
||||||
|
if (!result) {
|
||||||
|
mOperations.addFirst(op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executeNextGattOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeNextGattOperation() {
|
||||||
|
synchronized (mOperations) {
|
||||||
|
if (mCurrentOperation != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mOperations.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
mCurrentOperation = mOperations.removeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run in main thread
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (mOperations) {
|
||||||
|
if (mCurrentOperation == null) {
|
||||||
|
Log.e(TAG, "Current operation null in executor?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentOperation.run();
|
||||||
|
// now wait for the GATT callback and when it comes, finish this operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queueGattOperation(GattOperation op) {
|
||||||
|
synchronized (mOperations) {
|
||||||
|
mOperations.add(op);
|
||||||
|
}
|
||||||
|
executeNextGattOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enableNotification(UUID chrUuid) {
|
||||||
|
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
|
||||||
|
queueGattOperation(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeCharacteristic(UUID uuid, byte[] value) {
|
||||||
|
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
|
||||||
|
queueGattOperation(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readCharacteristic(UUID uuid) {
|
||||||
|
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
|
||||||
|
queueGattOperation(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////// BluetoothGattCallback overridden methods
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
|
||||||
|
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
|
||||||
|
mIsReconnecting = false;
|
||||||
|
if (newState == 2) {
|
||||||
|
mIsConnected = true;
|
||||||
|
// Run directly, without GattOperation
|
||||||
|
if (!isRegistered()) {
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mGatt.discoverServices();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (newState == 0) {
|
||||||
|
mIsConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
|
||||||
|
//Log.v(TAG, "onServicesDiscovered status=" + status);
|
||||||
|
if (status == 0) {
|
||||||
|
if (gatt.getServices().size() == 0) {
|
||||||
|
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
|
||||||
|
mIsReconnecting = true;
|
||||||
|
mIsConnected = false;
|
||||||
|
gatt.disconnect();
|
||||||
|
mGatt = connectGatt(false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
probeService(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||||
|
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
|
||||||
|
|
||||||
|
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
|
||||||
|
mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
finishCurrentGattOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||||
|
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
|
||||||
|
|
||||||
|
if (characteristic.getUuid().equals(reportCharacteristic)) {
|
||||||
|
// Only register controller with the native side once it has been fully configured
|
||||||
|
if (!isRegistered()) {
|
||||||
|
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
|
||||||
|
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0);
|
||||||
|
setRegistered();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finishCurrentGattOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||||
|
// Enable this for verbose logging of controller input reports
|
||||||
|
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
|
||||||
|
|
||||||
|
if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
|
||||||
|
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||||
|
//Log.v(TAG, "onDescriptorRead status=" + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||||
|
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
|
||||||
|
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
|
||||||
|
|
||||||
|
if (chr.getUuid().equals(inputCharacteristic)) {
|
||||||
|
boolean hasWrittenInputDescriptor = true;
|
||||||
|
BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
|
||||||
|
if (reportChr != null) {
|
||||||
|
Log.v(TAG, "Writing report characteristic to enter valve mode");
|
||||||
|
reportChr.setValue(enterValveMode);
|
||||||
|
gatt.writeCharacteristic(reportChr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finishCurrentGattOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
|
||||||
|
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
|
||||||
|
//Log.v(TAG, "onReadRemoteRssi status=" + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||||
|
//Log.v(TAG, "onMtuChanged status=" + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////// Public API
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getId() {
|
||||||
|
return mDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVendorId() {
|
||||||
|
// Valve Corporation
|
||||||
|
final int VALVE_USB_VID = 0x28DE;
|
||||||
|
return VALVE_USB_VID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getProductId() {
|
||||||
|
// We don't have an easy way to query from the Bluetooth device, but we know what it is
|
||||||
|
final int D0G_BLE2_PID = 0x1106;
|
||||||
|
return D0G_BLE2_PID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSerialNumber() {
|
||||||
|
// This will be read later via feature report by Steam
|
||||||
|
return "12345";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVersion() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getManufacturerName() {
|
||||||
|
return "Valve Corporation";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProductName() {
|
||||||
|
return "Steam Controller";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UsbDevice getDevice() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean open() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendFeatureReport(byte[] report) {
|
||||||
|
if (!isRegistered()) {
|
||||||
|
Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!");
|
||||||
|
if (mIsConnected) {
|
||||||
|
probeService(this);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to skip the first byte, as that doesn't go over the air
|
||||||
|
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
|
||||||
|
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report));
|
||||||
|
writeCharacteristic(reportCharacteristic, actual_report);
|
||||||
|
return report.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendOutputReport(byte[] report) {
|
||||||
|
if (!isRegistered()) {
|
||||||
|
Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!");
|
||||||
|
if (mIsConnected) {
|
||||||
|
probeService(this);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
|
||||||
|
writeCharacteristic(reportCharacteristic, report);
|
||||||
|
return report.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getFeatureReport(byte[] report) {
|
||||||
|
if (!isRegistered()) {
|
||||||
|
Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!");
|
||||||
|
if (mIsConnected) {
|
||||||
|
probeService(this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Log.v(TAG, "getFeatureReport");
|
||||||
|
readCharacteristic(reportCharacteristic);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFrozen(boolean frozen) {
|
||||||
|
mFrozen = frozen;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
close();
|
||||||
|
|
||||||
|
BluetoothGatt g = mGatt;
|
||||||
|
if (g != null) {
|
||||||
|
g.disconnect();
|
||||||
|
g.close();
|
||||||
|
mGatt = null;
|
||||||
|
}
|
||||||
|
mManager = null;
|
||||||
|
mIsRegistered = false;
|
||||||
|
mIsConnected = false;
|
||||||
|
mOperations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,679 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothManager;
|
||||||
|
import android.bluetooth.BluetoothProfile;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.hardware.usb.*;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HIDDeviceManager {
|
||||||
|
private static final String TAG = "hidapi";
|
||||||
|
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
|
||||||
|
|
||||||
|
private static HIDDeviceManager sManager;
|
||||||
|
private static int sManagerRefCount = 0;
|
||||||
|
|
||||||
|
public static HIDDeviceManager acquire(Context context) {
|
||||||
|
if (sManagerRefCount == 0) {
|
||||||
|
sManager = new HIDDeviceManager(context);
|
||||||
|
}
|
||||||
|
++sManagerRefCount;
|
||||||
|
return sManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void release(HIDDeviceManager manager) {
|
||||||
|
if (manager == sManager) {
|
||||||
|
--sManagerRefCount;
|
||||||
|
if (sManagerRefCount == 0) {
|
||||||
|
sManager.close();
|
||||||
|
sManager = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Context mContext;
|
||||||
|
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
|
||||||
|
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
|
||||||
|
private int mNextDeviceId = 0;
|
||||||
|
private SharedPreferences mSharedPreferences = null;
|
||||||
|
private boolean mIsChromebook = false;
|
||||||
|
private UsbManager mUsbManager;
|
||||||
|
private Handler mHandler;
|
||||||
|
private BluetoothManager mBluetoothManager;
|
||||||
|
private List<BluetoothDevice> mLastBluetoothDevices;
|
||||||
|
|
||||||
|
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||||
|
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
handleUsbDeviceAttached(usbDevice);
|
||||||
|
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
|
||||||
|
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
handleUsbDeviceDetached(usbDevice);
|
||||||
|
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
|
||||||
|
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
// Bluetooth device was connected. If it was a Steam Controller, handle it
|
||||||
|
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
|
||||||
|
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||||
|
Log.d(TAG, "Bluetooth device connected: " + device);
|
||||||
|
|
||||||
|
if (isSteamController(device)) {
|
||||||
|
connectBluetoothDevice(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bluetooth device was disconnected, remove from controller manager (if any)
|
||||||
|
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
|
||||||
|
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||||
|
Log.d(TAG, "Bluetooth device disconnected: " + device);
|
||||||
|
|
||||||
|
disconnectBluetoothDevice(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private HIDDeviceManager(final Context context) {
|
||||||
|
mContext = context;
|
||||||
|
|
||||||
|
HIDDeviceRegisterCallback();
|
||||||
|
|
||||||
|
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
|
||||||
|
mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
|
||||||
|
|
||||||
|
// if (shouldClear) {
|
||||||
|
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||||
|
// spedit.clear();
|
||||||
|
// spedit.commit();
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
{
|
||||||
|
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Context getContext() {
|
||||||
|
return mContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDeviceIDForIdentifier(String identifier) {
|
||||||
|
SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||||
|
|
||||||
|
int result = mSharedPreferences.getInt(identifier, 0);
|
||||||
|
if (result == 0) {
|
||||||
|
result = mNextDeviceId++;
|
||||||
|
spedit.putInt("next_device_id", mNextDeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
spedit.putInt(identifier, result);
|
||||||
|
spedit.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeUSB() {
|
||||||
|
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
|
||||||
|
if (mUsbManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Logging
|
||||||
|
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
|
||||||
|
Log.i(TAG,"Path: " + device.getDeviceName());
|
||||||
|
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
|
||||||
|
Log.i(TAG,"Product: " + device.getProductName());
|
||||||
|
Log.i(TAG,"ID: " + device.getDeviceId());
|
||||||
|
Log.i(TAG,"Class: " + device.getDeviceClass());
|
||||||
|
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
|
||||||
|
Log.i(TAG,"Vendor ID " + device.getVendorId());
|
||||||
|
Log.i(TAG,"Product ID: " + device.getProductId());
|
||||||
|
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
|
||||||
|
Log.i(TAG,"---------------------------------------");
|
||||||
|
|
||||||
|
// Get interface details
|
||||||
|
for (int index = 0; index < device.getInterfaceCount(); index++) {
|
||||||
|
UsbInterface mUsbInterface = device.getInterface(index);
|
||||||
|
Log.i(TAG," ***** *****");
|
||||||
|
Log.i(TAG," Interface index: " + index);
|
||||||
|
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
|
||||||
|
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
|
||||||
|
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
|
||||||
|
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
|
||||||
|
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
|
||||||
|
|
||||||
|
// Get endpoint details
|
||||||
|
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
|
||||||
|
{
|
||||||
|
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
|
||||||
|
Log.i(TAG," ++++ ++++ ++++");
|
||||||
|
Log.i(TAG," Endpoint index: " + epi);
|
||||||
|
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
|
||||||
|
Log.i(TAG," Direction: " + mEndpoint.getDirection());
|
||||||
|
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
|
||||||
|
Log.i(TAG," Interval: " + mEndpoint.getInterval());
|
||||||
|
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
|
||||||
|
Log.i(TAG," Type: " + mEndpoint.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(TAG," No more devices connected.");
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Register for USB broadcasts and permission completions
|
||||||
|
IntentFilter filter = new IntentFilter();
|
||||||
|
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
|
||||||
|
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
|
||||||
|
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
|
||||||
|
mContext.registerReceiver(mUsbBroadcast, filter);
|
||||||
|
|
||||||
|
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
|
||||||
|
handleUsbDeviceAttached(usbDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UsbManager getUSBManager() {
|
||||||
|
return mUsbManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shutdownUSB() {
|
||||||
|
try {
|
||||||
|
mContext.unregisterReceiver(mUsbBroadcast);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// We may not have registered, that's okay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||||
|
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||||
|
final int XB360_IFACE_SUBCLASS = 93;
|
||||||
|
final int XB360_IFACE_PROTOCOL = 1; // Wired
|
||||||
|
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
|
||||||
|
final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x0079, // GPD Win 2
|
||||||
|
0x044f, // Thrustmaster
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x046d, // Logitech
|
||||||
|
0x056e, // Elecom
|
||||||
|
0x06a3, // Saitek
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x07ff, // Mad Catz
|
||||||
|
0x0e6f, // PDP
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1038, // SteelSeries
|
||||||
|
0x11c9, // Nacon
|
||||||
|
0x12ab, // Unknown
|
||||||
|
0x1430, // RedOctane
|
||||||
|
0x146b, // BigBen
|
||||||
|
0x1532, // Razer Sabertooth
|
||||||
|
0x15e4, // Numark
|
||||||
|
0x162e, // Joytech
|
||||||
|
0x1689, // Razer Onza
|
||||||
|
0x1949, // Lab126, Inc.
|
||||||
|
0x1bad, // Harmonix
|
||||||
|
0x20d6, // PowerA
|
||||||
|
0x24c6, // PowerA
|
||||||
|
0x2c22, // Qanba
|
||||||
|
};
|
||||||
|
|
||||||
|
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
||||||
|
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
|
||||||
|
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
|
||||||
|
int vendor_id = usbDevice.getVendorId();
|
||||||
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
|
if (vendor_id == supportedVid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||||
|
final int XB1_IFACE_SUBCLASS = 71;
|
||||||
|
final int XB1_IFACE_PROTOCOL = 208;
|
||||||
|
final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x0e6f, // PDP
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1532, // Razer Wildcat
|
||||||
|
0x20d6, // PowerA
|
||||||
|
0x24c6, // PowerA
|
||||||
|
0x2dc8, /* 8BitDo */
|
||||||
|
0x2e24, // Hyperkin
|
||||||
|
};
|
||||||
|
|
||||||
|
if (usbInterface.getId() == 0 &&
|
||||||
|
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||||
|
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||||
|
int vendor_id = usbDevice.getVendorId();
|
||||||
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
|
if (vendor_id == supportedVid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
|
||||||
|
connectHIDDeviceUSB(usbDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
|
||||||
|
List<Integer> devices = new ArrayList<Integer>();
|
||||||
|
for (HIDDevice device : mDevicesById.values()) {
|
||||||
|
if (usbDevice.equals(device.getDevice())) {
|
||||||
|
devices.add(device.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int id : devices) {
|
||||||
|
HIDDevice device = mDevicesById.get(id);
|
||||||
|
mDevicesById.remove(id);
|
||||||
|
device.shutdown();
|
||||||
|
HIDDeviceDisconnected(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
|
||||||
|
for (HIDDevice device : mDevicesById.values()) {
|
||||||
|
if (usbDevice.equals(device.getDevice())) {
|
||||||
|
boolean opened = false;
|
||||||
|
if (permission_granted) {
|
||||||
|
opened = device.open();
|
||||||
|
}
|
||||||
|
HIDDeviceOpenResult(device.getId(), opened);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
|
||||||
|
synchronized (this) {
|
||||||
|
int interface_mask = 0;
|
||||||
|
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
|
||||||
|
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
|
||||||
|
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
|
||||||
|
// Check to see if we've already added this interface
|
||||||
|
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
|
||||||
|
int interface_id = usbInterface.getId();
|
||||||
|
if ((interface_mask & (1 << interface_id)) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
interface_mask |= (1 << interface_id);
|
||||||
|
|
||||||
|
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
|
||||||
|
int id = device.getId();
|
||||||
|
mDevicesById.put(id, device);
|
||||||
|
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeBluetooth() {
|
||||||
|
Log.d(TAG, "Initializing Bluetooth");
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT <= 30 &&
|
||||||
|
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18)) {
|
||||||
|
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find bonded bluetooth controllers and create SteamControllers for them
|
||||||
|
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||||
|
if (mBluetoothManager == null) {
|
||||||
|
// This device doesn't support Bluetooth.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
|
||||||
|
if (btAdapter == null) {
|
||||||
|
// This device has Bluetooth support in the codebase, but has no available adapters.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get our bonded devices.
|
||||||
|
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
|
||||||
|
|
||||||
|
Log.d(TAG, "Bluetooth device available: " + device);
|
||||||
|
if (isSteamController(device)) {
|
||||||
|
connectBluetoothDevice(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: These don't work on Chromebooks, to my undying dismay.
|
||||||
|
IntentFilter filter = new IntentFilter();
|
||||||
|
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
|
||||||
|
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
|
||||||
|
mContext.registerReceiver(mBluetoothBroadcast, filter);
|
||||||
|
|
||||||
|
if (mIsChromebook) {
|
||||||
|
mHandler = new Handler(Looper.getMainLooper());
|
||||||
|
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
|
||||||
|
|
||||||
|
// final HIDDeviceManager finalThis = this;
|
||||||
|
// mHandler.postDelayed(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public void run() {
|
||||||
|
// finalThis.chromebookConnectionHandler();
|
||||||
|
// }
|
||||||
|
// }, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shutdownBluetooth() {
|
||||||
|
try {
|
||||||
|
mContext.unregisterReceiver(mBluetoothBroadcast);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// We may not have registered, that's okay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
|
||||||
|
// This function provides a sort of dummy version of that, watching for changes in the
|
||||||
|
// connected devices and attempting to add controllers as things change.
|
||||||
|
public void chromebookConnectionHandler() {
|
||||||
|
if (!mIsChromebook) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
|
||||||
|
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
|
||||||
|
|
||||||
|
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
|
||||||
|
|
||||||
|
for (BluetoothDevice bluetoothDevice : currentConnected) {
|
||||||
|
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
|
||||||
|
connected.add(bluetoothDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
|
||||||
|
if (!currentConnected.contains(bluetoothDevice)) {
|
||||||
|
disconnected.add(bluetoothDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mLastBluetoothDevices = currentConnected;
|
||||||
|
|
||||||
|
for (BluetoothDevice bluetoothDevice : disconnected) {
|
||||||
|
disconnectBluetoothDevice(bluetoothDevice);
|
||||||
|
}
|
||||||
|
for (BluetoothDevice bluetoothDevice : connected) {
|
||||||
|
connectBluetoothDevice(bluetoothDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
final HIDDeviceManager finalThis = this;
|
||||||
|
mHandler.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
finalThis.chromebookConnectionHandler();
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
|
||||||
|
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
|
||||||
|
synchronized (this) {
|
||||||
|
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
|
||||||
|
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
|
||||||
|
|
||||||
|
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
|
||||||
|
device.reconnect();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
|
||||||
|
int id = device.getId();
|
||||||
|
mBluetoothDevices.put(bluetoothDevice, device);
|
||||||
|
mDevicesById.put(id, device);
|
||||||
|
|
||||||
|
// The Steam Controller will mark itself connected once initialization is complete
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
|
||||||
|
synchronized (this) {
|
||||||
|
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
|
||||||
|
if (device == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int id = device.getId();
|
||||||
|
mBluetoothDevices.remove(bluetoothDevice);
|
||||||
|
mDevicesById.remove(id);
|
||||||
|
device.shutdown();
|
||||||
|
HIDDeviceDisconnected(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSteamController(BluetoothDevice bluetoothDevice) {
|
||||||
|
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
|
||||||
|
if (bluetoothDevice == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the device has no local name, we really don't want to try an equality check against it.
|
||||||
|
if (bluetoothDevice.getName() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close() {
|
||||||
|
shutdownUSB();
|
||||||
|
shutdownBluetooth();
|
||||||
|
synchronized (this) {
|
||||||
|
for (HIDDevice device : mDevicesById.values()) {
|
||||||
|
device.shutdown();
|
||||||
|
}
|
||||||
|
mDevicesById.clear();
|
||||||
|
mBluetoothDevices.clear();
|
||||||
|
HIDDeviceReleaseCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFrozen(boolean frozen) {
|
||||||
|
synchronized (this) {
|
||||||
|
for (HIDDevice device : mDevicesById.values()) {
|
||||||
|
device.setFrozen(frozen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private HIDDevice getDevice(int id) {
|
||||||
|
synchronized (this) {
|
||||||
|
HIDDevice result = mDevicesById.get(id);
|
||||||
|
if (result == null) {
|
||||||
|
Log.v(TAG, "No device for id: " + id);
|
||||||
|
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////// JNI interface functions
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public boolean initialize(boolean usb, boolean bluetooth) {
|
||||||
|
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
|
||||||
|
|
||||||
|
if (usb) {
|
||||||
|
initializeUSB();
|
||||||
|
}
|
||||||
|
if (bluetooth) {
|
||||||
|
initializeBluetooth();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean openDevice(int deviceID) {
|
||||||
|
Log.v(TAG, "openDevice deviceID=" + deviceID);
|
||||||
|
HIDDevice device = getDevice(deviceID);
|
||||||
|
if (device == null) {
|
||||||
|
HIDDeviceDisconnected(deviceID);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look to see if this is a USB device and we have permission to access it
|
||||||
|
UsbDevice usbDevice = device.getDevice();
|
||||||
|
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
|
||||||
|
HIDDeviceOpenPending(deviceID);
|
||||||
|
try {
|
||||||
|
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
|
||||||
|
int flags;
|
||||||
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
|
flags = FLAG_MUTABLE;
|
||||||
|
} else {
|
||||||
|
flags = 0;
|
||||||
|
}
|
||||||
|
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
|
||||||
|
HIDDeviceOpenResult(deviceID, false);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return device.open();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int sendOutputReport(int deviceID, byte[] report) {
|
||||||
|
try {
|
||||||
|
//Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length);
|
||||||
|
HIDDevice device;
|
||||||
|
device = getDevice(deviceID);
|
||||||
|
if (device == null) {
|
||||||
|
HIDDeviceDisconnected(deviceID);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return device.sendOutputReport(report);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int sendFeatureReport(int deviceID, byte[] report) {
|
||||||
|
try {
|
||||||
|
//Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
|
||||||
|
HIDDevice device;
|
||||||
|
device = getDevice(deviceID);
|
||||||
|
if (device == null) {
|
||||||
|
HIDDeviceDisconnected(deviceID);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return device.sendFeatureReport(report);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getFeatureReport(int deviceID, byte[] report) {
|
||||||
|
try {
|
||||||
|
//Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
|
||||||
|
HIDDevice device;
|
||||||
|
device = getDevice(deviceID);
|
||||||
|
if (device == null) {
|
||||||
|
HIDDeviceDisconnected(deviceID);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return device.getFeatureReport(report);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeDevice(int deviceID) {
|
||||||
|
try {
|
||||||
|
Log.v(TAG, "closeDevice deviceID=" + deviceID);
|
||||||
|
HIDDevice device;
|
||||||
|
device = getDevice(deviceID);
|
||||||
|
if (device == null) {
|
||||||
|
HIDDeviceDisconnected(deviceID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////// Native methods
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private native void HIDDeviceRegisterCallback();
|
||||||
|
private native void HIDDeviceReleaseCallback();
|
||||||
|
|
||||||
|
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol);
|
||||||
|
native void HIDDeviceOpenPending(int deviceID);
|
||||||
|
native void HIDDeviceOpenResult(int deviceID, boolean opened);
|
||||||
|
native void HIDDeviceDisconnected(int deviceID);
|
||||||
|
|
||||||
|
native void HIDDeviceInputReport(int deviceID, byte[] report);
|
||||||
|
native void HIDDeviceFeatureReport(int deviceID, byte[] report);
|
||||||
|
}
|
309
android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceUSB.java
Normal file
309
android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceUSB.java
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.hardware.usb.*;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
class HIDDeviceUSB implements HIDDevice {
|
||||||
|
|
||||||
|
private static final String TAG = "hidapi";
|
||||||
|
|
||||||
|
protected HIDDeviceManager mManager;
|
||||||
|
protected UsbDevice mDevice;
|
||||||
|
protected int mInterfaceIndex;
|
||||||
|
protected int mInterface;
|
||||||
|
protected int mDeviceId;
|
||||||
|
protected UsbDeviceConnection mConnection;
|
||||||
|
protected UsbEndpoint mInputEndpoint;
|
||||||
|
protected UsbEndpoint mOutputEndpoint;
|
||||||
|
protected InputThread mInputThread;
|
||||||
|
protected boolean mRunning;
|
||||||
|
protected boolean mFrozen;
|
||||||
|
|
||||||
|
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
|
||||||
|
mManager = manager;
|
||||||
|
mDevice = usbDevice;
|
||||||
|
mInterfaceIndex = interface_index;
|
||||||
|
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
|
||||||
|
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
|
||||||
|
mRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdentifier() {
|
||||||
|
return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getId() {
|
||||||
|
return mDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVendorId() {
|
||||||
|
return mDevice.getVendorId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getProductId() {
|
||||||
|
return mDevice.getProductId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSerialNumber() {
|
||||||
|
String result = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
try {
|
||||||
|
result = mDevice.getSerialNumber();
|
||||||
|
}
|
||||||
|
catch (SecurityException exception) {
|
||||||
|
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
result = "";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVersion() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getManufacturerName() {
|
||||||
|
String result = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
result = mDevice.getManufacturerName();
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
result = String.format("%x", getVendorId());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProductName() {
|
||||||
|
String result = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
result = mDevice.getProductName();
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
result = String.format("%x", getProductId());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UsbDevice getDevice() {
|
||||||
|
return mDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDeviceName() {
|
||||||
|
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean open() {
|
||||||
|
mConnection = mManager.getUSBManager().openDevice(mDevice);
|
||||||
|
if (mConnection == null) {
|
||||||
|
Log.w(TAG, "Unable to open USB device " + getDeviceName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force claim our interface
|
||||||
|
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||||
|
if (!mConnection.claimInterface(iface, true)) {
|
||||||
|
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the endpoints
|
||||||
|
for (int j = 0; j < iface.getEndpointCount(); j++) {
|
||||||
|
UsbEndpoint endpt = iface.getEndpoint(j);
|
||||||
|
switch (endpt.getDirection()) {
|
||||||
|
case UsbConstants.USB_DIR_IN:
|
||||||
|
if (mInputEndpoint == null) {
|
||||||
|
mInputEndpoint = endpt;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UsbConstants.USB_DIR_OUT:
|
||||||
|
if (mOutputEndpoint == null) {
|
||||||
|
mOutputEndpoint = endpt;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the required endpoints were present
|
||||||
|
if (mInputEndpoint == null || mOutputEndpoint == null) {
|
||||||
|
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening for input
|
||||||
|
mRunning = true;
|
||||||
|
mInputThread = new InputThread();
|
||||||
|
mInputThread.start();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendFeatureReport(byte[] report) {
|
||||||
|
int res = -1;
|
||||||
|
int offset = 0;
|
||||||
|
int length = report.length;
|
||||||
|
boolean skipped_report_id = false;
|
||||||
|
byte report_number = report[0];
|
||||||
|
|
||||||
|
if (report_number == 0x0) {
|
||||||
|
++offset;
|
||||||
|
--length;
|
||||||
|
skipped_report_id = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
res = mConnection.controlTransfer(
|
||||||
|
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
|
||||||
|
0x09/*HID set_report*/,
|
||||||
|
(3/*HID feature*/ << 8) | report_number,
|
||||||
|
mInterface,
|
||||||
|
report, offset, length,
|
||||||
|
1000/*timeout millis*/);
|
||||||
|
|
||||||
|
if (res < 0) {
|
||||||
|
Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipped_report_id) {
|
||||||
|
++length;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendOutputReport(byte[] report) {
|
||||||
|
int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
|
||||||
|
if (r != report.length) {
|
||||||
|
Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName());
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getFeatureReport(byte[] report) {
|
||||||
|
int res = -1;
|
||||||
|
int offset = 0;
|
||||||
|
int length = report.length;
|
||||||
|
boolean skipped_report_id = false;
|
||||||
|
byte report_number = report[0];
|
||||||
|
|
||||||
|
if (report_number == 0x0) {
|
||||||
|
/* Offset the return buffer by 1, so that the report ID
|
||||||
|
will remain in byte 0. */
|
||||||
|
++offset;
|
||||||
|
--length;
|
||||||
|
skipped_report_id = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
res = mConnection.controlTransfer(
|
||||||
|
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
|
||||||
|
0x01/*HID get_report*/,
|
||||||
|
(3/*HID feature*/ << 8) | report_number,
|
||||||
|
mInterface,
|
||||||
|
report, offset, length,
|
||||||
|
1000/*timeout millis*/);
|
||||||
|
|
||||||
|
if (res < 0) {
|
||||||
|
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipped_report_id) {
|
||||||
|
++res;
|
||||||
|
++length;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] data;
|
||||||
|
if (res == length) {
|
||||||
|
data = report;
|
||||||
|
} else {
|
||||||
|
data = Arrays.copyOfRange(report, 0, res);
|
||||||
|
}
|
||||||
|
mManager.HIDDeviceFeatureReport(mDeviceId, data);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
mRunning = false;
|
||||||
|
if (mInputThread != null) {
|
||||||
|
while (mInputThread.isAlive()) {
|
||||||
|
mInputThread.interrupt();
|
||||||
|
try {
|
||||||
|
mInputThread.join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Keep trying until we're done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mInputThread = null;
|
||||||
|
}
|
||||||
|
if (mConnection != null) {
|
||||||
|
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||||
|
mConnection.releaseInterface(iface);
|
||||||
|
mConnection.close();
|
||||||
|
mConnection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
close();
|
||||||
|
mManager = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFrozen(boolean frozen) {
|
||||||
|
mFrozen = frozen;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class InputThread extends Thread {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int packetSize = mInputEndpoint.getMaxPacketSize();
|
||||||
|
byte[] packet = new byte[packetSize];
|
||||||
|
while (mRunning) {
|
||||||
|
int r;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (r < 0) {
|
||||||
|
// Could be a timeout or an I/O error
|
||||||
|
}
|
||||||
|
if (r > 0) {
|
||||||
|
byte[] data;
|
||||||
|
if (r == packetSize) {
|
||||||
|
data = packet;
|
||||||
|
} else {
|
||||||
|
data = Arrays.copyOfRange(packet, 0, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mFrozen) {
|
||||||
|
mManager.HIDDeviceInputReport(mDeviceId, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
87
android/vcmi-app/src/main/java/org/libsdl/app/SDL.java
Normal file
87
android/vcmi-app/src/main/java/org/libsdl/app/SDL.java
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import java.lang.Class;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import eu.vcmi.vcmi.NativeMethods;
|
||||||
|
|
||||||
|
/**
|
||||||
|
SDL library initialization
|
||||||
|
*/
|
||||||
|
public class SDL {
|
||||||
|
|
||||||
|
// This function should be called first and sets up the native code
|
||||||
|
// so it can call into the Java classes
|
||||||
|
public static void setupJNI() {
|
||||||
|
SDLActivity.nativeSetupJNI();
|
||||||
|
SDLAudioManager.nativeSetupJNI();
|
||||||
|
SDLControllerManager.nativeSetupJNI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function should be called each time the activity is started
|
||||||
|
public static void initialize() {
|
||||||
|
setContext(null);
|
||||||
|
|
||||||
|
SDLActivity.initialize();
|
||||||
|
SDLAudioManager.initialize();
|
||||||
|
SDLControllerManager.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function stores the current activity (SDL or not)
|
||||||
|
public static void setContext(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Context getContext() {
|
||||||
|
return mContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
|
||||||
|
|
||||||
|
if (libraryName == null) {
|
||||||
|
throw new NullPointerException("No library name provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Let's see if we have ReLinker available in the project. This is necessary for
|
||||||
|
// some projects that have huge numbers of local libraries bundled, and thus may
|
||||||
|
// trip a bug in Android's native library loader which ReLinker works around. (If
|
||||||
|
// loadLibrary works properly, ReLinker will simply use the normal Android method
|
||||||
|
// internally.)
|
||||||
|
//
|
||||||
|
// To use ReLinker, just add it as a dependency. For more information, see
|
||||||
|
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
|
||||||
|
//
|
||||||
|
Class<?> relinkClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
|
||||||
|
Class<?> relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
|
||||||
|
Class<?> contextClass = mContext.getClassLoader().loadClass("android.content.Context");
|
||||||
|
Class<?> stringClass = mContext.getClassLoader().loadClass("java.lang.String");
|
||||||
|
|
||||||
|
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
|
||||||
|
// they've changed during updates.
|
||||||
|
Method forceMethod = relinkClass.getDeclaredMethod("force");
|
||||||
|
Object relinkInstance = forceMethod.invoke(null);
|
||||||
|
Class<?> relinkInstanceClass = relinkInstance.getClass();
|
||||||
|
|
||||||
|
// Actually load the library!
|
||||||
|
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
|
||||||
|
loadMethod.invoke(relinkInstance, mContext, libraryName, null, null);
|
||||||
|
}
|
||||||
|
catch (final Throwable e) {
|
||||||
|
// Fall back
|
||||||
|
try {
|
||||||
|
System.loadLibrary(libraryName);
|
||||||
|
}
|
||||||
|
catch (final UnsatisfiedLinkError ule) {
|
||||||
|
throw ule;
|
||||||
|
}
|
||||||
|
catch (final SecurityException se) {
|
||||||
|
throw se;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Context mContext;
|
||||||
|
}
|
1931
android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java
Normal file
1931
android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,394 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.media.AudioRecord;
|
||||||
|
import android.media.AudioTrack;
|
||||||
|
import android.media.MediaRecorder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public class SDLAudioManager
|
||||||
|
{
|
||||||
|
protected static final String TAG = "SDLAudio";
|
||||||
|
|
||||||
|
protected static AudioTrack mAudioTrack;
|
||||||
|
protected static AudioRecord mAudioRecord;
|
||||||
|
|
||||||
|
public static void initialize() {
|
||||||
|
mAudioTrack = null;
|
||||||
|
mAudioRecord = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
|
||||||
|
protected static String getAudioFormatString(int audioFormat) {
|
||||||
|
switch (audioFormat) {
|
||||||
|
case AudioFormat.ENCODING_PCM_8BIT:
|
||||||
|
return "8-bit";
|
||||||
|
case AudioFormat.ENCODING_PCM_16BIT:
|
||||||
|
return "16-bit";
|
||||||
|
case AudioFormat.ENCODING_PCM_FLOAT:
|
||||||
|
return "float";
|
||||||
|
default:
|
||||||
|
return Integer.toString(audioFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
|
||||||
|
int channelConfig;
|
||||||
|
int sampleSize;
|
||||||
|
int frameSize;
|
||||||
|
|
||||||
|
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz");
|
||||||
|
|
||||||
|
/* On older devices let's use known good settings */
|
||||||
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
|
if (desiredChannels > 2) {
|
||||||
|
desiredChannels = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */
|
||||||
|
if (Build.VERSION.SDK_INT < 22) {
|
||||||
|
if (sampleRate < 8000) {
|
||||||
|
sampleRate = 8000;
|
||||||
|
} else if (sampleRate > 48000) {
|
||||||
|
sampleRate = 48000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
|
||||||
|
int minSDKVersion = (isCapture ? 23 : 21);
|
||||||
|
if (Build.VERSION.SDK_INT < minSDKVersion) {
|
||||||
|
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (audioFormat)
|
||||||
|
{
|
||||||
|
case AudioFormat.ENCODING_PCM_8BIT:
|
||||||
|
sampleSize = 1;
|
||||||
|
break;
|
||||||
|
case AudioFormat.ENCODING_PCM_16BIT:
|
||||||
|
sampleSize = 2;
|
||||||
|
break;
|
||||||
|
case AudioFormat.ENCODING_PCM_FLOAT:
|
||||||
|
sampleSize = 4;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT");
|
||||||
|
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
|
||||||
|
sampleSize = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCapture) {
|
||||||
|
switch (desiredChannels) {
|
||||||
|
case 1:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_MONO;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
|
||||||
|
desiredChannels = 2;
|
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (desiredChannels) {
|
||||||
|
case 1:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
|
||||||
|
} else {
|
||||||
|
Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround");
|
||||||
|
desiredChannels = 6;
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
|
||||||
|
desiredChannels = 2;
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Log.v(TAG, "Speaker configuration (and order of channels):");
|
||||||
|
|
||||||
|
if ((channelConfig & 0x00000004) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000008) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000010) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000020) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000040) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_BACK_LEFT");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000080) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000100) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000200) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000400) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_BACK_CENTER");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000800) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00001000) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
frameSize = (sampleSize * desiredChannels);
|
||||||
|
|
||||||
|
// Let the user pick a larger buffer if they really want -- but ye
|
||||||
|
// gods they probably shouldn't, the minimums are horrifyingly high
|
||||||
|
// latency already
|
||||||
|
int minBufferSize;
|
||||||
|
if (isCapture) {
|
||||||
|
minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
|
||||||
|
} else {
|
||||||
|
minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
|
||||||
|
}
|
||||||
|
desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize);
|
||||||
|
|
||||||
|
int[] results = new int[4];
|
||||||
|
|
||||||
|
if (isCapture) {
|
||||||
|
if (mAudioRecord == null) {
|
||||||
|
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
|
||||||
|
channelConfig, audioFormat, desiredFrames * frameSize);
|
||||||
|
|
||||||
|
// see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
|
||||||
|
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
|
||||||
|
Log.e(TAG, "Failed during initialization of AudioRecord");
|
||||||
|
mAudioRecord.release();
|
||||||
|
mAudioRecord = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mAudioRecord.startRecording();
|
||||||
|
}
|
||||||
|
|
||||||
|
results[0] = mAudioRecord.getSampleRate();
|
||||||
|
results[1] = mAudioRecord.getAudioFormat();
|
||||||
|
results[2] = mAudioRecord.getChannelCount();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (mAudioTrack == null) {
|
||||||
|
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
|
||||||
|
|
||||||
|
// Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
|
||||||
|
// Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
|
||||||
|
// Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
|
||||||
|
if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
|
||||||
|
/* Try again, with safer values */
|
||||||
|
|
||||||
|
Log.e(TAG, "Failed during initialization of Audio Track");
|
||||||
|
mAudioTrack.release();
|
||||||
|
mAudioTrack = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mAudioTrack.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
results[0] = mAudioTrack.getSampleRate();
|
||||||
|
results[1] = mAudioTrack.getAudioFormat();
|
||||||
|
results[2] = mAudioTrack.getChannelCount();
|
||||||
|
}
|
||||||
|
results[3] = desiredFrames;
|
||||||
|
|
||||||
|
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz");
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
|
||||||
|
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void audioWriteFloatBuffer(float[] buffer) {
|
||||||
|
if (mAudioTrack == null) {
|
||||||
|
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < buffer.length;) {
|
||||||
|
int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING);
|
||||||
|
if (result > 0) {
|
||||||
|
i += result;
|
||||||
|
} else if (result == 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1);
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
// Nom nom
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "SDL audio: error return from write(float)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void audioWriteShortBuffer(short[] buffer) {
|
||||||
|
if (mAudioTrack == null) {
|
||||||
|
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < buffer.length;) {
|
||||||
|
int result = mAudioTrack.write(buffer, i, buffer.length - i);
|
||||||
|
if (result > 0) {
|
||||||
|
i += result;
|
||||||
|
} else if (result == 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1);
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
// Nom nom
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "SDL audio: error return from write(short)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void audioWriteByteBuffer(byte[] buffer) {
|
||||||
|
if (mAudioTrack == null) {
|
||||||
|
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < buffer.length; ) {
|
||||||
|
int result = mAudioTrack.write(buffer, i, buffer.length - i);
|
||||||
|
if (result > 0) {
|
||||||
|
i += result;
|
||||||
|
} else if (result == 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1);
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
// Nom nom
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "SDL audio: error return from write(byte)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
|
||||||
|
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
|
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
|
||||||
|
if (Build.VERSION.SDK_INT < 23) {
|
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length);
|
||||||
|
} else {
|
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
|
||||||
|
if (Build.VERSION.SDK_INT < 23) {
|
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length);
|
||||||
|
} else {
|
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static void audioClose() {
|
||||||
|
if (mAudioTrack != null) {
|
||||||
|
mAudioTrack.stop();
|
||||||
|
mAudioTrack.release();
|
||||||
|
mAudioTrack = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static void captureClose() {
|
||||||
|
if (mAudioRecord != null) {
|
||||||
|
mAudioRecord.stop();
|
||||||
|
mAudioRecord.release();
|
||||||
|
mAudioRecord = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static void audioSetThreadPriority(boolean iscapture, int device_id) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
/* Set thread name */
|
||||||
|
if (iscapture) {
|
||||||
|
Thread.currentThread().setName("SDLAudioC" + device_id);
|
||||||
|
} else {
|
||||||
|
Thread.currentThread().setName("SDLAudioP" + device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set thread priority */
|
||||||
|
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.v(TAG, "modify thread properties failed " + e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native int nativeSetupJNI();
|
||||||
|
}
|
@ -0,0 +1,788 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.VibrationEffect;
|
||||||
|
import android.os.Vibrator;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
|
||||||
|
public class SDLControllerManager
|
||||||
|
{
|
||||||
|
|
||||||
|
public static native int nativeSetupJNI();
|
||||||
|
|
||||||
|
public static native int nativeAddJoystick(int device_id, String name, String desc,
|
||||||
|
int vendor_id, int product_id,
|
||||||
|
boolean is_accelerometer, int button_mask,
|
||||||
|
int naxes, int nhats, int nballs);
|
||||||
|
public static native int nativeRemoveJoystick(int device_id);
|
||||||
|
public static native int nativeAddHaptic(int device_id, String name);
|
||||||
|
public static native int nativeRemoveHaptic(int device_id);
|
||||||
|
public static native int onNativePadDown(int device_id, int keycode);
|
||||||
|
public static native int onNativePadUp(int device_id, int keycode);
|
||||||
|
public static native void onNativeJoy(int device_id, int axis,
|
||||||
|
float value);
|
||||||
|
public static native void onNativeHat(int device_id, int hat_id,
|
||||||
|
int x, int y);
|
||||||
|
|
||||||
|
protected static SDLJoystickHandler mJoystickHandler;
|
||||||
|
protected static SDLHapticHandler mHapticHandler;
|
||||||
|
|
||||||
|
private static final String TAG = "SDLControllerManager";
|
||||||
|
|
||||||
|
public static void initialize() {
|
||||||
|
if (mJoystickHandler == null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 19) {
|
||||||
|
mJoystickHandler = new SDLJoystickHandler_API19();
|
||||||
|
} else {
|
||||||
|
mJoystickHandler = new SDLJoystickHandler_API16();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHapticHandler == null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
|
mHapticHandler = new SDLHapticHandler_API26();
|
||||||
|
} else {
|
||||||
|
mHapticHandler = new SDLHapticHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
|
||||||
|
public static boolean handleJoystickMotionEvent(MotionEvent event) {
|
||||||
|
return mJoystickHandler.handleMotionEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void pollInputDevices() {
|
||||||
|
mJoystickHandler.pollInputDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void pollHapticDevices() {
|
||||||
|
mHapticHandler.pollHapticDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void hapticRun(int device_id, float intensity, int length) {
|
||||||
|
mHapticHandler.run(device_id, intensity, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void hapticStop(int device_id)
|
||||||
|
{
|
||||||
|
mHapticHandler.stop(device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a given device is considered a possible SDL joystick
|
||||||
|
public static boolean isDeviceSDLJoystick(int deviceId) {
|
||||||
|
InputDevice device = InputDevice.getDevice(deviceId);
|
||||||
|
// We cannot use InputDevice.isVirtual before API 16, so let's accept
|
||||||
|
// only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
|
||||||
|
if ((device == null) || (deviceId < 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int sources = device.getSources();
|
||||||
|
|
||||||
|
/* This is called for every button press, so let's not spam the logs */
|
||||||
|
/*
|
||||||
|
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||||
|
Log.v(TAG, "Input device " + device.getName() + " has class joystick.");
|
||||||
|
}
|
||||||
|
if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
|
||||||
|
Log.v(TAG, "Input device " + device.getName() + " is a dpad.");
|
||||||
|
}
|
||||||
|
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
|
||||||
|
Log.v(TAG, "Input device " + device.getName() + " is a gamepad.");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ||
|
||||||
|
((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
|
||||||
|
((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLJoystickHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles given MotionEvent.
|
||||||
|
* @param event the event to be handled.
|
||||||
|
* @return if given event was processed.
|
||||||
|
*/
|
||||||
|
public boolean handleMotionEvent(MotionEvent event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles adding and removing of input devices.
|
||||||
|
*/
|
||||||
|
public void pollInputDevices() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actual joystick functionality available for API >= 12 devices */
|
||||||
|
class SDLJoystickHandler_API16 extends SDLJoystickHandler {
|
||||||
|
|
||||||
|
static class SDLJoystick {
|
||||||
|
public int device_id;
|
||||||
|
public String name;
|
||||||
|
public String desc;
|
||||||
|
public ArrayList<InputDevice.MotionRange> axes;
|
||||||
|
public ArrayList<InputDevice.MotionRange> hats;
|
||||||
|
}
|
||||||
|
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
|
||||||
|
@Override
|
||||||
|
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
|
||||||
|
// Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
|
||||||
|
int arg0Axis = arg0.getAxis();
|
||||||
|
int arg1Axis = arg1.getAxis();
|
||||||
|
if (arg0Axis == MotionEvent.AXIS_GAS) {
|
||||||
|
arg0Axis = MotionEvent.AXIS_BRAKE;
|
||||||
|
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
|
||||||
|
arg0Axis = MotionEvent.AXIS_GAS;
|
||||||
|
}
|
||||||
|
if (arg1Axis == MotionEvent.AXIS_GAS) {
|
||||||
|
arg1Axis = MotionEvent.AXIS_BRAKE;
|
||||||
|
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
|
||||||
|
arg1Axis = MotionEvent.AXIS_GAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arg0Axis - arg1Axis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ArrayList<SDLJoystick> mJoysticks;
|
||||||
|
|
||||||
|
public SDLJoystickHandler_API16() {
|
||||||
|
|
||||||
|
mJoysticks = new ArrayList<SDLJoystick>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pollInputDevices() {
|
||||||
|
int[] deviceIds = InputDevice.getDeviceIds();
|
||||||
|
|
||||||
|
for (int device_id : deviceIds) {
|
||||||
|
if (SDLControllerManager.isDeviceSDLJoystick(device_id)) {
|
||||||
|
SDLJoystick joystick = getJoystick(device_id);
|
||||||
|
if (joystick == null) {
|
||||||
|
InputDevice joystickDevice = InputDevice.getDevice(device_id);
|
||||||
|
joystick = new SDLJoystick();
|
||||||
|
joystick.device_id = device_id;
|
||||||
|
joystick.name = joystickDevice.getName();
|
||||||
|
joystick.desc = getJoystickDescriptor(joystickDevice);
|
||||||
|
joystick.axes = new ArrayList<InputDevice.MotionRange>();
|
||||||
|
joystick.hats = new ArrayList<InputDevice.MotionRange>();
|
||||||
|
|
||||||
|
List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
|
||||||
|
Collections.sort(ranges, new RangeComparator());
|
||||||
|
for (InputDevice.MotionRange range : ranges) {
|
||||||
|
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||||
|
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
|
||||||
|
joystick.hats.add(range);
|
||||||
|
} else {
|
||||||
|
joystick.axes.add(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mJoysticks.add(joystick);
|
||||||
|
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
|
||||||
|
getVendorId(joystickDevice), getProductId(joystickDevice), false,
|
||||||
|
getButtonMask(joystickDevice), joystick.axes.size(), joystick.hats.size()/2, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check removed devices */
|
||||||
|
ArrayList<Integer> removedDevices = null;
|
||||||
|
for (SDLJoystick joystick : mJoysticks) {
|
||||||
|
int device_id = joystick.device_id;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < deviceIds.length; i++) {
|
||||||
|
if (device_id == deviceIds[i]) break;
|
||||||
|
}
|
||||||
|
if (i == deviceIds.length) {
|
||||||
|
if (removedDevices == null) {
|
||||||
|
removedDevices = new ArrayList<Integer>();
|
||||||
|
}
|
||||||
|
removedDevices.add(device_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedDevices != null) {
|
||||||
|
for (int device_id : removedDevices) {
|
||||||
|
SDLControllerManager.nativeRemoveJoystick(device_id);
|
||||||
|
for (int i = 0; i < mJoysticks.size(); i++) {
|
||||||
|
if (mJoysticks.get(i).device_id == device_id) {
|
||||||
|
mJoysticks.remove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SDLJoystick getJoystick(int device_id) {
|
||||||
|
for (SDLJoystick joystick : mJoysticks) {
|
||||||
|
if (joystick.device_id == device_id) {
|
||||||
|
return joystick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleMotionEvent(MotionEvent event) {
|
||||||
|
int actionPointerIndex = event.getActionIndex();
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
if (action == MotionEvent.ACTION_MOVE) {
|
||||||
|
SDLJoystick joystick = getJoystick(event.getDeviceId());
|
||||||
|
if (joystick != null) {
|
||||||
|
for (int i = 0; i < joystick.axes.size(); i++) {
|
||||||
|
InputDevice.MotionRange range = joystick.axes.get(i);
|
||||||
|
/* Normalize the value to -1...1 */
|
||||||
|
float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
|
||||||
|
SDLControllerManager.onNativeJoy(joystick.device_id, i, value);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < joystick.hats.size() / 2; i++) {
|
||||||
|
int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex));
|
||||||
|
int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex));
|
||||||
|
SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getJoystickDescriptor(InputDevice joystickDevice) {
|
||||||
|
String desc = joystickDevice.getDescriptor();
|
||||||
|
|
||||||
|
if (desc != null && !desc.isEmpty()) {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return joystickDevice.getName();
|
||||||
|
}
|
||||||
|
public int getProductId(InputDevice joystickDevice) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public int getVendorId(InputDevice joystickDevice) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public int getButtonMask(InputDevice joystickDevice) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getProductId(InputDevice joystickDevice) {
|
||||||
|
return joystickDevice.getProductId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVendorId(InputDevice joystickDevice) {
|
||||||
|
return joystickDevice.getVendorId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getButtonMask(InputDevice joystickDevice) {
|
||||||
|
int button_mask = 0;
|
||||||
|
int[] keys = new int[] {
|
||||||
|
KeyEvent.KEYCODE_BUTTON_A,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_B,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_X,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_Y,
|
||||||
|
KeyEvent.KEYCODE_BACK,
|
||||||
|
KeyEvent.KEYCODE_MENU,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_MODE,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_START,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_THUMBL,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_THUMBR,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_L1,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_R1,
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP,
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_SELECT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||||
|
|
||||||
|
// These don't map into any SDL controller buttons directly
|
||||||
|
KeyEvent.KEYCODE_BUTTON_L2,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_R2,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_C,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_Z,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_1,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_2,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_3,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_4,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_5,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_6,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_7,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_8,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_9,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_10,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_11,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_12,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_13,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_14,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_15,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_16,
|
||||||
|
};
|
||||||
|
int[] masks = new int[] {
|
||||||
|
(1 << 0), // A -> A
|
||||||
|
(1 << 1), // B -> B
|
||||||
|
(1 << 2), // X -> X
|
||||||
|
(1 << 3), // Y -> Y
|
||||||
|
(1 << 4), // BACK -> BACK
|
||||||
|
(1 << 6), // MENU -> START
|
||||||
|
(1 << 5), // MODE -> GUIDE
|
||||||
|
(1 << 6), // START -> START
|
||||||
|
(1 << 7), // THUMBL -> LEFTSTICK
|
||||||
|
(1 << 8), // THUMBR -> RIGHTSTICK
|
||||||
|
(1 << 9), // L1 -> LEFTSHOULDER
|
||||||
|
(1 << 10), // R1 -> RIGHTSHOULDER
|
||||||
|
(1 << 11), // DPAD_UP -> DPAD_UP
|
||||||
|
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
|
||||||
|
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
|
||||||
|
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
|
||||||
|
(1 << 4), // SELECT -> BACK
|
||||||
|
(1 << 0), // DPAD_CENTER -> A
|
||||||
|
(1 << 15), // L2 -> ??
|
||||||
|
(1 << 16), // R2 -> ??
|
||||||
|
(1 << 17), // C -> ??
|
||||||
|
(1 << 18), // Z -> ??
|
||||||
|
(1 << 20), // 1 -> ??
|
||||||
|
(1 << 21), // 2 -> ??
|
||||||
|
(1 << 22), // 3 -> ??
|
||||||
|
(1 << 23), // 4 -> ??
|
||||||
|
(1 << 24), // 5 -> ??
|
||||||
|
(1 << 25), // 6 -> ??
|
||||||
|
(1 << 26), // 7 -> ??
|
||||||
|
(1 << 27), // 8 -> ??
|
||||||
|
(1 << 28), // 9 -> ??
|
||||||
|
(1 << 29), // 10 -> ??
|
||||||
|
(1 << 30), // 11 -> ??
|
||||||
|
(1 << 31), // 12 -> ??
|
||||||
|
// We're out of room...
|
||||||
|
0xFFFFFFFF, // 13 -> ??
|
||||||
|
0xFFFFFFFF, // 14 -> ??
|
||||||
|
0xFFFFFFFF, // 15 -> ??
|
||||||
|
0xFFFFFFFF, // 16 -> ??
|
||||||
|
};
|
||||||
|
boolean[] has_keys = joystickDevice.hasKeys(keys);
|
||||||
|
for (int i = 0; i < keys.length; ++i) {
|
||||||
|
if (has_keys[i]) {
|
||||||
|
button_mask |= masks[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return button_mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLHapticHandler_API26 extends SDLHapticHandler {
|
||||||
|
@Override
|
||||||
|
public void run(int device_id, float intensity, int length) {
|
||||||
|
SDLHaptic haptic = getHaptic(device_id);
|
||||||
|
if (haptic != null) {
|
||||||
|
Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length);
|
||||||
|
if (intensity == 0.0f) {
|
||||||
|
stop(device_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vibeValue = Math.round(intensity * 255);
|
||||||
|
|
||||||
|
if (vibeValue > 255) {
|
||||||
|
vibeValue = 255;
|
||||||
|
}
|
||||||
|
if (vibeValue < 1) {
|
||||||
|
stop(device_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
|
||||||
|
// something went horribly wrong with the Android 8.0 APIs.
|
||||||
|
haptic.vib.vibrate(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLHapticHandler {
|
||||||
|
|
||||||
|
static class SDLHaptic {
|
||||||
|
public int device_id;
|
||||||
|
public String name;
|
||||||
|
public Vibrator vib;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ArrayList<SDLHaptic> mHaptics;
|
||||||
|
|
||||||
|
public SDLHapticHandler() {
|
||||||
|
mHaptics = new ArrayList<SDLHaptic>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run(int device_id, float intensity, int length) {
|
||||||
|
SDLHaptic haptic = getHaptic(device_id);
|
||||||
|
if (haptic != null) {
|
||||||
|
haptic.vib.vibrate(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop(int device_id) {
|
||||||
|
SDLHaptic haptic = getHaptic(device_id);
|
||||||
|
if (haptic != null) {
|
||||||
|
haptic.vib.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pollHapticDevices() {
|
||||||
|
|
||||||
|
final int deviceId_VIBRATOR_SERVICE = 999999;
|
||||||
|
boolean hasVibratorService = false;
|
||||||
|
|
||||||
|
int[] deviceIds = InputDevice.getDeviceIds();
|
||||||
|
// It helps processing the device ids in reverse order
|
||||||
|
// For example, in the case of the XBox 360 wireless dongle,
|
||||||
|
// so the first controller seen by SDL matches what the receiver
|
||||||
|
// considers to be the first controller
|
||||||
|
|
||||||
|
for (int i = deviceIds.length - 1; i > -1; i--) {
|
||||||
|
SDLHaptic haptic = getHaptic(deviceIds[i]);
|
||||||
|
if (haptic == null) {
|
||||||
|
InputDevice device = InputDevice.getDevice(deviceIds[i]);
|
||||||
|
Vibrator vib = device.getVibrator();
|
||||||
|
if (vib.hasVibrator()) {
|
||||||
|
haptic = new SDLHaptic();
|
||||||
|
haptic.device_id = deviceIds[i];
|
||||||
|
haptic.name = device.getName();
|
||||||
|
haptic.vib = vib;
|
||||||
|
mHaptics.add(haptic);
|
||||||
|
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check VIBRATOR_SERVICE */
|
||||||
|
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
|
if (vib != null) {
|
||||||
|
hasVibratorService = vib.hasVibrator();
|
||||||
|
|
||||||
|
if (hasVibratorService) {
|
||||||
|
SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE);
|
||||||
|
if (haptic == null) {
|
||||||
|
haptic = new SDLHaptic();
|
||||||
|
haptic.device_id = deviceId_VIBRATOR_SERVICE;
|
||||||
|
haptic.name = "VIBRATOR_SERVICE";
|
||||||
|
haptic.vib = vib;
|
||||||
|
mHaptics.add(haptic);
|
||||||
|
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check removed devices */
|
||||||
|
ArrayList<Integer> removedDevices = null;
|
||||||
|
for (SDLHaptic haptic : mHaptics) {
|
||||||
|
int device_id = haptic.device_id;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < deviceIds.length; i++) {
|
||||||
|
if (device_id == deviceIds[i]) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
|
||||||
|
if (i == deviceIds.length) {
|
||||||
|
if (removedDevices == null) {
|
||||||
|
removedDevices = new ArrayList<Integer>();
|
||||||
|
}
|
||||||
|
removedDevices.add(device_id);
|
||||||
|
}
|
||||||
|
} // else: don't remove the vibrator if it is still present
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedDevices != null) {
|
||||||
|
for (int device_id : removedDevices) {
|
||||||
|
SDLControllerManager.nativeRemoveHaptic(device_id);
|
||||||
|
for (int i = 0; i < mHaptics.size(); i++) {
|
||||||
|
if (mHaptics.get(i).device_id == device_id) {
|
||||||
|
mHaptics.remove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SDLHaptic getHaptic(int device_id) {
|
||||||
|
for (SDLHaptic haptic : mHaptics) {
|
||||||
|
if (haptic.device_id == device_id) {
|
||||||
|
return haptic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
|
||||||
|
// Generic Motion (mouse hover, joystick...) events go here
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||||
|
float x, y;
|
||||||
|
int action;
|
||||||
|
|
||||||
|
switch ( event.getSource() ) {
|
||||||
|
case InputDevice.SOURCE_JOYSTICK:
|
||||||
|
return SDLControllerManager.handleJoystickMotionEvent(event);
|
||||||
|
|
||||||
|
case InputDevice.SOURCE_MOUSE:
|
||||||
|
action = event.getActionMasked();
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_SCROLL:
|
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
x = event.getX(0);
|
||||||
|
y = event.getY(0);
|
||||||
|
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event was not managed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsRelativeMouse() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean inRelativeMode() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reclaimRelativeMouseModeIfNeeded()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getEventX(MotionEvent event) {
|
||||||
|
return event.getX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getEventY(MotionEvent event) {
|
||||||
|
return event.getY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 {
|
||||||
|
// Generic Motion (mouse hover, joystick...) events go here
|
||||||
|
|
||||||
|
private boolean mRelativeModeEnabled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||||
|
|
||||||
|
// Handle relative mouse mode
|
||||||
|
if (mRelativeModeEnabled) {
|
||||||
|
if (event.getSource() == InputDevice.SOURCE_MOUSE) {
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
if (action == MotionEvent.ACTION_HOVER_MOVE) {
|
||||||
|
float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||||
|
float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event was not managed, call SDLGenericMotionListener_API12 method
|
||||||
|
return super.onGenericMotion(v, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRelativeMouse() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inRelativeMode() {
|
||||||
|
return mRelativeModeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||||
|
mRelativeModeEnabled = enabled;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getEventX(MotionEvent event) {
|
||||||
|
if (mRelativeModeEnabled) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||||
|
} else {
|
||||||
|
return event.getX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getEventY(MotionEvent event) {
|
||||||
|
if (mRelativeModeEnabled) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||||
|
} else {
|
||||||
|
return event.getY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
|
||||||
|
// Generic Motion (mouse hover, joystick...) events go here
|
||||||
|
private boolean mRelativeModeEnabled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||||
|
float x, y;
|
||||||
|
int action;
|
||||||
|
|
||||||
|
switch ( event.getSource() ) {
|
||||||
|
case InputDevice.SOURCE_JOYSTICK:
|
||||||
|
return SDLControllerManager.handleJoystickMotionEvent(event);
|
||||||
|
|
||||||
|
case InputDevice.SOURCE_MOUSE:
|
||||||
|
// DeX desktop mouse cursor is a separate non-standard input type.
|
||||||
|
case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN:
|
||||||
|
action = event.getActionMasked();
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_SCROLL:
|
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
x = event.getX(0);
|
||||||
|
y = event.getY(0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InputDevice.SOURCE_MOUSE_RELATIVE:
|
||||||
|
action = event.getActionMasked();
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_SCROLL:
|
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
x = event.getX(0);
|
||||||
|
y = event.getY(0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event was not managed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRelativeMouse() {
|
||||||
|
return (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inRelativeMode() {
|
||||||
|
return mRelativeModeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||||
|
if (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)) {
|
||||||
|
if (enabled) {
|
||||||
|
SDLActivity.getContentView().requestPointerCapture();
|
||||||
|
} else {
|
||||||
|
SDLActivity.getContentView().releasePointerCapture();
|
||||||
|
}
|
||||||
|
mRelativeModeEnabled = enabled;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reclaimRelativeMouseModeIfNeeded()
|
||||||
|
{
|
||||||
|
if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
|
||||||
|
SDLActivity.getContentView().requestPointerCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getEventX(MotionEvent event) {
|
||||||
|
// Relative mouse in capture mode will only have relative for X/Y
|
||||||
|
return event.getX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getEventY(MotionEvent event) {
|
||||||
|
// Relative mouse in capture mode will only have relative for X/Y
|
||||||
|
return event.getY(0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,144 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.BaseInputConnection;
|
||||||
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
public class SDLInputConnection extends BaseInputConnection
|
||||||
|
{
|
||||||
|
|
||||||
|
protected EditText mEditText;
|
||||||
|
protected String mCommittedText = "";
|
||||||
|
|
||||||
|
public SDLInputConnection(View targetView, boolean fullEditor)
|
||||||
|
{
|
||||||
|
super(targetView, fullEditor);
|
||||||
|
mEditText = new EditText(SDL.getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Editable getEditable()
|
||||||
|
{
|
||||||
|
return mEditText.getEditableText();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean sendKeyEvent(KeyEvent event)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard)
|
||||||
|
* However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses
|
||||||
|
* and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys
|
||||||
|
* that still do, we empty this out.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return DOES still generate a key event, however. So rather than using it as the 'click a button' key
|
||||||
|
* as we do with physical keyboards, let's just use it to hide the keyboard.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)
|
||||||
|
{
|
||||||
|
if (SDLActivity.onNativeSoftReturnKey())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.sendKeyEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean commitText(CharSequence text, int newCursorPosition)
|
||||||
|
{
|
||||||
|
if (!super.commitText(text, newCursorPosition))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
updateText();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setComposingText(CharSequence text, int newCursorPosition)
|
||||||
|
{
|
||||||
|
if (!super.setComposingText(text, newCursorPosition))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
updateText();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteSurroundingText(int beforeLength, int afterLength)
|
||||||
|
{
|
||||||
|
if (!super.deleteSurroundingText(beforeLength, afterLength))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
updateText();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateText()
|
||||||
|
{
|
||||||
|
final Editable content = getEditable();
|
||||||
|
if (content == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = content.toString();
|
||||||
|
int compareLength = Math.min(text.length(), mCommittedText.length());
|
||||||
|
int matchLength, offset;
|
||||||
|
|
||||||
|
/* Backspace over characters that are no longer in the string */
|
||||||
|
for (matchLength = 0; matchLength < compareLength; )
|
||||||
|
{
|
||||||
|
int codePoint = mCommittedText.codePointAt(matchLength);
|
||||||
|
if (codePoint != text.codePointAt(matchLength))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
matchLength += Character.charCount(codePoint);
|
||||||
|
}
|
||||||
|
/* FIXME: This doesn't handle graphemes, like '🌬️' */
|
||||||
|
for (offset = matchLength; offset < mCommittedText.length(); )
|
||||||
|
{
|
||||||
|
int codePoint = mCommittedText.codePointAt(offset);
|
||||||
|
nativeGenerateScancodeForUnichar('\b');
|
||||||
|
offset += Character.charCount(codePoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchLength < text.length())
|
||||||
|
{
|
||||||
|
String pendingText = text.subSequence(matchLength, text.length()).toString();
|
||||||
|
for (offset = 0; offset < pendingText.length(); )
|
||||||
|
{
|
||||||
|
int codePoint = pendingText.codePointAt(offset);
|
||||||
|
if (codePoint == '\n')
|
||||||
|
{
|
||||||
|
if (SDLActivity.onNativeSoftReturnKey())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Higher code points don't generate simulated scancodes */
|
||||||
|
if (codePoint < 128)
|
||||||
|
{
|
||||||
|
nativeGenerateScancodeForUnichar((char) codePoint);
|
||||||
|
}
|
||||||
|
offset += Character.charCount(codePoint);
|
||||||
|
}
|
||||||
|
SDLInputConnection.nativeCommitText(pendingText, 0);
|
||||||
|
}
|
||||||
|
mCommittedText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native void nativeCommitText(String text, int newCursorPosition);
|
||||||
|
|
||||||
|
public static native void nativeGenerateScancodeForUnichar(char c);
|
||||||
|
}
|
405
android/vcmi-app/src/main/java/org/libsdl/app/SDLSurface.java
Normal file
405
android/vcmi-app/src/main/java/org/libsdl/app/SDLSurface.java
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ActivityInfo;
|
||||||
|
import android.hardware.Sensor;
|
||||||
|
import android.hardware.SensorEvent;
|
||||||
|
import android.hardware.SensorEventListener;
|
||||||
|
import android.hardware.SensorManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Display;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.SurfaceHolder;
|
||||||
|
import android.view.SurfaceView;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
SDLSurface. This is what we draw on, so we need to know when it's created
|
||||||
|
in order to do anything useful.
|
||||||
|
|
||||||
|
Because of this, that's where we set up the SDL thread
|
||||||
|
*/
|
||||||
|
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
|
||||||
|
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
|
||||||
|
|
||||||
|
// Sensors
|
||||||
|
protected SensorManager mSensorManager;
|
||||||
|
protected Display mDisplay;
|
||||||
|
|
||||||
|
// Keep track of the surface size to normalize touch events
|
||||||
|
protected float mWidth, mHeight;
|
||||||
|
|
||||||
|
// Is SurfaceView ready for rendering
|
||||||
|
public boolean mIsSurfaceReady;
|
||||||
|
|
||||||
|
// Startup
|
||||||
|
public SDLSurface(Context context) {
|
||||||
|
super(context);
|
||||||
|
getHolder().addCallback(this);
|
||||||
|
|
||||||
|
setFocusable(true);
|
||||||
|
setFocusableInTouchMode(true);
|
||||||
|
requestFocus();
|
||||||
|
setOnKeyListener(this);
|
||||||
|
setOnTouchListener(this);
|
||||||
|
|
||||||
|
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||||||
|
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||||
|
|
||||||
|
setOnGenericMotionListener(SDLActivity.getMotionListener());
|
||||||
|
|
||||||
|
// Some arbitrary defaults to avoid a potential division by zero
|
||||||
|
mWidth = 1.0f;
|
||||||
|
mHeight = 1.0f;
|
||||||
|
|
||||||
|
mIsSurfaceReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handlePause() {
|
||||||
|
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleResume() {
|
||||||
|
setFocusable(true);
|
||||||
|
setFocusableInTouchMode(true);
|
||||||
|
requestFocus();
|
||||||
|
setOnKeyListener(this);
|
||||||
|
setOnTouchListener(this);
|
||||||
|
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Surface getNativeSurface() {
|
||||||
|
return getHolder().getSurface();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when we have a valid drawing surface
|
||||||
|
@Override
|
||||||
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
|
Log.v("SDL", "surfaceCreated()");
|
||||||
|
SDLActivity.onNativeSurfaceCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when we lose the surface
|
||||||
|
@Override
|
||||||
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
|
Log.v("SDL", "surfaceDestroyed()");
|
||||||
|
|
||||||
|
// Transition to pause, if needed
|
||||||
|
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
|
||||||
|
SDLActivity.handleNativeState();
|
||||||
|
|
||||||
|
mIsSurfaceReady = false;
|
||||||
|
SDLActivity.onNativeSurfaceDestroyed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the surface is resized
|
||||||
|
@Override
|
||||||
|
public void surfaceChanged(SurfaceHolder holder,
|
||||||
|
int format, int width, int height) {
|
||||||
|
Log.v("SDL", "surfaceChanged()");
|
||||||
|
|
||||||
|
if (SDLActivity.mSingleton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mWidth = width;
|
||||||
|
mHeight = height;
|
||||||
|
int nDeviceWidth = width;
|
||||||
|
int nDeviceHeight = height;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SDK_INT >= 17) {
|
||||||
|
DisplayMetrics realMetrics = new DisplayMetrics();
|
||||||
|
mDisplay.getRealMetrics( realMetrics );
|
||||||
|
nDeviceWidth = realMetrics.widthPixels;
|
||||||
|
nDeviceHeight = realMetrics.heightPixels;
|
||||||
|
}
|
||||||
|
} catch(Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(SDLActivity.getContext()) {
|
||||||
|
// In case we're waiting on a size change after going fullscreen, send a notification.
|
||||||
|
SDLActivity.getContext().notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v("SDL", "Window size: " + width + "x" + height);
|
||||||
|
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
|
||||||
|
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
|
||||||
|
SDLActivity.onNativeResize();
|
||||||
|
|
||||||
|
// Prevent a screen distortion glitch,
|
||||||
|
// for instance when the device is in Landscape and a Portrait App is resumed.
|
||||||
|
boolean skip = false;
|
||||||
|
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
|
||||||
|
|
||||||
|
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
|
||||||
|
if (mWidth > mHeight) {
|
||||||
|
skip = true;
|
||||||
|
}
|
||||||
|
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
|
||||||
|
if (mWidth < mHeight) {
|
||||||
|
skip = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special Patch for Square Resolution: Black Berry Passport
|
||||||
|
if (skip) {
|
||||||
|
double min = Math.min(mWidth, mHeight);
|
||||||
|
double max = Math.max(mWidth, mHeight);
|
||||||
|
|
||||||
|
if (max / min < 1.20) {
|
||||||
|
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
|
||||||
|
skip = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't skip in MultiWindow.
|
||||||
|
if (skip) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 24) {
|
||||||
|
if (SDLActivity.mSingleton.isInMultiWindowMode()) {
|
||||||
|
Log.v("SDL", "Don't skip in Multi-Window");
|
||||||
|
skip = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip) {
|
||||||
|
Log.v("SDL", "Skip .. Surface is not ready.");
|
||||||
|
mIsSurfaceReady = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
|
||||||
|
SDLActivity.onNativeSurfaceChanged();
|
||||||
|
|
||||||
|
/* Surface is ready */
|
||||||
|
mIsSurfaceReady = true;
|
||||||
|
|
||||||
|
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
|
||||||
|
SDLActivity.handleNativeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key events
|
||||||
|
@Override
|
||||||
|
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||||
|
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
|
/* Ref: http://developer.android.com/training/gestures/multi.html */
|
||||||
|
int touchDevId = event.getDeviceId();
|
||||||
|
final int pointerCount = event.getPointerCount();
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
int pointerFingerId;
|
||||||
|
int i = -1;
|
||||||
|
float x,y,p;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Prevent id to be -1, since it's used in SDL internal for synthetic events
|
||||||
|
* Appears when using Android emulator, eg:
|
||||||
|
* adb shell input mouse tap 100 100
|
||||||
|
* adb shell input touchscreen tap 100 100
|
||||||
|
*/
|
||||||
|
if (touchDevId < 0) {
|
||||||
|
touchDevId -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12290 = Samsung DeX mode desktop mouse
|
||||||
|
// 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
|
||||||
|
// 0x2 = SOURCE_CLASS_POINTER
|
||||||
|
if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
|
||||||
|
int mouseButton = 1;
|
||||||
|
try {
|
||||||
|
Object object = event.getClass().getMethod("getButtonState").invoke(event);
|
||||||
|
if (object != null) {
|
||||||
|
mouseButton = (Integer) object;
|
||||||
|
}
|
||||||
|
} catch(Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
|
||||||
|
// if we are. We'll leverage our existing mouse motion listener
|
||||||
|
SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
|
||||||
|
x = motionListener.getEventX(event);
|
||||||
|
y = motionListener.getEventY(event);
|
||||||
|
|
||||||
|
SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
|
||||||
|
} else {
|
||||||
|
switch(action) {
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
for (i = 0; i < pointerCount; i++) {
|
||||||
|
pointerFingerId = event.getPointerId(i);
|
||||||
|
x = event.getX(i) / mWidth;
|
||||||
|
y = event.getY(i) / mHeight;
|
||||||
|
p = event.getPressure(i);
|
||||||
|
if (p > 1.0f) {
|
||||||
|
// may be larger than 1.0f on some devices
|
||||||
|
// see the documentation of getPressure(i)
|
||||||
|
p = 1.0f;
|
||||||
|
}
|
||||||
|
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
// Primary pointer up/down, the index is always zero
|
||||||
|
i = 0;
|
||||||
|
/* fallthrough */
|
||||||
|
case MotionEvent.ACTION_POINTER_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN:
|
||||||
|
// Non primary pointer up/down
|
||||||
|
if (i == -1) {
|
||||||
|
i = event.getActionIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
pointerFingerId = event.getPointerId(i);
|
||||||
|
x = event.getX(i) / mWidth;
|
||||||
|
y = event.getY(i) / mHeight;
|
||||||
|
p = event.getPressure(i);
|
||||||
|
if (p > 1.0f) {
|
||||||
|
// may be larger than 1.0f on some devices
|
||||||
|
// see the documentation of getPressure(i)
|
||||||
|
p = 1.0f;
|
||||||
|
}
|
||||||
|
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
for (i = 0; i < pointerCount; i++) {
|
||||||
|
pointerFingerId = event.getPointerId(i);
|
||||||
|
x = event.getX(i) / mWidth;
|
||||||
|
y = event.getY(i) / mHeight;
|
||||||
|
p = event.getPressure(i);
|
||||||
|
if (p > 1.0f) {
|
||||||
|
// may be larger than 1.0f on some devices
|
||||||
|
// see the documentation of getPressure(i)
|
||||||
|
p = 1.0f;
|
||||||
|
}
|
||||||
|
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sensor events
|
||||||
|
public void enableSensor(int sensortype, boolean enabled) {
|
||||||
|
// TODO: This uses getDefaultSensor - what if we have >1 accels?
|
||||||
|
if (enabled) {
|
||||||
|
mSensorManager.registerListener(this,
|
||||||
|
mSensorManager.getDefaultSensor(sensortype),
|
||||||
|
SensorManager.SENSOR_DELAY_GAME, null);
|
||||||
|
} else {
|
||||||
|
mSensorManager.unregisterListener(this,
|
||||||
|
mSensorManager.getDefaultSensor(sensortype));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSensorChanged(SensorEvent event) {
|
||||||
|
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||||
|
|
||||||
|
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
|
||||||
|
// We thus should check here.
|
||||||
|
int newOrientation;
|
||||||
|
|
||||||
|
float x, y;
|
||||||
|
switch (mDisplay.getRotation()) {
|
||||||
|
case Surface.ROTATION_90:
|
||||||
|
x = -event.values[1];
|
||||||
|
y = event.values[0];
|
||||||
|
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_270:
|
||||||
|
x = event.values[1];
|
||||||
|
y = -event.values[0];
|
||||||
|
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_180:
|
||||||
|
x = -event.values[0];
|
||||||
|
y = -event.values[1];
|
||||||
|
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_0:
|
||||||
|
default:
|
||||||
|
x = event.values[0];
|
||||||
|
y = event.values[1];
|
||||||
|
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newOrientation != SDLActivity.mCurrentOrientation) {
|
||||||
|
SDLActivity.mCurrentOrientation = newOrientation;
|
||||||
|
SDLActivity.onNativeOrientationChanged(newOrientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
|
||||||
|
y / SensorManager.GRAVITY_EARTH,
|
||||||
|
event.values[2] / SensorManager.GRAVITY_EARTH);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Captured pointer events for API 26.
|
||||||
|
public boolean onCapturedPointerEvent(MotionEvent event)
|
||||||
|
{
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
|
||||||
|
float x, y;
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_SCROLL:
|
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
x = event.getX(0);
|
||||||
|
y = event.getY(0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_BUTTON_PRESS:
|
||||||
|
case MotionEvent.ACTION_BUTTON_RELEASE:
|
||||||
|
|
||||||
|
// Change our action value to what SDL's code expects.
|
||||||
|
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
|
||||||
|
action = MotionEvent.ACTION_DOWN;
|
||||||
|
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
|
||||||
|
action = MotionEvent.ACTION_UP;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = event.getX(0);
|
||||||
|
y = event.getY(0);
|
||||||
|
int button = event.getButtonState();
|
||||||
|
|
||||||
|
SDLActivity.onNativeMouse(button, action, x, y, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
BIN
android/vcmi-app/src/main/res/drawable-nodpi/divider_compat.png
Normal file
BIN
android/vcmi-app/src/main/res/drawable-nodpi/divider_compat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 156 B |
@ -0,0 +1,30 @@
|
|||||||
|
<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>
|
@ -0,0 +1,4 @@
|
|||||||
|
<?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>
|
9
android/vcmi-app/src/main/res/drawable/ic_error.xml
Normal file
9
android/vcmi-app/src/main/res/drawable/ic_error.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
9
android/vcmi-app/src/main/res/drawable/ic_info.xml
Normal file
9
android/vcmi-app/src/main/res/drawable/ic_info.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
@ -0,0 +1,170 @@
|
|||||||
|
<?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>
|
9
android/vcmi-app/src/main/res/drawable/ic_star_empty.xml
Normal file
9
android/vcmi-app/src/main/res/drawable/ic_star_empty.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
9
android/vcmi-app/src/main/res/drawable/ic_star_full.xml
Normal file
9
android/vcmi-app/src/main/res/drawable/ic_star_full.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
9
android/vcmi-app/src/main/res/drawable/ic_star_half.xml
Normal file
9
android/vcmi-app/src/main/res/drawable/ic_star_half.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
@ -0,0 +1,6 @@
|
|||||||
|
<?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>
|
@ -0,0 +1,7 @@
|
|||||||
|
<?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>
|
19
android/vcmi-app/src/main/res/layout-v21/inc_toolbar.xml
Normal file
19
android/vcmi-app/src/main/res/layout-v21/inc_toolbar.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?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>
|
76
android/vcmi-app/src/main/res/layout/activity_about.xml
Normal file
76
android/vcmi-app/src/main/res/layout/activity_about.xml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?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_libs"
|
||||||
|
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
|
||||||
|
android:text="@string/about_btn_libs" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/about_btn_privacy"
|
||||||
|
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
|
||||||
|
android:text="@string/about_btn_privacy" />
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
22
android/vcmi-app/src/main/res/layout/activity_error.xml
Normal file
22
android/vcmi-app/src/main/res/layout/activity_error.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?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>
|
19
android/vcmi-app/src/main/res/layout/activity_game.xml
Normal file
19
android/vcmi-app/src/main/res/layout/activity_game.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/game_outer_frame"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/game_progress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</FrameLayout>
|
106
android/vcmi-app/src/main/res/layout/activity_launcher.xml
Normal file
106
android/vcmi-app/src/main/res/layout/activity_launcher.xml
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<?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_res"
|
||||||
|
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>
|
29
android/vcmi-app/src/main/res/layout/activity_mods.xml
Normal file
29
android/vcmi-app/src/main/res/layout/activity_mods.xml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?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>
|
@ -0,0 +1,18 @@
|
|||||||
|
<?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>
|
34
android/vcmi-app/src/main/res/layout/dialog_authors.xml
Normal file
34
android/vcmi-app/src/main/res/layout/dialog_authors.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?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>
|
33
android/vcmi-app/src/main/res/layout/inc_launcher_btn.xml
Normal file
33
android/vcmi-app/src/main/res/layout/inc_launcher_btn.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?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>
|
27
android/vcmi-app/src/main/res/layout/inc_launcher_slider.xml
Normal file
27
android/vcmi-app/src/main/res/layout/inc_launcher_slider.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?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>
|
7
android/vcmi-app/src/main/res/layout/inc_separator.xml
Normal file
7
android/vcmi-app/src/main/res/layout/inc_separator.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?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>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user