Compare commits

...

6 Commits

Author SHA1 Message Date
Uriel
9be83cf108 Don't change xmx of gradle 2025-09-06 17:06:35 -03:00
Uriel
d7436e3972 remove debug dev env 2025-09-06 16:06:03 -04:00
Uriel
bebc34d035 Start migrating android into tauri 2025-09-06 15:51:20 -04:00
Uriel
0f06ac0253 update linux metadata (#1524) 2025-09-04 13:44:17 -03:00
Butterscotch!
4568ebb41a Foot snapping fix (#1519) 2025-09-03 21:11:24 -03:00
Ilia Ki
2777d8af89 Fix endian docs for smol HID (#1520) 2025-09-03 20:03:39 -03:00
38 changed files with 445 additions and 136 deletions

4
.gitignore vendored
View File

@@ -36,6 +36,8 @@ build/
# Rust build artifacts
/target
/.tauri
/tauri.settings.gradle
# direnv has been claimed for Nix usage
.direnv/
@@ -43,6 +45,8 @@ build/
# Ignore Android local properties
local.properties
keystore.properties
/.android
# Ignore temporary config
vrconfig.yml.tmp

1
Cargo.lock generated
View File

@@ -4081,7 +4081,6 @@ name = "slimevr"
version = "0.0.0"
dependencies = [
"cfg-if",
"cfg_aliases",
"clap",
"clap-verbosity-flag",
"color-eyre",

View File

@@ -1,3 +1,21 @@
plugins {
id("org.ajoberstar.grgit")
}
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.6.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${rootProject.properties["kotlinVersion"]}")
}
}
subprojects.filter { it.name.contains("tauri") }.forEach {
it.repositories {
mavenCentral()
google()
}
}

23
buildSrc/build.gradle.kts Normal file
View File

@@ -0,0 +1,23 @@
plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins {
create("pluginsForCoolKids") {
id = "rust"
implementationClass = "RustPlugin"
}
}
}
repositories {
google()
mavenCentral()
}
dependencies {
compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:8.6.1")
}

View File

@@ -0,0 +1,54 @@
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
open class BuildTask : DefaultTask() {
@Input
var rootDirRel: String? = null
@Input
var target: String? = null
@Input
var release: Boolean? = null
@TaskAction
fun assemble() {
val executable = """pnpm""";
try {
runTauriCli(executable)
} catch (e: Exception) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
runTauriCli("$executable.cmd")
} else if (Os.isFamily(Os.FAMILY_UNIX)){
runTauriCli("/nix/store/r2n3dbbp0djly6wjxx43sbffaq3abjpy-pnpm-10.15.0/bin/pnpm")
} else {
throw e
}
}
}
fun runTauriCli(executable: String) {
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
val target = target ?: throw GradleException("target cannot be null")
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("tauri", "android", "android-studio-script")
project.exec {
workingDir(File(project.projectDir, rootDirRel))
executable(executable)
args(args)
if (project.logger.isEnabled(LogLevel.DEBUG)) {
args("-vv")
} else if (project.logger.isEnabled(LogLevel.INFO)) {
args("-v")
}
if (release) {
args("--release")
}
args(listOf("--target", target))
}.assertNormalExitValue()
}
}

View File

@@ -0,0 +1,85 @@
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.get
const val TASK_GROUP = "rust"
open class Config {
lateinit var rootDirRel: String
}
open class RustPlugin : Plugin<Project> {
private lateinit var config: Config
override fun apply(project: Project) = with(project) {
config = extensions.create("rust", Config::class.java)
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
extensions.configure<ApplicationExtension> {
@Suppress("UnstableApiUsage")
flavorDimensions.add("abi")
productFlavors {
create("universal") {
dimension = "abi"
ndk {
abiFilters += abiList
}
}
defaultArchList.forEachIndexed { index, arch ->
create(arch) {
dimension = "abi"
ndk {
abiFilters.add(defaultAbiList[index])
}
}
}
}
}
afterEvaluate {
for (profile in listOf("debug", "release")) {
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
val buildTask = tasks.maybeCreate(
"rustBuildUniversal$profileCapitalized",
DefaultTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for all targets"
}
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
for (targetPair in targetsList.withIndex()) {
val targetName = targetPair.value
val targetArch = archList[targetPair.index]
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
val targetBuildTask = project.tasks.maybeCreate(
"rustBuild$targetArchCapitalized$profileCapitalized",
BuildTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for $targetArch"
rootDirRel = config.rootDirRel
target = targetName
release = profile == "release"
}
buildTask.dependsOn(targetBuildTask)
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
targetBuildTask
)
}
}
}
}
}

