mirror of
https://github.com/vcmi/vcmi.git
synced 2024-12-24 22:14:36 +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…
Reference in New Issue
Block a user