diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..ee4ba493 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +.gradle/ +build/ +.externalNativeBuild +.cxx/ +.idea/ +local.properties +.project +.classpath +.gradle +.settings +release/ +*.hprof +.vscode/ +*.bak \ No newline at end of file diff --git a/android/.gitmodules b/android/.gitmodules new file mode 100644 index 00000000..d3ba1cf3 --- /dev/null +++ b/android/.gitmodules @@ -0,0 +1,11 @@ +[submodule "hlsdk-portable"] + path = app/src/main/cpp/hlsdk-portable + url = https://github.com/FWGS/hlsdk-portable + branch = mobile_hacks +[submodule "xash3d-fwgs"] + path = app/src/main/cpp/xash3d-fwgs + url = https://github.com/FWGS/xash3d-fwgs +[submodule "SDL"] + path = app/src/main/cpp/SDL + url = https://github.com/libsdl-org/SDL + branch = release-2.24.1 diff --git a/android/Gemfile b/android/Gemfile new file mode 100644 index 00000000..adc90d98 --- /dev/null +++ b/android/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" \ No newline at end of file diff --git a/android/Gemfile.lock b/android/Gemfile.lock new file mode 100644 index 00000000..c0d4cedc --- /dev/null +++ b/android/Gemfile.lock @@ -0,0 +1,221 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.5) + rexml + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.2.0) + aws-partitions (1.666.0) + aws-sdk-core (3.168.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.59.0) + aws-sdk-core (~> 3, >= 3.165.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.117.1) + aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.5.2) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.4) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.94.0) + faraday (1.10.2) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.2.6) + fastlane (2.214.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.31.0) + google-apis-core (>= 0.9.1, < 2.a) + google-apis-core (0.9.1) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.16.0) + google-apis-core (>= 0.9.1, < 2.a) + google-apis-playcustomapp_v1 (0.12.0) + google-apis-core (>= 0.9.1, < 2.a) + google-apis-storage_v1 (0.19.0) + google-apis-core (>= 0.9.0, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.0) + google-cloud-storage (1.44.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.19.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.3.0) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.6.2) + jwt (2.5.0) + memoist (0.16.2) + mini_magick (4.11.0) + mini_mime (1.1.2) + multi_json (1.15.0) + multipart-post (2.0.0) + nanaimo (0.3.0) + naturally (2.2.1) + optparse (0.1.1) + os (1.1.4) + plist (3.6.0) + public_suffix (5.0.0) + rake (13.0.6) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.3.2) + strscan + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + signet (0.17.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + strscan (3.1.0) + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.8.2) + unf_ext (0.0.8.2-x64-mingw-ucrt) + unicode-display_width (1.8.0) + webrick (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.19.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + x64-mingw-ucrt + x86_64-linux + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.3.26 diff --git a/android/README.md b/android/README.md new file mode 100644 index 00000000..d91be5ef --- /dev/null +++ b/android/README.md @@ -0,0 +1,9 @@ +# Xash3D Android + +![translation progress](https://l10n.mentality.rip/widgets/xash3d-fwgs-android/-/svg-badge.svg) + +This repo is intended only to store Android launcher and wrapper code for +[Xash3D FWGS](https://github.com/FWGS/xash3d-fwgs). +Issues must be sent to Xash3D FWGS repository. + +Translations to Android launcher are done through [Weblate](https://l10n.mentality.rip). diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 00000000..bcfc148f --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,130 @@ +import java.time.LocalDateTime +import java.time.Month +import java.time.temporal.ChronoUnit + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "su.xash.engine" + ndkVersion = "26.1.10909125" + + defaultConfig { + applicationId = "su.xash" + applicationIdSuffix = "engine" + versionName = "0.21" + versionCode = getBuildNum() + minSdk = 21 + targetSdk = 34 + compileSdk = 34 + + externalNativeBuild { + cmake { + abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64") + arguments("-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF") + } + } + } + + externalNativeBuild { + cmake { + version = "3.22.1" + path = file("${project.projectDir}/src/main/cpp/CMakeLists.txt") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + buildTypes { + debug { + isMinifyEnabled = false + isShrinkResources = false + isDebuggable = true + applicationIdSuffix = ".test" + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) + } + + release { + isMinifyEnabled = false + isShrinkResources = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) + } + + register("asan") { + initWith(getByName("debug")) + } + + register("continuous") { + initWith(getByName("release")) + applicationIdSuffix = ".test" + } + } + + sourceSets { + getByName("main") { + assets.srcDir("${project.projectDir}/src/main/cpp/xash3d-fwgs/3rdparty/extras/xash-extras") + assets.srcDir("${project.projectDir}/../moddb") + java.srcDir("${project.projectDir}/src/main/cpp/SDL/android-project/app/src/main/java") + } + } + + lint { + abortOnError = false + } + + buildFeatures { + viewBinding = true + buildConfig = true + } + + androidResources { + noCompress += "" + } + + packaging { + jniLibs { + useLegacyPackaging = true + } + } +} + +dependencies { + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") + implementation("androidx.navigation:navigation-ui-ktx:2.7.7") + implementation("androidx.cardview:cardview:1.0.0") + implementation("androidx.annotation:annotation:1.7.1") + implementation("androidx.fragment:fragment-ktx:1.6.2") + implementation("androidx.preference:preference-ktx:1.2.1") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + implementation("androidx.work:work-runtime-ktx:2.9.0") +// implementation "androidx.legacy:legacy-support-v4:1.0.0" + + implementation("com.madgag.spongycastle:prov:1.58.0.0") + implementation("in.dragonbra:javasteam:1.2.0") + + implementation("ch.acra:acra-http:5.11.2") +} + +fun getBuildNum(): Int { + val now = LocalDateTime.now() + val releaseDate = LocalDateTime.of(2015, Month.APRIL, 1, 0, 0, 0) + val qBuildNum = releaseDate.until(now, ChronoUnit.DAYS) + val minuteOfDay = now.hour * 60 + now.minute + return (qBuildNum * 10000 + minuteOfDay).toInt() +} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/android/app/src/asan/jniLibs/arm64-v8a/libclang_rt.asan-aarch64-android.so b/android/app/src/asan/jniLibs/arm64-v8a/libclang_rt.asan-aarch64-android.so new file mode 100644 index 00000000..3b5dea16 Binary files /dev/null and b/android/app/src/asan/jniLibs/arm64-v8a/libclang_rt.asan-aarch64-android.so differ diff --git a/android/app/src/asan/jniLibs/armeabi-v7a/libclang_rt.asan-arm-android.so b/android/app/src/asan/jniLibs/armeabi-v7a/libclang_rt.asan-arm-android.so new file mode 100644 index 00000000..45b90d7f Binary files /dev/null and b/android/app/src/asan/jniLibs/armeabi-v7a/libclang_rt.asan-arm-android.so differ diff --git a/android/app/src/asan/jniLibs/x86/libclang_rt.asan-i686-android.so b/android/app/src/asan/jniLibs/x86/libclang_rt.asan-i686-android.so new file mode 100644 index 00000000..6a8357cc Binary files /dev/null and b/android/app/src/asan/jniLibs/x86/libclang_rt.asan-i686-android.so differ diff --git a/android/app/src/asan/jniLibs/x86_64/libclang_rt.asan-x86_64-android.so b/android/app/src/asan/jniLibs/x86_64/libclang_rt.asan-x86_64-android.so new file mode 100644 index 00000000..0400ec9d Binary files /dev/null and b/android/app/src/asan/jniLibs/x86_64/libclang_rt.asan-x86_64-android.so differ diff --git a/android/app/src/asan/res/values/strings.xml b/android/app/src/asan/res/values/strings.xml new file mode 100644 index 00000000..fa9fca0c --- /dev/null +++ b/android/app/src/asan/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Xash3D FWGS (Test) + su.xash.engine.test.documents + \ No newline at end of file diff --git a/android/app/src/asan/resources/lib/arm64-v8a/wrap.sh b/android/app/src/asan/resources/lib/arm64-v8a/wrap.sh new file mode 100644 index 00000000..99045e69 --- /dev/null +++ b/android/app/src/asan/resources/lib/arm64-v8a/wrap.sh @@ -0,0 +1,26 @@ + +#!/system/bin/sh +HERE=$(cd "$(dirname "$0")" && pwd) +cmd=$1 +shift +# This must be called *before* `LD_PRELOAD` is set. Otherwise, if this is a 32- +# bit app running on a 64-bit device, the 64-bit getprop will fail to load +# because it will preload a 32-bit ASan runtime. +# https://github.com/android/ndk/issues/1744 +os_version=$(getprop ro.build.version.sdk) +if [ "$os_version" -eq "27" ]; then + cmd="$cmd -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable $@" +elif [ "$os_version" -eq "28" ]; then + cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable $@" +else + cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y $@" +fi +export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1 +ASAN_LIB=$(ls "$HERE"/libclang_rt.asan-*-android.so) +if [ -f "$HERE/libc++_shared.so" ]; then + # Workaround for https://github.com/android-ndk/ndk/issues/988. + export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so" +else + export LD_PRELOAD="$ASAN_LIB" +fi +exec $cmd \ No newline at end of file diff --git a/android/app/src/asan/resources/lib/armeabi-v7a/wrap.sh b/android/app/src/asan/resources/lib/armeabi-v7a/wrap.sh new file mode 100644 index 00000000..99045e69 --- /dev/null +++ b/android/app/src/asan/resources/lib/armeabi-v7a/wrap.sh @@ -0,0 +1,26 @@ + +#!/system/bin/sh +HERE=$(cd "$(dirname "$0")" && pwd) +cmd=$1 +shift +# This must be called *before* `LD_PRELOAD` is set. Otherwise, if this is a 32- +# bit app running on a 64-bit device, the 64-bit getprop will fail to load +# because it will preload a 32-bit ASan runtime. +# https://github.com/android/ndk/issues/1744 +os_version=$(getprop ro.build.version.sdk) +if [ "$os_version" -eq "27" ]; then + cmd="$cmd -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable $@" +elif [ "$os_version" -eq "28" ]; then + cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable $@" +else + cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y $@" +fi +export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1 +ASAN_LIB=$(ls "$HERE"/libclang_rt.asan-*-android.so) +if [ -f "$HERE/libc++_shared.so" ]; then + # Workaround for https://github.com/android-ndk/ndk/issues/988. + export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so" +else + export LD_PRELOAD="$ASAN_LIB" +fi +exec $cmd \ No newline at end of file diff --git a/android/app/src/asan/resources/lib/x86/wrap.sh b/android/app/src/asan/resources/lib/x86/wrap.sh new file mode 100644 index 00000000..99045e69 --- /dev/null +++ b/android/app/src/asan/resources/lib/x86/wrap.sh @@ -0,0 +1,26 @@ + +#!/system/bin/sh +HERE=$(cd "$(dirname "$0")" && pwd) +cmd=$1 +shift +# This must be called *before* `LD_PRELOAD` is set. Otherwise, if this is a 32- +# bit app running on a 64-bit device, the 64-bit getprop will fail to load +# because it will preload a 32-bit ASan runtime. +# https://github.com/android/ndk/issues/1744 +os_version=$(getprop ro.build.version.sdk) +if [ "$os_version" -eq "27" ]; then + cmd="$cmd -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable $@" +elif [ "$os_version" -eq "28" ]; then + cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable $@" +else + cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y $@" +fi +export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1 +ASAN_LIB=$(ls "$HERE"/libclang_rt.asan-*-android.so) +if [ -f "$HERE/libc++_shared.so" ]; then + # Workaround for https://github.com/android-ndk/ndk/issues/988. + export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so" +else + export LD_PRELOAD="$ASAN_LIB" +fi +exec $cmd \ No newline at end of file diff --git a/android/app/src/asan/resources/lib/x86_64/wrap.sh b/android/app/src/asan/resources/lib/x86_64/wrap.sh new file mode 100644 index 00000000..99045e69 --- /dev/null +++ b/android/app/src/asan/resources/lib/x86_64/wrap.sh @@ -0,0 +1,26 @@ + +#!/system/bin/sh +HERE=$(cd "$(dirname "$0")" && pwd) +cmd=$1 +shift +# This must be called *before* `LD_PRELOAD` is set. Otherwise, if this is a 32- +# bit app running on a 64-bit device, the 64-bit getprop will fail to load +# because it will preload a 32-bit ASan runtime. +# https://github.com/android/ndk/issues/1744 +os_version=$(getprop ro.build.version.sdk) +if [ "$os_version" -eq "27" ]; then + cmd="$cmd -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable $@" +elif [ "$os_version" -eq "28" ]; then + cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable $@" +else + cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y $@" +fi +export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1 +ASAN_LIB=$(ls "$HERE"/libclang_rt.asan-*-android.so) +if [ -f "$HERE/libc++_shared.so" ]; then + # Workaround for https://github.com/android-ndk/ndk/issues/988. + export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so" +else + export LD_PRELOAD="$ASAN_LIB" +fi +exec $cmd \ No newline at end of file diff --git a/android/app/src/continuous/res/values/strings.xml b/android/app/src/continuous/res/values/strings.xml new file mode 100644 index 00000000..fa9fca0c --- /dev/null +++ b/android/app/src/continuous/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Xash3D FWGS (Test) + su.xash.engine.test.documents + \ No newline at end of file diff --git a/android/app/src/debug/res/values/strings.xml b/android/app/src/debug/res/values/strings.xml new file mode 100644 index 00000000..fa9fca0c --- /dev/null +++ b/android/app/src/debug/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Xash3D FWGS (Test) + su.xash.engine.test.documents + \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..731303cd --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..6274cf13 --- /dev/null +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.6) + +project(XASH_ANDROID) + +# armeabi-v7a requires cpufeatures library +include(AndroidNdkModules) +android_ndk_import_module_cpufeatures() + +find_package(PythonInterp 2.7 REQUIRED) + +get_filename_component(C_COMPILER_ID ${CMAKE_C_COMPILER} NAME_WE) +get_filename_component(CXX_COMPILER_ID ${CMAKE_CXX_COMPILER} NAME_WE) + +if(${CMAKE_BUILD_TYPE} STREQUAL "Debug") + set(BUILD_TYPE "debug") +else() + set(BUILD_TYPE "release") +endif() + +set(CMAKE_VERBOSE_MAKEFILE ON) + +set(WAF_CC "${CMAKE_C_COMPILER} --target=${CMAKE_C_COMPILER_TARGET}") +set(WAF_CXX "${CMAKE_CXX_COMPILER} --target=${CMAKE_CXX_COMPILER_TARGET}") + +execute_process( + COMMAND ${CMAKE_COMMAND} -E env + CC=${WAF_CC} CXX=${WAF_CXX} + AR=${CMAKE_AR} STRIP=${CMAKE_STRIP} + ${PYTHON_EXECUTABLE} waf configure -vvv -T ${BUILD_TYPE} cmake + --check-c-compiler=${C_COMPILER_ID} --check-cxx-compiler=${CXX_COMPILER_ID} + -s "${CMAKE_CURRENT_SOURCE_DIR}/SDL" --skip-sdl2-sanity-check + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/xash3d-fwgs" +) + +if(CMAKE_SIZEOF_VOID_P MATCHES "8") + set(64BIT ON CACHE BOOL "" FORCE) +endif() +add_subdirectory("hlsdk-portable") +add_subdirectory("SDL") +add_subdirectory("xash3d-fwgs") +add_subdirectory("xash3d-fwgs/3rdparty/mainui") \ No newline at end of file diff --git a/android/app/src/main/cpp/SDL b/android/app/src/main/cpp/SDL new file mode 160000 index 00000000..2eef7ca4 --- /dev/null +++ b/android/app/src/main/cpp/SDL @@ -0,0 +1 @@ +Subproject commit 2eef7ca475decd2b864214cdbfe72b143b16d459 diff --git a/android/app/src/main/cpp/hlsdk-portable b/android/app/src/main/cpp/hlsdk-portable new file mode 160000 index 00000000..0d8a19fd --- /dev/null +++ b/android/app/src/main/cpp/hlsdk-portable @@ -0,0 +1 @@ +Subproject commit 0d8a19fd82758746cc41af5e18946a9410e4533f diff --git a/android/app/src/main/cpp/xash3d-fwgs b/android/app/src/main/cpp/xash3d-fwgs new file mode 160000 index 00000000..1c84a5c8 --- /dev/null +++ b/android/app/src/main/cpp/xash3d-fwgs @@ -0,0 +1 @@ +Subproject commit 1c84a5c8ade7cfac17d6b558669d4f721a0bf11f diff --git a/android/app/src/main/java/su/xash/engine/AndroidBug5497Workaround.java b/android/app/src/main/java/su/xash/engine/AndroidBug5497Workaround.java new file mode 100644 index 00000000..951d63cf --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/AndroidBug5497Workaround.java @@ -0,0 +1,49 @@ +package su.xash.engine; + +import android.app.Activity; +import android.graphics.Rect; +import android.view.View; +import android.widget.FrameLayout; + +public class AndroidBug5497Workaround { + // For more information, see https://code.google.com/p/android/issues/detail?id=5497 + // To use this class, simply invoke assistActivity() on an Activity that already has its content view set. + + public static void assistActivity(Activity activity) { + new AndroidBug5497Workaround(activity); + } + + private View mChildOfContent; + private int usableHeightPrevious; + private FrameLayout.LayoutParams frameLayoutParams; + + private AndroidBug5497Workaround(Activity activity) { + FrameLayout content = activity.findViewById(android.R.id.content); + mChildOfContent = content.getChildAt(0); + mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent); + frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams(); + } + + private void possiblyResizeChildOfContent() { + int usableHeightNow = computeUsableHeight(); + if (usableHeightNow != usableHeightPrevious) { + int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight(); + int heightDifference = usableHeightSansKeyboard - usableHeightNow; + if (heightDifference > (usableHeightSansKeyboard / 4)) { + // keyboard probably just became visible + frameLayoutParams.height = usableHeightSansKeyboard - heightDifference; + } else { + // keyboard probably just became hidden + frameLayoutParams.height = usableHeightSansKeyboard; + } + mChildOfContent.requestLayout(); + usableHeightPrevious = usableHeightNow; + } + } + + private int computeUsableHeight() { + Rect r = new Rect(); + mChildOfContent.getWindowVisibleDisplayFrame(r); + return (r.bottom - r.top); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/DedicatedActivity.kt b/android/app/src/main/java/su/xash/engine/DedicatedActivity.kt new file mode 100644 index 00000000..81d1cd89 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/DedicatedActivity.kt @@ -0,0 +1,4 @@ +package su.xash.engine + +class DedicatedActivity { +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/DedicatedService.kt b/android/app/src/main/java/su/xash/engine/DedicatedService.kt new file mode 100644 index 00000000..4462af43 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/DedicatedService.kt @@ -0,0 +1,11 @@ +package su.xash.engine + +import android.app.Service +import android.content.Intent +import android.os.IBinder + +class DedicatedService: Service() { + override fun onBind(intent: Intent?): IBinder? { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/MainActivity.kt b/android/app/src/main/java/su/xash/engine/MainActivity.kt new file mode 100644 index 00000000..4e07df52 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/MainActivity.kt @@ -0,0 +1,38 @@ +package su.xash.engine + +import android.os.Bundle +import android.os.PersistableBundle +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.NavController +import androidx.navigation.findNavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.setupActionBarWithNavController +import androidx.navigation.ui.setupWithNavController +import su.xash.engine.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var navController: NavController + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment + navController = navHostFragment.navController + appBarConfiguration = AppBarConfiguration(navController.graph) + setupActionBarWithNavController(navController, appBarConfiguration) + } + + override fun onSupportNavigateUp(): Boolean { + return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/MainApplication.kt b/android/app/src/main/java/su/xash/engine/MainApplication.kt new file mode 100644 index 00000000..b17c5088 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/MainApplication.kt @@ -0,0 +1,28 @@ +package su.xash.engine + +import android.app.Application +import android.content.Context +import android.os.StrictMode +import org.acra.config.httpSender +import org.acra.data.StringFormat +import org.acra.ktx.initAcra + +class MainApplication : Application() { + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + + if (!BuildConfig.DEBUG) { + initAcra { + buildConfigClass = BuildConfig::class.java + reportFormat = StringFormat.JSON + + httpSender { + uri = "http://bodis.pp.ua:5000/report" + } + } + } else { + // enable strict mode to detect memory leaks etc. + StrictMode.enableDefaults(); + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/XashActivity.java b/android/app/src/main/java/su/xash/engine/XashActivity.java new file mode 100644 index 00000000..89d628a7 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/XashActivity.java @@ -0,0 +1,138 @@ +package su.xash.engine; + +import android.annotation.SuppressLint; +import android.content.pm.ActivityInfo; +import android.content.res.AssetManager; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings.Secure; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.WindowManager; + +import org.libsdl.app.SDLActivity; + +public class XashActivity extends SDLActivity { + private boolean mUseVolumeKeys; + private String mPackageName; + private static final String TAG = "XashActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + //getWindow().addFlags(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES); + getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } + + AndroidBug5497Workaround.assistActivity(this); + } + + @Override + protected String[] getLibraries() { + return new String[]{"SDL2", "xash"}; + } + + @SuppressLint("HardwareIds") + private String getAndroidID() { + return Secure.getString(getContentResolver(), Secure.ANDROID_ID); + } + + @SuppressLint("ApplySharedPref") + private void saveAndroidID(String id) { + getSharedPreferences("xash_preferences", MODE_PRIVATE).edit().putString("xash_id", id).commit(); + } + + private String loadAndroidID() { + return getSharedPreferences("xash_preferences", MODE_PRIVATE).getString("xash_id", ""); + } + + @Override + public String getCallingPackage() { + if (mPackageName != null) { + return mPackageName; + } + + return super.getCallingPackage(); + } + + private AssetManager getAssets(boolean isEngine) { + AssetManager am = null; + + if (isEngine) { + am = getAssets(); + } else { + try { + am = getPackageManager().getResourcesForApplication(getCallingPackage()).getAssets(); + } catch (Exception e) { + Log.e(TAG, "Unable to load mod assets!"); + e.printStackTrace(); + } + } + + return am; + } + + private String[] getAssetsList(boolean isEngine, String path) { + AssetManager am = getAssets(isEngine); + + try { + return am.list(path); + } catch (Exception e) { + e.printStackTrace(); + } + + return new String[]{}; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (SDLActivity.mBrokenLibraries) { + return false; + } + + int keyCode = event.getKeyCode(); + if (!mUseVolumeKeys) { + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || + keyCode == KeyEvent.KEYCODE_VOLUME_UP || + keyCode == KeyEvent.KEYCODE_CAMERA || + keyCode == KeyEvent.KEYCODE_ZOOM_IN || + keyCode == KeyEvent.KEYCODE_ZOOM_OUT) { + return false; + } + } + + return getWindow().superDispatchKeyEvent(event); + } + + // TODO: REMOVE LATER, temporary launchers support? + @Override + protected String[] getArguments() { + String gamedir = getIntent().getStringExtra("gamedir"); + if (gamedir == null) gamedir = "valve"; + nativeSetenv("XASH3D_GAME", gamedir); + + String gamelibdir = getIntent().getStringExtra("gamelibdir"); + if (gamelibdir != null) nativeSetenv("XASH3D_GAMELIBDIR", gamelibdir); + + String pakfile = getIntent().getStringExtra("pakfile"); + if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile); + + mUseVolumeKeys = getIntent().getBooleanExtra("usevolume", false); + mPackageName = getIntent().getStringExtra("package"); + + String[] env = getIntent().getStringArrayExtra("env"); + if (env != null) { + for (int i = 0; i < env.length; i += 2) + nativeSetenv(env[i], env[i + 1]); + } + + String argv = getIntent().getStringExtra("argv"); + if (argv == null) argv = "-dev 2 -log"; + return argv.split(" "); + } +} diff --git a/android/app/src/main/java/su/xash/engine/XashDocumentsProvider.java b/android/app/src/main/java/su/xash/engine/XashDocumentsProvider.java new file mode 100644 index 00000000..f0319285 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/XashDocumentsProvider.java @@ -0,0 +1,254 @@ +package su.xash.engine; + +import android.annotation.TargetApi; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.graphics.Point; +import android.os.Build; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract.Document; +import android.provider.DocumentsContract.Root; +import android.provider.DocumentsProvider; +import android.webkit.MimeTypeMap; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +@TargetApi(Build.VERSION_CODES.KITKAT) +public class XashDocumentsProvider extends DocumentsProvider { + private static final String ALL_MIME_TYPES = "*/*"; + private File mRootDir; + + private static final String TAG = "XashDocumentsProvider"; + + @Override + public boolean onCreate() { + mRootDir = getContext().getExternalFilesDir(null); + + return true; + } + + private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{Root.COLUMN_ROOT_ID, + Root.COLUMN_MIME_TYPES, + Root.COLUMN_FLAGS, + Root.COLUMN_ICON, + Root.COLUMN_TITLE, + Root.COLUMN_SUMMARY, + Root.COLUMN_DOCUMENT_ID, + Root.COLUMN_AVAILABLE_BYTES}; + + private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_FLAGS, + Document.COLUMN_SIZE}; + + @Override + public Cursor queryRoots(String[] projection) { + final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION); + + final String appName = getContext().getString(R.string.app_name); + final String docId = getDocIdForFile(mRootDir); + + int flags; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH; + } else { + flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD; + } + + final MatrixCursor.RowBuilder row = result.newRow(); + row.add(Root.COLUMN_ROOT_ID, docId); + row.add(Root.COLUMN_DOCUMENT_ID, docId); + row.add(Root.COLUMN_SUMMARY, null); + row.add(Root.COLUMN_FLAGS, flags); + row.add(Root.COLUMN_TITLE, appName); + row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES); + row.add(Root.COLUMN_AVAILABLE_BYTES, mRootDir.getFreeSpace()); + row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher); + + return result; + } + + @Override + public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); + + includeFile(result, documentId, null); + + return result; + } + + @Override + public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); + + final File parent = getFileForDocId(parentDocumentId); + final File[] filesList = parent.listFiles(); + + if (filesList != null) { + for (File file : filesList) { + includeFile(result, null, file); + } + } + + return result; + } + + @Override + public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException { + final File file = getFileForDocId(documentId); + + final int accessMode = ParcelFileDescriptor.parseMode(mode); + + return ParcelFileDescriptor.open(file, accessMode); + } + + @Override + public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { + final File file = getFileForDocId(documentId); + + final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + + return new AssetFileDescriptor(pfd, 0, file.length()); + } + + @Override + public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { + File newFile = new File(parentDocumentId, displayName); + + int noConflictId = 1; + + while (newFile.exists()) { + newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")"); + } + + try { + boolean succeeded; + + if (Document.MIME_TYPE_DIR.equals(mimeType)) { + succeeded = newFile.mkdir(); + } else { + succeeded = newFile.createNewFile(); + } + + if (!succeeded) { + throw new FileNotFoundException("Failed to create document with id " + newFile.getPath()); + } + } catch (IOException e) { + throw new FileNotFoundException("Failed to create document with id " + newFile.getPath()); + } + + return newFile.getPath(); + } + + @Override + public void deleteDocument(String documentId) throws FileNotFoundException { + File file = getFileForDocId(documentId); + + if (file.isDirectory()) { + if (!deleteDirectory(file)) { + throw new FileNotFoundException("Failed to delete document with id " + documentId); + } + } else if (!file.delete()) { + throw new FileNotFoundException("Failed to delete document with id " + documentId); + } + } + + @Override + public String getDocumentType(String documentId) throws FileNotFoundException { + File file = getFileForDocId(documentId); + + return getMimeType(file); + } + + @Override + public boolean isChildDocument(String parentDocumentId, String documentId) { + return documentId.startsWith(parentDocumentId); + } + + private static File getFileForDocId(String docId) throws FileNotFoundException { + final File f = new File(docId); + + if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found"); + + return f; + } + + private static String getDocIdForFile(File file) { + return file.getAbsolutePath(); + } + + private static String getMimeType(File file) { + if (file.isDirectory()) { + return Document.MIME_TYPE_DIR; + } else { + final String name = file.getName(); + final int lastDot = name.lastIndexOf('.'); + + if (lastDot >= 0) { + final String extension = name.substring(lastDot + 1).toLowerCase(); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + + if (mime != null) return mime; + } + + return "application/octet-stream"; + } + } + + private static boolean deleteDirectory(File dir) { + final File[] allContents = dir.listFiles(); + + if (allContents != null) { + for (File file : allContents) { + deleteDirectory(file); + } + } + return dir.delete(); + } + + private void includeFile(MatrixCursor result, String docId, File file) throws FileNotFoundException { + if (docId == null) { + docId = getDocIdForFile(file); + } else { + file = getFileForDocId(docId); + } + + int flags = 0; + + if (file.isDirectory()) { + if (file.canWrite()) { + flags |= Document.FLAG_DIR_SUPPORTS_CREATE; + } + } else if (file.canWrite()) { + flags |= Document.FLAG_SUPPORTS_WRITE; + } + + File parentFile = file.getParentFile(); + if (parentFile != null && parentFile.canWrite()) { + flags |= Document.FLAG_SUPPORTS_DELETE; + } + + final String displayName = file.getName(); + final String mimeType = getMimeType(file); + + if (mimeType.startsWith("image/")) { + flags |= Document.FLAG_SUPPORTS_THUMBNAIL; + } + + final MatrixCursor.RowBuilder row = result.newRow(); + row.add(Document.COLUMN_DOCUMENT_ID, docId); + row.add(Document.COLUMN_DISPLAY_NAME, displayName); + row.add(Document.COLUMN_SIZE, file.length()); + row.add(Document.COLUMN_MIME_TYPE, mimeType); + row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified()); + row.add(Document.COLUMN_FLAGS, flags); + row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher); + } +} diff --git a/android/app/src/main/java/su/xash/engine/adapters/GameAdapter.kt b/android/app/src/main/java/su/xash/engine/adapters/GameAdapter.kt new file mode 100644 index 00000000..dacb5a43 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/adapters/GameAdapter.kt @@ -0,0 +1,77 @@ +package su.xash.engine.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.findNavController +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import su.xash.engine.R +import su.xash.engine.databinding.CardGameBinding +import su.xash.engine.model.Game +import su.xash.engine.ui.library.LibraryViewModel + + +class GameAdapter(private val libraryViewModel: LibraryViewModel) : + ListAdapter(DiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameAdapter.GameViewHolder { + val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return GameViewHolder(binding) + } + + override fun onBindViewHolder(holder: GameAdapter.GameViewHolder, position: Int) { + return holder.bind(getItem(position)) + } + + private class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { + return oldItem.basedir.name == newItem.basedir.name + } + + override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { + return oldItem.basedir.name == newItem.basedir.name && oldItem.installed == newItem.installed + } + + } + + inner class GameViewHolder(val binding: CardGameBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(game: Game) { + binding.apply { + gameTitle.text = game.title + + if (game.icon != null) { + gameIcon.setImageBitmap(game.icon) + } else { + gameIcon.visibility = View.GONE + } + + if (game.cover != null) { + gameCover.setImageBitmap(game.cover) + } else { + gameCover.visibility = View.GONE + } + + if (!game.installed) { + launchButton.visibility = View.GONE + settingsButton.visibility = View.GONE + progressIndicator.visibility = View.VISIBLE + return + } + + settingsButton.setOnClickListener { + libraryViewModel.setSelectedGame(game) + it.findNavController() + .navigate(R.id.action_libraryFragment_to_gameSettingsFragment) + } + + root.setOnClickListener { libraryViewModel.startEngine(it.context, game) } + launchButton.setOnClickListener { + libraryViewModel.startEngine(it.context, game) + } + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/model/BackgroundBitmap.kt b/android/app/src/main/java/su/xash/engine/model/BackgroundBitmap.kt new file mode 100644 index 00000000..d7443070 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/model/BackgroundBitmap.kt @@ -0,0 +1,89 @@ +package su.xash.engine.model + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import androidx.documentfile.provider.DocumentFile +import su.xash.engine.util.TGAReader +import java.util.Scanner + + +object BackgroundBitmap { + private const val BACKGROUND_ROWS = 3 + private const val BACKGROUND_COLUMNS = 4 + private const val BACKGROUND_WIDTH = 800 + private const val BACKGROUND_HEIGHT = 600 + + fun createBackground(ctx: Context, file: DocumentFile): Bitmap { + var bitmap = + Bitmap.createBitmap(BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888) + var canvas = Canvas(bitmap) + var x: Int + var y = 0 + var width: Int + var height = 0 + + var bgLayout = file.findFile("resource")?.findFile("HD_BackgroundLayout.txt") + if (bgLayout == null) { + bgLayout = file.findFile("resource")?.findFile("BackgroundLayout.txt") + } + + if (bgLayout == null) { + val dir = file.findFile("resource")?.findFile("background") + for (i in 0 until BACKGROUND_ROWS) { + x = 0 + for (j in 0 until BACKGROUND_COLUMNS) { + val filename = "${BACKGROUND_WIDTH}_${i + 1}_${'a' + j}_loading.tga" + val bmpFile = dir?.findFile(filename) + val bmpImage = loadTga(ctx, bmpFile!!) + + canvas.drawBitmap(bmpImage, x.toFloat(), y.toFloat(), null) + x += bmpImage.width + height = bmpImage.height + + } + y += height + } + return bitmap + } + + ctx.contentResolver.openInputStream(bgLayout.uri).use { inputStream -> + Scanner(inputStream).use { scanner -> + while (scanner.hasNext()) { + when (val str = scanner.next()) { + "resolution" -> { + width = scanner.nextInt() + height = scanner.nextInt() + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + canvas = Canvas(bitmap) + } + + else -> { + var bmpFile = file + str.split("/").forEach { bmpFile = bmpFile.findFile(it)!! } + //skip + scanner.next() + x = scanner.nextInt() + y = scanner.nextInt() + val bmp = loadTga(ctx, bmpFile) + canvas.drawBitmap(bmp, x.toFloat(), y.toFloat(), null) + } + } + } + } + } + return bitmap + } + + private fun loadTga(ctx: Context, file: DocumentFile): Bitmap { + ctx.contentResolver.openInputStream(file.uri).use { + val buffer = it?.readBytes() + val pixels = TGAReader.read(buffer, TGAReader.ARGB) + + val width = TGAReader.getWidth(buffer) + val height = TGAReader.getHeight(buffer) + + return Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.ARGB_8888) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/model/Game.kt b/android/app/src/main/java/su/xash/engine/model/Game.kt new file mode 100644 index 00000000..89ac48d0 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/model/Game.kt @@ -0,0 +1,129 @@ +package su.xash.engine.model + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.net.Uri +import android.provider.MediaStore +import androidx.documentfile.provider.DocumentFile +import su.xash.engine.XashActivity + + +class Game(val ctx: Context, val basedir: DocumentFile, var installed: Boolean = true) { + private var iconName = "game.ico" + var title = "Unknown Game" + var icon: Bitmap? = null + var cover: Bitmap? = null + + private val pref = ctx.getSharedPreferences(basedir.name, Context.MODE_PRIVATE) + + init { + basedir.findFile("gameinfo.txt")?.let { + parseGameInfo(it) + } ?: basedir.findFile("liblist.gam")?.let { parseGameInfo(it) } + + basedir.findFile(iconName) + ?.let { icon = MediaStore.Images.Media.getBitmap(ctx.contentResolver, it.uri) } + + try { + cover = BackgroundBitmap.createBackground(ctx, basedir) + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun startEngine(ctx: Context) { + ctx.startActivity(Intent(ctx, XashActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra("gamedir", basedir.name) + putExtra("argv", pref.getString("arguments", "-dev 2 -log")) + putExtra("usevolume", pref.getBoolean("use_volume_buttons", false)) + //.putExtra("gamelibdir", getGameLibDir(context)) + //.putExtra("package", getPackageName()) } + }) + } + + private fun parseGameInfo(file: DocumentFile) { + ctx.contentResolver.openInputStream(file.uri).use { inputStream -> + inputStream?.bufferedReader().use { reader -> + reader?.forEachLine { + val tokens = it.split("\\s+".toRegex(), limit = 2) + if (tokens.size >= 2) { + val k = tokens[0] + val v = tokens[1].trim('"') + + if (k == "title" || k == "game") title = v + if (k == "icon") iconName = v + } + } + } + } + } + + private fun getPackageName(): String? { +// return if (mDbEntry != null) { +// mDbEntry.getPackageName() +// } else null + return null + } + + private fun getGameLibDir(ctx: Context): String? { + val pkgName = getPackageName() + if (pkgName != null) { + val pkgInfo: PackageInfo = try { + ctx.packageManager.getPackageInfo(pkgName, 0) + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + ctx.startActivity( + Intent( + Intent.ACTION_VIEW, Uri.parse("market://details?id=$pkgName") + ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + ) + return null + } + return pkgInfo.applicationInfo.nativeLibraryDir + } + return ctx.applicationInfo.nativeLibraryDir + } + + companion object { + fun getGames(ctx: Context, file: DocumentFile): List { + val games = mutableListOf() + + if (checkIfGamedir(file)) { + games.add(Game(ctx, file)) + } else { + file.listFiles().forEach { + if (it.isDirectory) { + if (checkIfGamedir(it)) { + games.add(Game(ctx, it)) + } + } + } + } + + return games + } + + fun checkIfGamedir(file: DocumentFile): Boolean { + file.findFile("liblist.gam")?.let { return true } + file.findFile("gameinfo.txt")?.let { return true } + return false + } + } +} + +// Intent intent = new Intent("su.xash.engine.MOD"); +// for (ResolveInfo info : context.getPackageManager() +// .queryIntentActivities(intent, PackageManager.GET_META_DATA)) { +// String packageName = info.activityInfo.applicationInfo.packageName; +// String gameDir = info.activityInfo.applicationInfo.metaData.getString( +// "su.xash.engine.gamedir"); +// Log.d(TAG, "package = " + packageName + " gamedir = " + gameDir); +// } + +//public void startEngine(Context context) { +// context.startActivity(new Intent(context, XashActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtra("gamedir", getGameDir()).putExtra("argv", getArguments()).putExtra("usevolume", getVolumeState()).putExtra("gamelibdir", getGameLibDir(context)).putExtra("package", getPackageName())); +//} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/model/ModDatabase.kt b/android/app/src/main/java/su/xash/engine/model/ModDatabase.kt new file mode 100644 index 00000000..a7720598 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/model/ModDatabase.kt @@ -0,0 +1,59 @@ +package su.xash.engine.model + +import org.json.JSONArray +import org.json.JSONObject +import java.io.InputStream + +class ModDatabase(inputStream: InputStream) { + val entries = mutableListOf() + + companion object { + const val VERSION = 1 + + fun getFilename(): String { + return "v${VERSION}.json" + } + } + + init { + inputStream.bufferedReader().use { + val jsonArray = JSONArray(it.readText()) + + for (i in 0.. { + startActivity( + Intent(Intent.ACTION_VIEW).setDataAndType( + null, "vnd.android.document/directory" + ) + ) + } + + R.id.action_install -> { + findNavController().navigate(R.id.action_libraryFragment_to_setupFragment) + } + + R.id.action_settings -> { + findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment) + } + } + + return false + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/ui/library/LibraryViewModel.kt b/android/app/src/main/java/su/xash/engine/ui/library/LibraryViewModel.kt new file mode 100644 index 00000000..b82c8321 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/ui/library/LibraryViewModel.kt @@ -0,0 +1,127 @@ +package su.xash.engine.ui.library + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope +import androidx.preference.PreferenceManager +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkInfo +import androidx.work.WorkManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import su.xash.engine.model.Game +import su.xash.engine.model.ModDatabase +import su.xash.engine.workers.FileCopyWorker +import su.xash.engine.workers.KEY_FILE_URI +import java.util.Locale + +const val TAG_INSTALL = "TAG_INSTALL" + +class LibraryViewModel(application: Application) : AndroidViewModel(application) { + val installedGames: LiveData> get() = _installedGames + private val _installedGames = MutableLiveData(emptyList()) + + val downloads: LiveData> get() = _downloads + private val _downloads = MutableLiveData(emptyList()) + + val isReloading: LiveData get() = _isReloading + private val _isReloading = MutableLiveData(false) + + private val workManager = WorkManager.getInstance(application.applicationContext) + val workInfos: LiveData> = workManager.getWorkInfosByTagLiveData(TAG_INSTALL) + + val selectedItem: LiveData get() = _selectedItem + private val _selectedItem = MutableLiveData() + + val appPreferences = application.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) + val modDb: ModDatabase + + init { + modDb = application.assets.open(ModDatabase.getFilename()).use { ModDatabase(it) } + reloadGames(application.applicationContext) + } + + fun reloadGames(ctx: Context) { + if (isReloading.value == true) { + return + } + _isReloading.value = true + + viewModelScope.launch { + withContext(Dispatchers.IO) { + val games = mutableListOf() + val root = DocumentFile.fromFile(ctx.getExternalFilesDir(null)!!) + + val installedGames = Game.getGames(ctx, root) + .filter { p -> _downloads.value?.any { p.basedir.name == it.basedir.name } == false } + + games.addAll(installedGames) + downloads.value?.let { games.addAll(it) } + + _installedGames.postValue(games) + _isReloading.postValue(false) + } + } + } + + fun refreshDownloads(ctx: Context) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val games = mutableListOf() + + workInfos.value?.filter { + it.state == WorkInfo.State.RUNNING && !it.progress.getString(FileCopyWorker.Input) + .isNullOrEmpty() + }?.forEach { + val uri = Uri.parse(it.progress.getString(FileCopyWorker.Input)) + val file = DocumentFile.fromTreeUri(ctx, uri) + games.addAll(Game.getGames(ctx, file!!)) + games.forEach { g -> g.installed = false } + } + + _downloads.postValue(games) + } + } + } + + fun installGame(uri: Uri) { + val data = Data.Builder().putString(KEY_FILE_URI, uri.toString()).build() + val request = OneTimeWorkRequestBuilder().run { + setInputData(data) + addTag(TAG_INSTALL) + build() + } + workManager.enqueue(request) + } + + fun setSelectedGame(game: Game) { + _selectedItem.value = game + } + + fun uninstallGame(game: Game) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + game.installed = false + game.basedir.delete() + _installedGames.postValue(_installedGames.value) + } + } + } + + fun startEngine(ctx: Context, game: Game) { + game.startEngine(ctx) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsFragment.kt b/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsFragment.kt new file mode 100644 index 00000000..1d4e5a91 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsFragment.kt @@ -0,0 +1,37 @@ +package su.xash.engine.ui.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.navigation.fragment.findNavController +import su.xash.engine.R +import su.xash.engine.adapters.GameAdapter +import su.xash.engine.databinding.FragmentAppSettingsBinding +import su.xash.engine.databinding.FragmentGameSettingsBinding +import su.xash.engine.databinding.FragmentLibraryBinding + +class AppSettingsFragment : Fragment() { + private var _binding: FragmentAppSettingsBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + _binding = FragmentAppSettingsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + childFragmentManager.beginTransaction() + .add(binding.settingsFragment.id, AppSettingsPreferenceFragment()).commit(); + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsPreferenceFragment.kt b/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsPreferenceFragment.kt new file mode 100644 index 00000000..1521a79c --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsPreferenceFragment.kt @@ -0,0 +1,16 @@ +package su.xash.engine.ui.settings + +import android.os.Bundle +import androidx.preference.ListPreference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import su.xash.engine.BuildConfig +import su.xash.engine.R +import su.xash.engine.model.Game + +class AppSettingsPreferenceFragment() : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceManager.sharedPreferencesName = "app_preferences"; + setPreferencesFromResource(R.xml.app_preferences, rootKey); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/ui/settings/GameSettingsFragment.kt b/android/app/src/main/java/su/xash/engine/ui/settings/GameSettingsFragment.kt new file mode 100644 index 00000000..f6f1cd9e --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/ui/settings/GameSettingsFragment.kt @@ -0,0 +1,75 @@ +package su.xash.engine.ui.settings + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.core.view.MenuProvider +import androidx.core.view.get +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import su.xash.engine.R +import su.xash.engine.databinding.FragmentGameSettingsBinding +import su.xash.engine.ui.library.LibraryViewModel + +class GameSettingsFragment : Fragment() { + private var _binding: FragmentGameSettingsBinding? = null + private val binding get() = _binding!! + + private val libraryViewModel: LibraryViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + _binding = FragmentGameSettingsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val game = libraryViewModel.selectedItem.value!! + + binding.gameCard.apply { + gameTitle.text = game.title + + if (game.icon != null) { + gameIcon.setImageBitmap(game.icon) + } else { + gameIcon.visibility = View.GONE + } + + if (game.cover != null) { + gameCover.setImageBitmap(game.cover) + } else { + gameCover.visibility = View.GONE + } + + buttonsContainer.visibility = View.GONE + } + + childFragmentManager.beginTransaction() + .add(binding.settingsFragment.id, GameSettingsPreferenceFragment(game)) + .commit(); + + binding.bottomNavigation.menu.findItem(R.id.action_uninstall).setOnMenuItemClickListener { + libraryViewModel.uninstallGame(game) + findNavController().popBackStack() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/ui/settings/GameSettingsPreferenceFragment.kt b/android/app/src/main/java/su/xash/engine/ui/settings/GameSettingsPreferenceFragment.kt new file mode 100644 index 00000000..7a77e1b8 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/ui/settings/GameSettingsPreferenceFragment.kt @@ -0,0 +1,41 @@ +package su.xash.engine.ui.settings + +import android.os.Bundle +import androidx.preference.ListPreference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import su.xash.engine.BuildConfig +import su.xash.engine.R +import su.xash.engine.model.Game + +class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceManager.sharedPreferencesName = game.basedir.name; + setPreferencesFromResource(R.xml.game_preferences, rootKey); + + val packageList = findPreference("package_name")!! + packageList.entries = arrayOf(getString(R.string.app_name)) + packageList.entryValues = arrayOf(requireContext().packageName) + + if (packageList.value == null) { + packageList.setValueIndex(0); + } + + val separatePackages = findPreference("separate_libraries")!! + val clientPackage = findPreference("client_package")!! + val serverPackage = findPreference("server_package")!! + separatePackages.setOnPreferenceChangeListener { _, newValue -> + if (newValue == true) { + packageList.isVisible = false + clientPackage.isVisible = true + serverPackage.isVisible = true + } else { + packageList.isVisible = true + clientPackage.isVisible = false + serverPackage.isVisible = false + } + + true + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/ui/setup/SetupFragment.kt b/android/app/src/main/java/su/xash/engine/ui/setup/SetupFragment.kt new file mode 100644 index 00000000..5ee5d6e9 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/ui/setup/SetupFragment.kt @@ -0,0 +1,64 @@ +package su.xash.engine.ui.setup + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.viewbinding.ViewBinding +import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import su.xash.engine.R +import su.xash.engine.databinding.FragmentLibraryBinding +import su.xash.engine.databinding.FragmentSetupBinding +import su.xash.engine.databinding.PageLocationBinding +import su.xash.engine.databinding.PageWelcomeBinding +import su.xash.engine.ui.library.LibraryViewModel +import su.xash.engine.ui.setup.pages.LocationPageFragment +import su.xash.engine.ui.setup.pages.WelcomePageFragment +import su.xash.engine.workers.FileCopyWorker + +class SetupFragment : Fragment() { + private var _binding: FragmentSetupBinding? = null + private val binding get() = _binding!! + private val setupViewModel: SetupViewModel by activityViewModels() + + private lateinit var setupPageAdapter: SetupPageAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + _binding = FragmentSetupBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + setupPageAdapter = SetupPageAdapter(this) + binding.viewPager.isUserInputEnabled = false + binding.viewPager.adapter = setupPageAdapter + + setupViewModel.pageNumber.observe(viewLifecycleOwner) { + binding.viewPager.setCurrentItem(it, true) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + +class SetupPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { + val pages = listOf(WelcomePageFragment(), LocationPageFragment()) + override fun getItemCount(): Int = 2 + override fun createFragment(position: Int): Fragment = pages[position] +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/ui/setup/SetupViewModel.kt b/android/app/src/main/java/su/xash/engine/ui/setup/SetupViewModel.kt new file mode 100644 index 00000000..9bbdd669 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/ui/setup/SetupViewModel.kt @@ -0,0 +1,29 @@ +package su.xash.engine.ui.setup + +import android.app.Application +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import su.xash.engine.MainApplication +import su.xash.engine.model.Game +import su.xash.engine.workers.FileCopyWorker + +class SetupViewModel(application: Application) : AndroidViewModel(application) { + val pageNumber: LiveData get() = _pageNumber + private val _pageNumber = MutableLiveData(0) + + fun checkIfGameDir(uri: Uri): Boolean { + val ctx = getApplication().applicationContext + val file = DocumentFile.fromTreeUri(ctx, uri)!! + return Game.checkIfGamedir(file) + } + + fun setPageNumber(pos: Int) { + _pageNumber.value = pos + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/ui/setup/pages/LocationPageFragment.kt b/android/app/src/main/java/su/xash/engine/ui/setup/pages/LocationPageFragment.kt new file mode 100644 index 00000000..9f069d9b --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/ui/setup/pages/LocationPageFragment.kt @@ -0,0 +1,62 @@ +package su.xash.engine.ui.setup.pages + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.viewbinding.ViewBinding +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import su.xash.engine.R +import su.xash.engine.databinding.PageLocationBinding +import su.xash.engine.databinding.PageWelcomeBinding +import su.xash.engine.ui.library.LibraryViewModel +import su.xash.engine.ui.setup.SetupFragment +import su.xash.engine.ui.setup.SetupViewModel + +class LocationPageFragment : Fragment() { + private var _binding: PageLocationBinding? = null + private val binding get() = _binding!! + private val setupViewModel: SetupViewModel by activityViewModels() + private val libraryViewModel: LibraryViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + _binding = PageLocationBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.pageButton.setOnClickListener { + getGamesDirectory.launch(null) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private val getGamesDirectory = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { + it?.let { + if (!setupViewModel.checkIfGameDir(it)) { + MaterialAlertDialogBuilder(requireContext()).apply { + setTitle(R.string.error) + setMessage(R.string.setup_location_empty) + setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } + show() + } + } else { + requireContext().contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION) + libraryViewModel.installGame(it) + findNavController().navigate(R.id.action_setupFragment_to_libraryFragment) + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/ui/setup/pages/WelcomePageFragment.kt b/android/app/src/main/java/su/xash/engine/ui/setup/pages/WelcomePageFragment.kt new file mode 100644 index 00000000..de87bf95 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/ui/setup/pages/WelcomePageFragment.kt @@ -0,0 +1,38 @@ +package su.xash.engine.ui.setup.pages + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.viewbinding.ViewBinding +import su.xash.engine.databinding.PageLocationBinding +import su.xash.engine.databinding.PageWelcomeBinding +import su.xash.engine.ui.setup.SetupFragment +import su.xash.engine.ui.setup.SetupViewModel + +class WelcomePageFragment : Fragment() { + private var _binding: PageWelcomeBinding? = null + private val binding get() = _binding!! + private val setupViewModel: SetupViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + _binding = PageWelcomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.pageButton.setOnClickListener { + setupViewModel.setPageNumber(1) + } + setupViewModel.setPageNumber(0) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/util/TGAReader.java b/android/app/src/main/java/su/xash/engine/util/TGAReader.java new file mode 100644 index 00000000..1e9f7d3f --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/util/TGAReader.java @@ -0,0 +1,589 @@ +/** + * TGAReader.java + *