View File

@@ -57,11 +57,36 @@
_module.args.pkgs = import self.inputs.nixpkgs {
inherit system;
overlays = [nixgl.overlay];
# Allow android SDK
# config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
# "android-sdk-cmdline-tools"
# "android-sdk-platform-tools"
# "platform-tools"
# "android-sdk-tools"
# "android-sdk-emulator"
# "android-sdk-system-image-32-google_apis_playstore-arm64-v8a-system-image-32-google_apis_playstore-x86_64"
# "system-image-32-google_apis_playstore-arm64-v8a"
# "system-image-32-google_apis_playstore-x86_64"
# "android-sdk-system-image-34-google_apis_playstore-arm64-v8a-system-image-34-google_apis_playstore-x86_64"
# "system-image-34-google_apis_playstore-arm64-v8a"
# "system-image-34-google_apis_playstore-x86_64"
# "emulator"
# "tools"
# "android-sdk-build-tools"
# "build-tools"
# "android-sdk-platforms"
# "platforms"
# "cmake"
# "android-sdk-ndk"
# "ndk"
# "android-sdk-extras-google-gcm"
# "extras-google-gcm"
# "cmdline-tools"
# ];
};
devenv.shells.default = let
fenixpkgs = inputs'.fenix.packages;
rust_toolchain = lib.importTOML ./rust-toolchain.toml;
in {
name = "slimevr";
@@ -76,6 +101,7 @@
(with pkgs; [
pkgs.nixgl.nixGLIntel
cacert
stow
])
++ lib.optionals pkgs.stdenv.isLinux (with pkgs; [
atk
@@ -118,6 +144,12 @@
};
languages.kotlin.enable = true;
# android = {
# enable = true;
# googleAPIs.enable = false;
# googleTVAddOns.enable = false;
# };
languages.javascript = {
enable = true;
corepack.enable = true;
@@ -127,11 +159,10 @@
languages.rust = {
enable = true;
toolchain = fenixpkgs.fromToolchainName {
name = rust_toolchain.toolchain.channel;
sha256 = "sha256-yMuSb5eQPO/bHv+Bcf/US8LVMbf/G/0MSfiPwBhiPpk=";
toolchainPackage = fenixpkgs.fromToolchainFile {
file = ./rust-toolchain.toml;
sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE=";
};
components = rust_toolchain.toolchain.components;
};
env = {

View File

@@ -3,14 +3,26 @@ org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAME
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
-Dfile.encoding=UTF-8
kotlin.code.style=official
# https://github.com/Kotlin/kotlinx-atomicfu#atomicfu-compiler-plugin
kotlinx.atomicfu.enableJvmIrTransformation=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonFinalResIds=false
org.gradle.unsafe.configuration-cache=false
kotlinVersion=2.0.20

View File

@@ -12,6 +12,13 @@ default-run = "slimevr"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "slimevr_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
@@ -22,7 +29,6 @@ custom-protocol = ["tauri/custom-protocol"]
[build-dependencies]
tauri-build = { version = "2.0", features = [] }
cfg_aliases = "0.2"
shadow-rs = "0.35"
[dependencies]
@@ -49,7 +55,6 @@ shadow-rs = { version = "0.35", default-features = false }
const_format = "0.2.30"
cfg-if = "1"
color-eyre = "0.6"
rfd = { version = "0.15", features = ["gtk3"], default-features = false }
dirs-next = "2.0.0"
discord-sdk = "0.3.6"
tokio = { version = "1.37.0", features = ["time"] }
@@ -62,3 +67,6 @@ winreg = "0.52"
[target.'cfg(target_os = "linux")'.dependencies]
libloading = "0.8"
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
rfd = { version = "0.15", features = ["gtk3"], default-features = false }

View File

@@ -1,5 +1,3 @@
use cfg_aliases::cfg_aliases;
fn main() -> shadow_rs::SdResult<()> {
// Bypass for Nix script having libudev-zero and Tauri not liking it
if let Some(path) = option_env!("SLIMEVR_RUST_LD_LIBRARY_PATH") {
@@ -7,9 +5,5 @@ fn main() -> shadow_rs::SdResult<()> {
}
tauri_build::build();
cfg_aliases! {
mobile: { any(target_os = "ios", target_os = "android") },
desktop: { not(any(target_os = "ios", target_os = "android")) }
}
shadow_rs::new()
}

View File

@@ -65,6 +65,10 @@ work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
</provides>
<releases>
<release version="0.16.2" date="2025-08-01"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.2</url></release>
<release version="0.16.1" date="2025-07-27"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.1</url></release>
<release version="0.16.1~rc.2" type="development" date="2025-07-17"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.1-rc.2</url></release>
<release version="0.16.1~rc.1" type="development" date="2025-07-04"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.1-rc.1</url></release>
<release version="0.16.0" date="2025-07-01"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.0</url></release>
<release version="0.16.0~rc.2" type="development" date="2025-06-20"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.0-rc.2</url></release>
<release version="0.16.0~rc.1" type="development" date="2025-05-27"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.0-rc.1</url></release>

10
gui/src-tauri/gen/.stowrc Normal file
View File

@@ -0,0 +1,10 @@
--ignore='^gui$'
--ignore='^\..+'
--ignore='^node_modules$'
--ignore='^build$'
--ignore='^(local|keystore).properties$'
--ignore='^target$'
--ignore='^assets'
--ignore='.*\.(toml|yaml|nix|lock|json|md)$'
-d ../../../../
-t android

View File

@@ -0,0 +1 @@
../../../../server/android/

View File

@@ -0,0 +1 @@
../../../../../SlimeVR-Server/build.gradle.kts

View File

@@ -0,0 +1 @@
../../../../../SlimeVR-Server/buildSrc

View File

@@ -0,0 +1 @@
../../../../../SlimeVR-Server/gradle

View File

@@ -0,0 +1 @@
../../../../../SlimeVR-Server/gradle.properties

1
gui/src-tauri/gen/android/gradlew vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../../SlimeVR-Server/gradlew

1
gui/src-tauri/gen/android/gradlew.bat vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../../SlimeVR-Server/gradlew.bat

View File

@@ -0,0 +1 @@
../../../../../SlimeVR-Server/server

View File

@@ -0,0 +1 @@
../../../../../SlimeVR-Server/settings.gradle.kts

View File

@@ -0,0 +1 @@
../../../../../SlimeVR-Server/solarxr-protocol

View File

@@ -0,0 +1 @@
../../../../../SlimeVR-Server/tauri.settings.gradle

View File

@@ -0,0 +1,6 @@
pub struct TrayAvailable(pub bool);
#[tauri::command]
pub fn is_tray_available(tray_available: tauri::State<TrayAvailable>) -> bool {
tray_available.0
}

45
gui/src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,45 @@
use tauri::Manager;
mod cross;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
log_panics::init();
tauri::Builder::default()
.plugin(
tauri_plugin_log::Builder::new()
.target(tauri_plugin_log::Target::new(
tauri_plugin_log::TargetKind::LogDir {
file_name: Some("slimevr".to_string()),
},
))
.max_file_size(30_000 /* bytes */)
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepSome(3))
.build(),
)
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_http::init())
.invoke_handler(tauri::generate_handler![cross::is_tray_available,])
.setup(move |app| {
log::info!("SlimeVR started!");
let _ = tauri::WebviewWindowBuilder::new(
app,
"main",
tauri::WebviewUrl::App("index.html".into()),
)
.build()?;
app.manage(cross::TrayAvailable(false));
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -22,6 +22,7 @@ use crate::util::{
get_launch_path, show_error, valid_java_paths, Cli, JAVA_BIN, MINIMUM_JAVA_VERSION,
};
mod cross;
mod presence;
mod state;
mod tray;
@@ -106,6 +107,7 @@ fn main() -> Result<()> {
setup_webview2()?;
// Check for environment variables that can affect the server, and if so, warn in log and GUI
#[cfg(desktop)]
check_environment_variables();
// Spawn server process
@@ -189,6 +191,7 @@ fn setup_webview2() -> Result<()> {
Ok(())
}
#[cfg(desktop)]
fn check_environment_variables() {
use itertools::Itertools;
const ENVS_TO_CHECK: &[&str] = &["_JAVA_OPTIONS", "JAVA_TOOL_OPTIONS"];
@@ -264,7 +267,7 @@ fn setup_tauri(
open_logs_folder,
tray::update_translations,
tray::update_tray_text,
tray::is_tray_available,
cross::is_tray_available,
presence::discord_client_exists,
presence::update_presence,
presence::clear_presence,
@@ -299,7 +302,7 @@ fn setup_tauri(
tray::create_tray(handle)?;
presence::create_presence(handle)?;
} else {
app.manage(tray::TrayAvailable(false));
app.manage(cross::TrayAvailable(false));
}
app.manage(Mutex::new(window_state));

View File

@@ -1,5 +1,6 @@
use std::{collections::HashMap, sync::Mutex};
use crate::cross::TrayAvailable;
use tauri::{
include_image,
menu::{Menu, MenuBuilder, MenuItemBuilder, MenuItemKind},
@@ -8,7 +9,6 @@ use tauri::{
};
pub struct TrayMenu<R: Runtime>(Menu<R>);
pub struct TrayAvailable(pub bool);
pub struct TrayTranslations {
store: Mutex<HashMap<String, String>>,
@@ -22,11 +22,6 @@ impl TrayTranslations {
}
}
#[tauri::command]
pub fn is_tray_available(tray_available: State<TrayAvailable>) -> bool {
tray_available.0
}
#[tauri::command]
pub fn update_translations<R: Runtime>(
app: AppHandle<R>,

View File

@@ -0,0 +1,6 @@
{
"build": {
"beforeDevCommand": "pnpm run start --host"
},
"identifier": "dev.slimevr.android"
}

View File

@@ -2,3 +2,4 @@
channel = "1.82"
profile = "default"
components = ["rustc", "cargo", "clippy", "rustfmt", "rust-analyzer", "rust-src"]
targets = ["x86_64-linux-android", "aarch64-linux-android", "armv7-linux-androideabi", "i686-linux-android"]

View File

@@ -1,2 +1,9 @@
/build
/src/main/resources/web-gui
/src/main/java/dev/slimevr/android/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro
/tauri.properties

View File

@@ -7,14 +7,24 @@
*/
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.FileInputStream
import java.util.Properties
plugins {
kotlin("android")
kotlin("plugin.serialization")
id("com.github.gmazzo.buildconfig")
id("com.android.application") version "8.6.1"
id("com.android.application")
id("org.ajoberstar.grgit")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
kotlin {
@@ -28,17 +38,6 @@ java {
}
}
tasks.register<Copy>("copyGuiAssets") {
from(rootProject.layout.projectDirectory.dir("gui/dist"))
into(layout.projectDirectory.dir("src/main/resources/web-gui"))
if (inputs.sourceFiles.isEmpty) {
throw GradleException("You need to run \"pnpm run build\" on the gui folder first!")
}
}
tasks.preBuild {
dependsOn(":server:android:copyGuiAssets")
}
tasks.withType<KotlinCompile> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
@@ -61,6 +60,14 @@ repositories {
google()
}
rust {
rootDirRel = if (projectDir.absolutePath.contains("gen/android")) {
"../../../../../"
} else {
"../../gui/src-tauri"
}
}
dependencies {
implementation(project(":server:core"))
@@ -68,17 +75,15 @@ dependencies {
implementation("org.apache.commons:commons-lang3:3.15.0")
// Android stuff
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("androidx.core:core-ktx:1.13.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
// For hosting web GUI
implementation("io.ktor:ktor-server-core:2.3.12")
implementation("io.ktor:ktor-server-netty:2.3.10")
implementation("io.ktor:ktor-server-caching-headers:2.3.12")
// Serial
implementation("com.github.mik3y:usb-serial-for-android:3.7.0")
@@ -90,34 +95,18 @@ extra.apply {
set("gitVersionName", grgit.describe(mapOf("tags" to true, "always" to true)))
}
android {
// The app's namespace. Used primarily to access app resources.
namespace = "dev.slimevr.android"
/* compileSdk specifies the Android API level Gradle should use to
compile your app. This means your app can use the API features included in
this API level and lower. */
compileSdk = 35
/* The defaultConfig block encapsulates default settings and entries for all
build variants and can override some attributes in main/AndroidManifest.xml
dynamically from the build system. You can configure product flavors to override
these values for different versions of your app. */
packaging {
resources.excludes.add("META-INF/*")
}
defaultConfig {
// Uniquely identifies the package for publishing.
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "dev.slimevr.server.android"
// Defines the minimum API level required to run the app.
minSdk = 26
// Specifies the API level used to test the app.
targetSdk = 35
// adds an offset of the version code as we might do apk releases in the middle of actual
@@ -128,8 +117,27 @@ android {
// Defines a user-friendly version name for your app.
versionName = extra["gitVersionName"] as? String ?: "v0.0.0"
setProperty("archivesBaseName", "app")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters += listOf("x86", "x86_64", "arm64-v8a", "armeabi-v7a")
}
}
signingConfigs {
create("release") {
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["password"] as String?
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["password"] as String?
}
}
/* The buildTypes block is where you can configure multiple build types.
@@ -139,15 +147,25 @@ android {
build type applies ProGuard settings and is not signed by default. */
buildTypes {
/* By default, Android Studio configures the release build type to enable code
shrinking, using minifyEnabled, and specifies the default ProGuard rules file. */
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging {
jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
isMinifyEnabled = true // Enables code shrinking for the release build type.
isMinifyEnabled = true
signingConfig = signingConfigs.getByName("release")
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro",
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray(),
)
}
}
@@ -159,4 +177,13 @@ android {
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
buildConfig = true
}
}
try {
apply(from = "tauri.build.gradle.kts")
} catch (e: Exception) {
println("Couldn't enable tauri stuff")
}

View File

@@ -28,6 +28,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service
android:name=".ForegroundService"

View File

@@ -9,17 +9,7 @@ import dev.slimevr.Keybinding
import dev.slimevr.VRServer
import dev.slimevr.android.serial.AndroidSerialHandler
import io.eiren.util.logging.LogManager
import io.ktor.http.CacheControl
import io.ktor.http.CacheControl.Visibility
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.http.content.CachingOptions
import io.ktor.server.http.content.staticResources
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.cachingheaders.CachingHeaders
import io.ktor.server.routing.routing
import java.io.File
import java.time.ZonedDateTime
import kotlin.concurrent.thread
import kotlin.system.exitProcess
@@ -29,18 +19,6 @@ val vrServerInitialized: Boolean
get() = ::vrServer.isInitialized
fun main(activity: AppCompatActivity) {
// Host the web GUI server
embeddedServer(Netty, port = 34536) {
routing {
install(CachingHeaders) {
options { _, _ ->
CachingOptions(CacheControl.NoStore(Visibility.Public), ZonedDateTime.now())
}
}
staticResources("/", "web-gui", "index.html")
}
}.start(wait = false)
thread(start = true, name = "Main VRServer Thread") {
try {
LogManager.initialize(activity.filesDir)

View File

@@ -3,9 +3,7 @@ package dev.slimevr.android
import android.content.Intent
import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.enableEdgeToEdge
import io.eiren.util.logging.LogManager
class AndroidJsObject {
@@ -13,10 +11,10 @@ class AndroidJsObject {
fun isThere(): Boolean = true
}
class MainActivity : AppCompatActivity() {
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize logger (doesn't re-initialize if already run)
try {
@@ -33,35 +31,6 @@ class MainActivity : AppCompatActivity() {
LogManager.info("[MainActivity] VRServer is already running, skipping initialization.")
}
// Load the web GUI web page
LogManager.info("[MainActivity] Initializing GUI WebView...")
val guiWebView = findViewById<WebView>(R.id.guiWebView)
// ## Configure for web GUI ##
// Enable debug mode
WebView.setWebContentsDebuggingEnabled(true)
// Set required features
guiWebView.settings.javaScriptEnabled = true
guiWebView.settings.domStorageEnabled = true
// TODO: Let code know it is in android, should be gone when we start using tauri
guiWebView.addJavascriptInterface(AndroidJsObject(), "__ANDROID__")
// Try fixing zoom usability
guiWebView.settings.setSupportZoom(true)
guiWebView.settings.useWideViewPort = true
guiWebView.settings.loadWithOverviewMode = true
guiWebView.invokeZoomPicker()
// Disable cache! This is all local anyway
guiWebView.settings.cacheMode = WebSettings.LOAD_NO_CACHE
guiWebView.clearCache(true)
// Load GUI page
guiWebView.loadUrl("http://127.0.0.1:34536/")
LogManager.info("[MainActivity] GUI WebView has been initialized and loaded.")
// Start a foreground service to notify the user the SlimeVR Server is running
// This also helps prevent Android from ejecting the process unexpectedly
val serviceIntent = Intent(this, ForegroundService::class.java)

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -650,26 +650,30 @@ class LegTweaks(private val skeleton: HumanSkeleton) {
var weightR = getFootPlantWeight(rightFootPosition)
// if foot trackers exist add to the weights
val leftFootYaw = isolateYaw(leftFootRotation)
if (leftFootTracker) {
weightL *= getRotationalDistanceToPlant(
leftFootRotation,
leftFootYaw,
)
}
val rightFootYaw = isolateYaw(rightFootRotation)
if (rightFootTracker) {
weightR *= getRotationalDistanceToPlant(
rightFootRotation,
rightFootYaw,
)
}
// perform the correction
leftFootRotation = leftFootRotation
.interpR(
isolateYaw(leftFootRotation),
leftFootYaw,
weightL * masterWeightL,
)
rightFootRotation = rightFootRotation
.interpR(
isolateYaw(rightFootRotation),
rightFootYaw,
weightR * masterWeightR,
)
}
@@ -805,8 +809,7 @@ class LegTweaks(private val skeleton: HumanSkeleton) {
}
// returns the amount to slerp for foot plant when foot trackers are active
private fun getRotationalDistanceToPlant(footRot: Quaternion): Float {
val footRotYaw: Quaternion = isolateYaw(footRot)
private fun getRotationalDistanceToPlant(footRot: Quaternion, footRotYaw: Quaternion): Float {
var angle = footRot.angleToR(footRotYaw)
angle = (angle / (2 * Math.PI)).toFloat()
angle = FastMath.clamp(
@@ -1005,12 +1008,9 @@ class LegTweaks(private val skeleton: HumanSkeleton) {
}
}
// remove the x and z components of the given quaternion
private fun isolateYaw(quaternion: Quaternion): Quaternion = Quaternion(
quaternion.w,
0f,
quaternion.y,
0f,
// isolate the euler yaw component of a given quaternion
private fun isolateYaw(quaternion: Quaternion): Quaternion = Quaternion.rotationAroundYAxis(
quaternion.toEulerAngles(EulerOrder.YZX).y,
)
// return a quaternion that has been rotated by the new pitch amount

View File

@@ -307,7 +307,7 @@ class TrackersHID(name: String, private val trackersConsumer: Consumer<Tracker>)
mcu_id = dataReceived[i + 6].toUByte().toInt()
// imu_id = dataReceived[i + 8].toUByte().toInt()
// mag_id = dataReceived[i + 9].toUByte().toInt()
// ushort big endian
// ushort little endian
fw_date = dataReceived[i + 11].toUByte().toInt() shl 8 or dataReceived[i + 10].toUByte().toInt()
fw_major = dataReceived[i + 12].toUByte().toInt()
fw_minor = dataReceived[i + 13].toUByte().toInt()
@@ -319,11 +319,11 @@ class TrackersHID(name: String, private val trackersConsumer: Consumer<Tracker>)
// Q15: 1 is represented as 0x7FFF, -1 as 0x8000
// The sender can use integer saturation to avoid overflow
for (j in 0..3) { // quat received as fixed Q15
// Q15 as short big endian
// Q15 as short little endian
q[j] = dataReceived[i + 2 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 2 + j * 2].toUByte().toInt()
}
for (j in 0..2) { // accel received as fixed 7, in m/s^2
// Q7 as short big endian
// Q7 as short little endian
a[j] = dataReceived[i + 10 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 10 + j * 2].toUByte().toInt()
}
}
@@ -341,7 +341,7 @@ class TrackersHID(name: String, private val trackersConsumer: Consumer<Tracker>)
q[1] = (q_buf shr 10 and 2047u).toInt()
q[2] = (q_buf shr 21 and 2047u).toInt()
for (j in 0..2) { // accel received as fixed 7, in m/s^2
// Q7 as short big endian
// Q7 as short little endian
a[j] = dataReceived[i + 9 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 9 + j * 2].toUByte().toInt()
}
rssi = dataReceived[i + 15].toUByte().toInt()
@@ -355,11 +355,11 @@ class TrackersHID(name: String, private val trackersConsumer: Consumer<Tracker>)
4 -> { // full precision quat and mag, no extra data
for (j in 0..3) { // quat received as fixed Q15
// Q15 as short big endian
// Q15 as short little endian
q[j] = dataReceived[i + 2 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 2 + j * 2].toUByte().toInt()
}
for (j in 0..2) { // mag received as fixed 10, in gauss
// Q10 as short big endian
// Q10 as short little endian
m[j] = dataReceived[i + 10 + j * 2 + 1].toInt() shl 8 or dataReceived[i + 10 + j * 2].toUByte().toInt()
}
}

View File

@@ -40,3 +40,8 @@ project(":server").projectDir = File("server")
include(":server:core")
include(":server:desktop")
include(":server:android")
try {
apply(from = "tauri.settings.gradle")
} catch(e: Exception) {
println("Couldn't enable tauri stuff")
}