+ * Copyright (c) 2014 Kenji Sasaki + * Released under the MIT license. + * https://github.com/npedotnet/TGAReader/blob/master/LICENSE + *

+ * English document + * https://github.com/npedotnet/TGAReader/blob/master/README.md + *

+ * Japanese document + * http://3dtech.jp/wiki/index.php?TGAReader + */ + +package su.xash.engine.util; + +import java.io.IOException; + +public final class TGAReader { + + public static final Order ARGB = new Order(16, 8, 0, 24); + public static final Order ABGR = new Order(0, 8, 16, 24); + + public static int getWidth(byte[] buffer) { + return (buffer[12] & 0xFF) | (buffer[13] & 0xFF) << 8; + } + + public static int getHeight(byte[] buffer) { + return (buffer[14] & 0xFF) | (buffer[15] & 0xFF) << 8; + } + + public static int[] read(byte[] buffer, Order order) throws IOException { + + // header +// int idFieldLength = buffer[0] & 0xFF; +// int colormapType = buffer[1] & 0xFF; + int type = buffer[2] & 0xFF; + int colormapOrigin = (buffer[3] & 0xFF) | (buffer[4] & 0xFF) << 8; + int colormapLength = (buffer[5] & 0xFF) | (buffer[6] & 0xFF) << 8; + int colormapDepth = buffer[7] & 0xFF; +// int originX = (buffer[8] & 0xFF) | (buffer[9] & 0xFF) << 8; // unsupported +// int originY = (buffer[10] & 0xFF) | (buffer[11] & 0xFF) << 8; // unsupported + int width = getWidth(buffer); + int height = getHeight(buffer); + int depth = buffer[16] & 0xFF; + int descriptor = buffer[17] & 0xFF; + + int[] pixels; + + // data + switch (type) { + case COLORMAP: { + int imageDataOffset = 18 + (colormapDepth / 8) * colormapLength; + pixels = createPixelsFromColormap(width, height, colormapDepth, buffer, imageDataOffset, buffer, colormapOrigin, descriptor, order); + } + break; + case RGB: + pixels = createPixelsFromRGB(width, height, depth, buffer, 18, descriptor, order); + break; + case GRAYSCALE: + pixels = createPixelsFromGrayscale(width, height, depth, buffer, 18, descriptor, order); + break; + case COLORMAP_RLE: { + int imageDataOffset = 18 + (colormapDepth / 8) * colormapLength; + byte[] decodeBuffer = decodeRLE(width, height, depth, buffer, imageDataOffset); + pixels = createPixelsFromColormap(width, height, colormapDepth, decodeBuffer, 0, buffer, colormapOrigin, descriptor, order); + } + break; + case RGB_RLE: { + byte[] decodeBuffer = decodeRLE(width, height, depth, buffer, 18); + pixels = createPixelsFromRGB(width, height, depth, decodeBuffer, 0, descriptor, order); + } + break; + case GRAYSCALE_RLE: { + byte[] decodeBuffer = decodeRLE(width, height, depth, buffer, 18); + pixels = createPixelsFromGrayscale(width, height, depth, decodeBuffer, 0, descriptor, order); + } + break; + default: + throw new IOException("Unsupported image type: " + type); + } + + return pixels; + + } + + private static final int COLORMAP = 1; + private static final int RGB = 2; + private static final int GRAYSCALE = 3; + private static final int COLORMAP_RLE = 9; + private static final int RGB_RLE = 10; + private static final int GRAYSCALE_RLE = 11; + + private static final int RIGHT_ORIGIN = 0x10; + private static final int UPPER_ORIGIN = 0x20; + + private static byte[] decodeRLE(int width, int height, int depth, byte[] buffer, int offset) { + int elementCount = depth / 8; + byte[] elements = new byte[elementCount]; + int decodeBufferLength = elementCount * width * height; + byte[] decodeBuffer = new byte[decodeBufferLength]; + int decoded = 0; + while (decoded < decodeBufferLength) { + int packet = buffer[offset++] & 0xFF; + if ((packet & 0x80) != 0) { // RLE + for (int i = 0; i < elementCount; i++) { + elements[i] = buffer[offset++]; + } + int count = (packet & 0x7F) + 1; + for (int i = 0; i < count; i++) { + for (int j = 0; j < elementCount; j++) { + decodeBuffer[decoded++] = elements[j]; + } + } + } else { // RAW + int count = (packet + 1) * elementCount; + for (int i = 0; i < count; i++) { + decodeBuffer[decoded++] = buffer[offset++]; + } + } + } + return decodeBuffer; + } + + private static int[] createPixelsFromColormap(int width, int height, int depth, byte[] bytes, int offset, byte[] palette, int colormapOrigin, int descriptor, Order order) throws IOException { + int[] pixels; + int rs = order.redShift; + int gs = order.greenShift; + int bs = order.blueShift; + int as = order.alphaShift; + switch (depth) { + case 24: + pixels = new int[width * height]; + if ((descriptor & RIGHT_ORIGIN) != 0) { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int colormapIndex = bytes[offset + width * i + j] & + 0xFF - colormapOrigin; + int color = 0xFFFFFFFF; + if (colormapIndex >= 0) { + int index = 3 * colormapIndex + 18; + int b = palette[index] & 0xFF; + int g = palette[index + 1] & 0xFF; + int r = palette[index + 2] & 0xFF; + int a = 0xFF; + color = (r << rs) | (g << gs) | (b << bs) | (a << as); + } + pixels[width * i + (width - j - 1)] = color; + } + } + } else { + // LowerRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int colormapIndex = bytes[offset + width * i + j] & + 0xFF - colormapOrigin; + int color = 0xFFFFFFFF; + if (colormapIndex >= 0) { + int index = 3 * colormapIndex + 18; + int b = palette[index] & 0xFF; + int g = palette[index + 1] & 0xFF; + int r = palette[index + 2] & 0xFF; + int a = 0xFF; + color = (r << rs) | (g << gs) | (b << bs) | (a << as); + } + pixels[width * (height - i - 1) + (width - j - 1)] = color; + } + } + } + } else { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int colormapIndex = bytes[offset + width * i + j] & + 0xFF - colormapOrigin; + int color = 0xFFFFFFFF; + if (colormapIndex >= 0) { + int index = 3 * colormapIndex + 18; + int b = palette[index] & 0xFF; + int g = palette[index + 1] & 0xFF; + int r = palette[index + 2] & 0xFF; + int a = 0xFF; + color = (r << rs) | (g << gs) | (b << bs) | (a << as); + } + pixels[width * i + j] = color; + } + } + } else { + // LowerLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int colormapIndex = bytes[offset + width * i + j] & + 0xFF - colormapOrigin; + int color = 0xFFFFFFFF; + if (colormapIndex >= 0) { + int index = 3 * colormapIndex + 18; + int b = palette[index] & 0xFF; + int g = palette[index + 1] & 0xFF; + int r = palette[index + 2] & 0xFF; + int a = 0xFF; + color = (r << rs) | (g << gs) | (b << bs) | (a << as); + } + pixels[width * (height - i - 1) + j] = color; + } + } + } + } + break; + case 32: + pixels = new int[width * height]; + if ((descriptor & RIGHT_ORIGIN) != 0) { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int colormapIndex = bytes[offset + width * i + j] & + 0xFF - colormapOrigin; + int color = 0xFFFFFFFF; + if (colormapIndex >= 0) { + int index = 4 * colormapIndex + 18; + int b = palette[index] & 0xFF; + int g = palette[index + 1] & 0xFF; + int r = palette[index + 2] & 0xFF; + int a = palette[index + 3] & 0xFF; + color = (r << rs) | (g << gs) | (b << bs) | (a << as); + } + pixels[width * i + (width - j - 1)] = color; + } + } + } else { + // LowerRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int colormapIndex = bytes[offset + width * i + j] & + 0xFF - colormapOrigin; + int color = 0xFFFFFFFF; + if (colormapIndex >= 0) { + int index = 4 * colormapIndex + 18; + int b = palette[index] & 0xFF; + int g = palette[index + 1] & 0xFF; + int r = palette[index + 2] & 0xFF; + int a = palette[index + 3] & 0xFF; + color = (r << rs) | (g << gs) | (b << bs) | (a << as); + } + pixels[width * (height - i - 1) + (width - j - 1)] = color; + } + } + } + } else { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int colormapIndex = bytes[offset + width * i + j] & + 0xFF - colormapOrigin; + int color = 0xFFFFFFFF; + if (colormapIndex >= 0) { + int index = 4 * colormapIndex + 18; + int b = palette[index] & 0xFF; + int g = palette[index + 1] & 0xFF; + int r = palette[index + 2] & 0xFF; + int a = palette[index + 3] & 0xFF; + color = (r << rs) | (g << gs) | (b << bs) | (a << as); + } + pixels[width * i + j] = color; + } + } + } else { + // LowerLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int colormapIndex = bytes[offset + width * i + j] & + 0xFF - colormapOrigin; + int color = 0xFFFFFFFF; + if (colormapIndex >= 0) { + int index = 4 * colormapIndex + 18; + int b = palette[index] & 0xFF; + int g = palette[index + 1] & 0xFF; + int r = palette[index + 2] & 0xFF; + int a = palette[index + 3] & 0xFF; + color = (r << rs) | (g << gs) | (b << bs) | (a << as); + } + pixels[width * (height - i - 1) + j] = color; + } + } + } + } + break; + default: + throw new IOException("Unsupported depth:" + depth); + } + return pixels; + } + + private static int[] createPixelsFromRGB(int width, int height, int depth, byte[] bytes, int offset, int descriptor, Order order) throws IOException { + int[] pixels; + int rs = order.redShift; + int gs = order.greenShift; + int bs = order.blueShift; + int as = order.alphaShift; + switch (depth) { + case 24: + pixels = new int[width * height]; + if ((descriptor & RIGHT_ORIGIN) != 0) { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int index = offset + 3 * width * i + 3 * j; + int b = bytes[index] & 0xFF; + int g = bytes[index + 1] & 0xFF; + int r = bytes[index + 2] & 0xFF; + int a = 0xFF; + pixels[width * i + (width - j - 1)] = (r << rs) | + (g << gs) | + (b << bs) | + (a << as); + } + } + } else { + // LowerRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int index = offset + 3 * width * i + 3 * j; + int b = bytes[index] & 0xFF; + int g = bytes[index + 1] & 0xFF; + int r = bytes[index + 2] & 0xFF; + int a = 0xFF; + pixels[width * (height - i - 1) + (width - j - 1)] = (r << rs) | + (g << gs) | + (b << bs) | + (a << as); + } + } + } + } else { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int index = offset + 3 * width * i + 3 * j; + int b = bytes[index] & 0xFF; + int g = bytes[index + 1] & 0xFF; + int r = bytes[index + 2] & 0xFF; + int a = 0xFF; + pixels[width * i + j] = (r << rs) | + (g << gs) | + (b << bs) | + (a << as); + } + } + } else { + // LowerLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int index = offset + 3 * width * i + 3 * j; + int b = bytes[index] & 0xFF; + int g = bytes[index + 1] & 0xFF; + int r = bytes[index + 2] & 0xFF; + int a = 0xFF; + pixels[width * (height - i - 1) + j] = (r << rs) | + (g << gs) | + (b << bs) | + (a << as); + } + } + } + } + break; + case 32: + pixels = new int[width * height]; + if ((descriptor & RIGHT_ORIGIN) != 0) { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int index = offset + 4 * width * i + 4 * j; + int b = bytes[index] & 0xFF; + int g = bytes[index + 1] & 0xFF; + int r = bytes[index + 2] & 0xFF; + int a = bytes[index + 3] & 0xFF; + pixels[width * i + (width - j - 1)] = (r << rs) | + (g << gs) | + (b << bs) | + (a << as); + } + } + } else { + // LowerRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int index = offset + 4 * width * i + 4 * j; + int b = bytes[index] & 0xFF; + int g = bytes[index + 1] & 0xFF; + int r = bytes[index + 2] & 0xFF; + int a = bytes[index + 3] & 0xFF; + pixels[width * (height - i - 1) + (width - j - 1)] = (r << rs) | + (g << gs) | + (b << bs) | + (a << as); + } + } + } + } else { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int index = offset + 4 * width * i + 4 * j; + int b = bytes[index] & 0xFF; + int g = bytes[index + 1] & 0xFF; + int r = bytes[index + 2] & 0xFF; + int a = bytes[index + 3] & 0xFF; + pixels[width * i + j] = (r << rs) | + (g << gs) | + (b << bs) | + (a << as); + } + } + } else { + // LowerLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int index = offset + 4 * width * i + 4 * j; + int b = bytes[index] & 0xFF; + int g = bytes[index + 1] & 0xFF; + int r = bytes[index + 2] & 0xFF; + int a = bytes[index + 3] & 0xFF; + pixels[width * (height - i - 1) + j] = (r << rs) | + (g << gs) | + (b << bs) | + (a << as); + } + } + } + } + break; + default: + throw new IOException("Unsupported depth:" + depth); + } + return pixels; + } + + private static int[] createPixelsFromGrayscale(int width, int height, int depth, byte[] bytes, int offset, int descriptor, Order order) throws IOException { + int[] pixels; + int rs = order.redShift; + int gs = order.greenShift; + int bs = order.blueShift; + int as = order.alphaShift; + switch (depth) { + case 8: + pixels = new int[width * height]; + if ((descriptor & RIGHT_ORIGIN) != 0) { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int e = bytes[offset + width * i + j] & 0xFF; + int a = 0xFF; + pixels[width * i + (width - j - 1)] = (e << rs) | + (e << gs) | + (e << bs) | + (a << as); + } + } + } else { + // LowerRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int e = bytes[offset + width * i + j] & 0xFF; + int a = 0xFF; + pixels[width * (height - i - 1) + (width - j - 1)] = (e << rs) | + (e << gs) | + (e << bs) | + (a << as); + } + } + } + } else { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int e = bytes[offset + width * i + j] & 0xFF; + int a = 0xFF; + pixels[width * i + j] = (e << rs) | + (e << gs) | + (e << bs) | + (a << as); + } + } + } else { + // LowerLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int e = bytes[offset + width * i + j] & 0xFF; + int a = 0xFF; + pixels[width * (height - i - 1) + j] = (e << rs) | + (e << gs) | + (e << bs) | + (a << as); + } + } + } + } + break; + case 16: + pixels = new int[width * height]; + if ((descriptor & RIGHT_ORIGIN) != 0) { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int e = bytes[offset + 2 * width * i + 2 * j] & 0xFF; + int a = bytes[offset + 2 * width * i + 2 * j + 1] & 0xFF; + pixels[width * i + (width - j - 1)] = (e << rs) | + (e << gs) | + (e << bs) | + (a << as); + } + } + } else { + // LowerRight + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int e = bytes[offset + 2 * width * i + 2 * j] & 0xFF; + int a = bytes[offset + 2 * width * i + 2 * j + 1] & 0xFF; + pixels[width * (height - i - 1) + (width - j - 1)] = (e << rs) | + (e << gs) | + (e << bs) | + (a << as); + } + } + } + } else { + if ((descriptor & UPPER_ORIGIN) != 0) { + // UpperLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int e = bytes[offset + 2 * width * i + 2 * j] & 0xFF; + int a = bytes[offset + 2 * width * i + 2 * j + 1] & 0xFF; + pixels[width * i + j] = (e << rs) | + (e << gs) | + (e << bs) | + (a << as); + } + } + } else { + // LowerLeft + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + int e = bytes[offset + 2 * width * i + 2 * j] & 0xFF; + int a = bytes[offset + 2 * width * i + 2 * j + 1] & 0xFF; + pixels[width * (height - i - 1) + j] = (e << rs) | + (e << gs) | + (e << bs) | + (a << as); + } + } + } + } + break; + default: + throw new IOException("Unsupported depth:" + depth); + } + return pixels; + } + + private TGAReader() { + } + + public static final class Order { + Order(int redShift, int greenShift, int blueShift, int alphaShift) { + this.redShift = redShift; + this.greenShift = greenShift; + this.blueShift = blueShift; + this.alphaShift = alphaShift; + } + + public int redShift; + public int greenShift; + public int blueShift; + public int alphaShift; + } + +} \ No newline at end of file diff --git a/android/app/src/main/java/su/xash/engine/workers/FileCopyWorker.kt b/android/app/src/main/java/su/xash/engine/workers/FileCopyWorker.kt new file mode 100644 index 00000000..7627cb64 --- /dev/null +++ b/android/app/src/main/java/su/xash/engine/workers/FileCopyWorker.kt @@ -0,0 +1,58 @@ +package su.xash.engine.workers + +import android.content.Context +import android.net.Uri +import android.provider.DocumentsContract +import android.util.Log +import androidx.documentfile.provider.DocumentFile +import androidx.work.CoroutineWorker +import androidx.work.Worker +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import su.xash.engine.model.Game +import java.io.FileInputStream + +const val KEY_FILE_URI = "KEY_FILE_URI" + +class FileCopyWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { + companion object { + const val Input = "Input" + } + + override suspend fun doWork(): Result { + withContext(Dispatchers.IO) { + val fileUri = inputData.getString(KEY_FILE_URI) + setProgress(workDataOf(Input to fileUri)) + + val target = DocumentFile.fromFile(applicationContext.getExternalFilesDir(null)!!) + val source = DocumentFile.fromTreeUri(applicationContext, Uri.parse(fileUri)) + + source?.copyDirTo(applicationContext, target) ?: return@withContext Result.failure() + } + return Result.success() + } +} + +fun DocumentFile.copyFileTo(ctx: Context, file: DocumentFile) { + val outFile = file.createFile("application", name!!)!! + + ctx.contentResolver.openOutputStream(outFile.uri).use { os -> + ctx.contentResolver.openInputStream(uri).use { + it?.copyTo(os!!) + } + } +} + +fun DocumentFile.copyDirTo(ctx: Context, dir: DocumentFile) { + val outDir = dir.createDirectory(name!!)!! + + listFiles().forEach { + if (it.isDirectory) { + it.copyDirTo(ctx, outDir) + } else { + it.copyFileTo(ctx, outDir) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_baseline_add_24.xml b/android/app/src/main/res/drawable/ic_baseline_add_24.xml new file mode 100644 index 00000000..0553ae30 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_add_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_delete_24.xml b/android/app/src/main/res/drawable/ic_baseline_delete_24.xml new file mode 100644 index 00000000..79372b1d --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_delete_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_folder_open_24.xml b/android/app/src/main/res/drawable/ic_baseline_folder_open_24.xml new file mode 100644 index 00000000..76fd9059 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_folder_open_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_key_24.xml b/android/app/src/main/res/drawable/ic_baseline_key_24.xml new file mode 100644 index 00000000..f49ad6f0 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_key_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_person_24.xml b/android/app/src/main/res/drawable/ic_baseline_person_24.xml new file mode 100644 index 00000000..98730cd9 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_person_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml b/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml new file mode 100644 index 00000000..97f9a3c0 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_settings_24.xml b/android/app/src/main/res/drawable/ic_baseline_settings_24.xml new file mode 100644 index 00000000..a7c7678d --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_settings_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_terminal_24.xml b/android/app/src/main/res/drawable/ic_baseline_terminal_24.xml new file mode 100644 index 00000000..fa304167 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_terminal_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/steam_button_gradient.xml b/android/app/src/main/res/drawable/steam_button_gradient.xml new file mode 100644 index 00000000..6ee97b7b --- /dev/null +++ b/android/app/src/main/res/drawable/steam_button_gradient.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/steam_icon.xml b/android/app/src/main/res/drawable/steam_icon.xml new file mode 100644 index 00000000..92b4ad0f --- /dev/null +++ b/android/app/src/main/res/drawable/steam_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/steam_logo.xml b/android/app/src/main/res/drawable/steam_logo.xml new file mode 100644 index 00000000..4fb91059 --- /dev/null +++ b/android/app/src/main/res/drawable/steam_logo.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..3879b823 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/card_game.xml b/android/app/src/main/res/layout/card_game.xml new file mode 100644 index 00000000..853b30e7 --- /dev/null +++ b/android/app/src/main/res/layout/card_game.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/edit_text_preference.xml b/android/app/src/main/res/layout/edit_text_preference.xml new file mode 100644 index 00000000..55d2bd17 --- /dev/null +++ b/android/app/src/main/res/layout/edit_text_preference.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_app_settings.xml b/android/app/src/main/res/layout/fragment_app_settings.xml new file mode 100644 index 00000000..0e738e9d --- /dev/null +++ b/android/app/src/main/res/layout/fragment_app_settings.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_game_settings.xml b/android/app/src/main/res/layout/fragment_game_settings.xml new file mode 100644 index 00000000..af1a003d --- /dev/null +++ b/android/app/src/main/res/layout/fragment_game_settings.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_library.xml b/android/app/src/main/res/layout/fragment_library.xml new file mode 100644 index 00000000..ade658da --- /dev/null +++ b/android/app/src/main/res/layout/fragment_library.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_setup.xml b/android/app/src/main/res/layout/fragment_setup.xml new file mode 100644 index 00000000..644a5b6f --- /dev/null +++ b/android/app/src/main/res/layout/fragment_setup.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_steam_login.xml b/android/app/src/main/res/layout/fragment_steam_login.xml new file mode 100644 index 00000000..872437bd --- /dev/null +++ b/android/app/src/main/res/layout/fragment_steam_login.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + +