diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 4d00865..d62bf52 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -3,7 +3,7 @@
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
-ko_fi: # Replace with a single Ko-fi username
+ko_fi: brunosx
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..8268dac
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,2 @@
+/.cxx
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..05c3a5f
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,41 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion '30.0.3'
+
+ defaultConfig {
+ applicationId 'com.winlator'
+ minSdkVersion 26
+ targetSdkVersion 28
+ versionCode 9
+ versionName "3.2"
+ }
+
+ buildTypes {
+ debug {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ lintOptions {
+ checkReleaseBuilds false
+ }
+
+ externalNativeBuild {
+ cmake {
+ version '3.10.2'
+ path 'src/main/cpp/CMakeLists.txt'
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.preference:preference:1.1.1'
+ implementation 'com.google.android.material:material:1.4.0'
+ implementation 'com.github.luben:zstd-jni:1.5.2-3@aar'
+ implementation 'org.tukaani:xz:1.7'
+ implementation 'org.apache.commons:commons-compress:1.20'
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..1237350
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,19 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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 *;
+#}
+
+-dontobfuscate
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a75b95b
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/assets/box64_env_vars.json b/app/src/main/assets/box64_env_vars.json
new file mode 100644
index 0000000..046ca8f
--- /dev/null
+++ b/app/src/main/assets/box64_env_vars.json
@@ -0,0 +1,11 @@
+[
+ {"name" : "BOX64_DYNAREC_SAFEFLAGS", "values" : ["0", "1", "2"], "defaultValue" : "2"},
+ {"name" : "BOX64_DYNAREC_FASTNAN", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"},
+ {"name" : "BOX64_DYNAREC_FASTROUND", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"},
+ {"name" : "BOX64_DYNAREC_X87DOUBLE", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "0"},
+ {"name" : "BOX64_DYNAREC_BIGBLOCK", "values" : ["0", "1", "2", "3"], "defaultValue" : "1"},
+ {"name" : "BOX64_DYNAREC_STRONGMEM", "values" : ["0", "1", "2", "3"], "defaultValue" : "0"},
+ {"name" : "BOX64_DYNAREC_FORWARD", "values" : ["0", "128", "256", "512", "1024"], "defaultValue" : "128"},
+ {"name" : "BOX64_DYNAREC_CALLRET", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "0"},
+ {"name" : "BOX64_DYNAREC_WAIT", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"}
+]
\ No newline at end of file
diff --git a/app/src/main/assets/box86_64/box64-0.2.2.tzst b/app/src/main/assets/box86_64/box64-0.2.2.tzst
new file mode 100644
index 0000000..e3ce4d0
Binary files /dev/null and b/app/src/main/assets/box86_64/box64-0.2.2.tzst differ
diff --git a/app/src/main/assets/box86_64/box64-0.2.5.tzst b/app/src/main/assets/box86_64/box64-0.2.5.tzst
new file mode 100644
index 0000000..fcbdac4
Binary files /dev/null and b/app/src/main/assets/box86_64/box64-0.2.5.tzst differ
diff --git a/app/src/main/assets/box86_64/box86-0.3.0.tzst b/app/src/main/assets/box86_64/box86-0.3.0.tzst
new file mode 100644
index 0000000..383fd09
Binary files /dev/null and b/app/src/main/assets/box86_64/box86-0.3.0.tzst differ
diff --git a/app/src/main/assets/box86_64/box86-0.3.2.tzst b/app/src/main/assets/box86_64/box86-0.3.2.tzst
new file mode 100644
index 0000000..fc62abc
Binary files /dev/null and b/app/src/main/assets/box86_64/box86-0.3.2.tzst differ
diff --git a/app/src/main/assets/box86_env_vars.json b/app/src/main/assets/box86_env_vars.json
new file mode 100644
index 0000000..1c988e1
--- /dev/null
+++ b/app/src/main/assets/box86_env_vars.json
@@ -0,0 +1,10 @@
+[
+ {"name" : "BOX86_DYNAREC_SAFEFLAGS", "values" : ["0", "1", "2"], "defaultValue" : "2"},
+ {"name" : "BOX86_DYNAREC_FASTNAN", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"},
+ {"name" : "BOX86_DYNAREC_FASTROUND", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"},
+ {"name" : "BOX86_DYNAREC_X87DOUBLE", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "0"},
+ {"name" : "BOX86_DYNAREC_BIGBLOCK", "values" : ["0", "1", "2"], "defaultValue" : "1"},
+ {"name" : "BOX86_DYNAREC_STRONGMEM", "values" : ["0", "1", "2", "3"], "defaultValue" : "0"},
+ {"name" : "BOX86_DYNAREC_FORWARD", "values" : ["0", "128", "256", "512", "1024"], "defaultValue" : "128"},
+ {"name" : "BOX86_DYNAREC_WAIT", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"}
+]
\ No newline at end of file
diff --git a/app/src/main/assets/dxcomponents.json b/app/src/main/assets/dxcomponents.json
new file mode 100644
index 0000000..cf11451
--- /dev/null
+++ b/app/src/main/assets/dxcomponents.json
@@ -0,0 +1,7 @@
+{
+ "direct3d" : ["d3dcompiler_33", "d3dcompiler_34", "d3dcompiler_35", "d3dcompiler_36", "d3dcompiler_37", "d3dcompiler_38", "d3dcompiler_39", "d3dcompiler_40", "d3dcompiler_41", "d3dcompiler_42", "d3dcompiler_43", "d3dcompiler_46", "d3dcompiler_47", "d3dcsx_42", "d3dcsx_43", "d3dx10", "d3dx10_33", "d3dx10_34", "d3dx10_35", "d3dx10_36", "d3dx10_37", "d3dx10_38", "d3dx10_39", "d3dx10_40", "d3dx10_41", "d3dx10_42", "d3dx10_43", "d3dx11_42", "d3dx11_43", "d3dx9_24", "d3dx9_25", "d3dx9_26", "d3dx9_27", "d3dx9_28", "d3dx9_29", "d3dx9_30", "d3dx9_31", "d3dx9_32", "d3dx9_33", "d3dx9_34", "d3dx9_35", "d3dx9_36", "d3dx9_37", "d3dx9_38", "d3dx9_39", "d3dx9_40", "d3dx9_41", "d3dx9_42", "d3dx9_43"],
+ "directsound" : ["dsound"],
+ "directmusic" : ["dmband", "dmcompos", "dmime", "dmloader", "dmscript", "dmstyle", "dmsynth", "dmusic", "dmusic32", "dswave"],
+ "directshow" : ["amstream", "qasf", "qcap", "qdvd", "qedit", "quartz"],
+ "directplay" : ["dplaysvr.exe", "dplayx", "dpmodemx", "dpnet", "dpnhpast", "dpnhupnp", "dpnsvr.exe", "dpwsockx"]
+}
\ No newline at end of file
diff --git a/app/src/main/assets/dxwrapper/cnc-ddraw/Shaders/cubic/catmull-rom-bilinear.glsl b/app/src/main/assets/dxwrapper/cnc-ddraw/Shaders/cubic/catmull-rom-bilinear.glsl
new file mode 100644
index 0000000..03d5b77
--- /dev/null
+++ b/app/src/main/assets/dxwrapper/cnc-ddraw/Shaders/cubic/catmull-rom-bilinear.glsl
@@ -0,0 +1,137 @@
+/*
+ The following code is licensed under the MIT license: https://gist.github.com/TheRealMJP/bc503b0b87b643d3505d41eab8b332ae
+ Ported from code: https://gist.github.com/TheRealMJP/c83b8c0f46b63f3a88a5986f4fa982b1
+ Samples a texture with Catmull-Rom filtering, using 9 texture fetches instead of 16.
+ See http://vec3.ca/bicubic-filtering-in-fewer-taps/ for more details
+ ATENTION: This code only work using LINEAR filter sampling set on Retroarch!
+ Modified to use 5 texture fetches
+*/
+
+#if defined(VERTEX)
+
+#if __VERSION__ >= 130
+#define COMPAT_VARYING out
+#define COMPAT_ATTRIBUTE in
+#define COMPAT_TEXTURE texture
+#else
+#define COMPAT_VARYING varying
+#define COMPAT_ATTRIBUTE attribute
+#define COMPAT_TEXTURE texture2D
+#endif
+
+#ifdef GL_ES
+#define COMPAT_PRECISION mediump
+precision COMPAT_PRECISION float;
+#else
+#define COMPAT_PRECISION
+#endif
+
+COMPAT_ATTRIBUTE vec4 VertexCoord;
+COMPAT_ATTRIBUTE vec4 COLOR;
+COMPAT_ATTRIBUTE vec4 TexCoord;
+COMPAT_VARYING vec4 COL0;
+COMPAT_VARYING vec4 TEX0;
+
+uniform mat4 MVPMatrix;
+uniform COMPAT_PRECISION int FrameDirection;
+uniform COMPAT_PRECISION int FrameCount;
+uniform COMPAT_PRECISION vec2 OutputSize;
+uniform COMPAT_PRECISION vec2 TextureSize;
+uniform COMPAT_PRECISION vec2 InputSize;
+
+void main()
+{
+ gl_Position = MVPMatrix * VertexCoord;
+ COL0 = COLOR;
+ TEX0.xy = TexCoord.xy;
+}
+
+#elif defined(FRAGMENT)
+
+#if __VERSION__ >= 130
+#define COMPAT_VARYING in
+#define COMPAT_TEXTURE texture
+out mediump vec4 FragColor;
+#else
+#define COMPAT_VARYING varying
+#define FragColor gl_FragColor
+#define COMPAT_TEXTURE texture2D
+#endif
+
+#ifdef GL_ES
+#ifdef GL_FRAGMENT_PRECISION_HIGH
+precision highp float;
+#else
+precision mediump float;
+#endif
+#define COMPAT_PRECISION mediump
+#else
+#define COMPAT_PRECISION
+#endif
+
+uniform COMPAT_PRECISION int FrameDirection;
+uniform COMPAT_PRECISION int FrameCount;
+uniform COMPAT_PRECISION vec2 OutputSize;
+uniform COMPAT_PRECISION vec2 TextureSize;
+uniform COMPAT_PRECISION vec2 InputSize;
+uniform sampler2D Texture;
+COMPAT_VARYING vec4 TEX0;
+
+// compatibility #defines
+#define Source Texture
+#define vTexCoord TEX0.xy
+
+#define SourceSize vec4(TextureSize, 1.0 / TextureSize) //either TextureSize or InputSize
+#define outsize vec4(OutputSize, 1.0 / OutputSize)
+
+void main()
+{
+ // We're going to sample a a 4x4 grid of texels surrounding the target UV coordinate. We'll do this by rounding
+ // down the sample location to get the exact center of our "starting" texel. The starting texel will be at
+ // location [1, 1] in the grid, where [0, 0] is the top left corner.
+ vec2 samplePos = vTexCoord * SourceSize.xy;
+ vec2 texPos1 = floor(samplePos - 0.5) + 0.5;
+
+ // Compute the fractional offset from our starting texel to our original sample location, which we'll
+ // feed into the Catmull-Rom spline function to get our filter weights.
+ vec2 f = samplePos - texPos1;
+
+ // Compute the Catmull-Rom weights using the fractional offset that we calculated earlier.
+ // These equations are pre-expanded based on our knowledge of where the texels will be located,
+ // which lets us avoid having to evaluate a piece-wise function.
+ vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
+ vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
+ vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f));
+ vec2 w3 = f * f * (-0.5 + 0.5 * f);
+
+ // Work out weighting factors and sampling offsets that will let us use bilinear filtering to
+ // simultaneously evaluate the middle 2 samples from the 4x4 grid.
+ vec2 w12 = w1 + w2;
+ vec2 offset12 = w2 / (w1 + w2);
+
+ // Compute the final UV coordinates we'll use for sampling the texture
+ vec2 texPos0 = texPos1 - 1.;
+ vec2 texPos3 = texPos1 + 2.;
+ vec2 texPos12 = texPos1 + offset12;
+
+ texPos0 *= SourceSize.zw;
+ texPos3 *= SourceSize.zw;
+ texPos12 *= SourceSize.zw;
+
+ float wtm = w12.x * w0.y;
+ float wml = w0.x * w12.y;
+ float wmm = w12.x * w12.y;
+ float wmr = w3.x * w12.y;
+ float wbm = w12.x * w3.y;
+
+ vec3 result = vec3(0.0f);
+
+ result += COMPAT_TEXTURE(Source, vec2(texPos12.x, texPos0.y)).rgb * wtm;
+ result += COMPAT_TEXTURE(Source, vec2(texPos0.x, texPos12.y)).rgb * wml;
+ result += COMPAT_TEXTURE(Source, vec2(texPos12.x, texPos12.y)).rgb * wmm;
+ result += COMPAT_TEXTURE(Source, vec2(texPos3.x, texPos12.y)).rgb * wmr;
+ result += COMPAT_TEXTURE(Source, vec2(texPos12.x, texPos3.y)).rgb * wbm;
+
+ FragColor = vec4(result * (1./(wtm+wml+wmm+wmr+wbm)), 1.0);
+}
+#endif
diff --git a/app/src/main/assets/dxwrapper/cnc-ddraw/Shaders/interpolation/bilinear.glsl b/app/src/main/assets/dxwrapper/cnc-ddraw/Shaders/interpolation/bilinear.glsl
new file mode 100644
index 0000000..aa7bee6
--- /dev/null
+++ b/app/src/main/assets/dxwrapper/cnc-ddraw/Shaders/interpolation/bilinear.glsl
@@ -0,0 +1,73 @@
+#if defined(VERTEX)
+
+#if __VERSION__ >= 130
+#define COMPAT_VARYING out
+#define COMPAT_ATTRIBUTE in
+#define COMPAT_TEXTURE texture
+#else
+#define COMPAT_VARYING varying
+#define COMPAT_ATTRIBUTE attribute
+#define COMPAT_TEXTURE texture2D
+#endif
+
+#ifdef GL_ES
+#define COMPAT_PRECISION mediump
+#else
+#define COMPAT_PRECISION
+#endif
+
+COMPAT_ATTRIBUTE vec4 VertexCoord;
+COMPAT_ATTRIBUTE vec4 COLOR;
+COMPAT_ATTRIBUTE vec4 TexCoord;
+COMPAT_VARYING vec4 COL0;
+COMPAT_VARYING vec4 TEX0;
+
+uniform mat4 MVPMatrix;
+uniform COMPAT_PRECISION int FrameDirection;
+uniform COMPAT_PRECISION int FrameCount;
+uniform COMPAT_PRECISION vec2 OutputSize;
+uniform COMPAT_PRECISION vec2 TextureSize;
+uniform COMPAT_PRECISION vec2 InputSize;
+
+void main()
+{
+ gl_Position = VertexCoord.x * MVPMatrix[0] + VertexCoord.y * MVPMatrix[1] + VertexCoord.z * MVPMatrix[2] + VertexCoord.w * MVPMatrix[3];
+ TEX0.xy = TexCoord.xy;
+}
+
+#elif defined(FRAGMENT)
+
+#if __VERSION__ >= 130
+#define COMPAT_VARYING in
+#define COMPAT_TEXTURE texture
+out vec4 FragColor;
+#else
+#define COMPAT_VARYING varying
+#define FragColor gl_FragColor
+#define COMPAT_TEXTURE texture2D
+#endif
+
+#ifdef GL_ES
+#ifdef GL_FRAGMENT_PRECISION_HIGH
+precision highp float;
+#else
+precision mediump float;
+#endif
+#define COMPAT_PRECISION mediump
+#else
+#define COMPAT_PRECISION
+#endif
+
+uniform COMPAT_PRECISION int FrameDirection;
+uniform COMPAT_PRECISION int FrameCount;
+uniform COMPAT_PRECISION vec2 OutputSize;
+uniform COMPAT_PRECISION vec2 TextureSize;
+uniform COMPAT_PRECISION vec2 InputSize;
+uniform sampler2D Texture;
+COMPAT_VARYING vec4 TEX0;
+
+void main()
+{
+ FragColor = COMPAT_TEXTURE(Texture, TEX0.xy);
+}
+#endif
\ No newline at end of file
diff --git a/app/src/main/assets/dxwrapper/cnc-ddraw/ddraw.ini b/app/src/main/assets/dxwrapper/cnc-ddraw/ddraw.ini
new file mode 100644
index 0000000..aa5c3ca
--- /dev/null
+++ b/app/src/main/assets/dxwrapper/cnc-ddraw/ddraw.ini
@@ -0,0 +1,901 @@
+; cnc-ddraw - https://github.com/FunkyFr3sh/cnc-ddraw
+
+[ddraw]
+; ### Optional settings ###
+; Use the following settings to adjust the look and feel to your liking
+
+
+; Stretch to custom resolution, 0 = defaults to the size game requests
+width=0
+height=0
+
+; Override the width/height settings shown above and always stretch to fullscreen
+; Note: Can be combined with 'windowed=true' to get windowed-fullscreen aka borderless mode
+fullscreen=false
+
+; Run in windowed mode rather than going fullscreen
+windowed=false
+
+; Maintain aspect ratio
+maintas=false
+
+; Windowboxing / Integer Scaling
+boxing=false
+
+; Real rendering rate, -1 = screen rate, 0 = unlimited, n = cap
+; Note: Does not have an impact on the game speed, to limit your game speed use 'maxgameticks='
+maxfps=-1
+
+; Vertical synchronization, enable if you get tearing - (Requires 'renderer=auto/opengl*/direct3d9*')
+; Note: vsync=true can fix tearing but it will cause input lag
+vsync=false
+
+; Automatic mouse sensitivity scaling
+; Note: Only works if stretching is enabled. Sensitivity will be adjusted according to the size of the window
+adjmouse=true
+
+; Preliminary libretro shader support - (Requires 'renderer=opengl*') https://github.com/libretro/glsl-shaders
+; 2x scaling example: https://imgur.com/a/kxsM1oY - 4x scaling example: https://imgur.com/a/wjrhpFV
+; You can specify a full path to a .glsl shader file here or use one of the values listed below
+; Possible values: Nearest neighbor, Bilinear, Bicubic, Lanczos, xBR-lv2
+shader=C:\ProgramData\cnc-ddraw\Shaders\cubic\catmull-rom-bilinear.glsl
+
+; Window position, -32000 = center to screen
+posX=-32000
+posY=-32000
+
+; Renderer, possible values: auto, opengl, openglcore, gdi, direct3d9, direct3d9on12 (auto = try direct3d9/opengl, fallback = gdi)
+renderer=direct3d9
+
+; Developer mode (don't lock the cursor)
+devmode=false
+
+; Show window borders in windowed mode
+border=true
+
+; Save window position/size/state on game exit and restore it automatically on next game start
+; Possible values: 0 = disabled, 1 = save to global 'ddraw' section, 2 = save to game specific section
+savesettings=1
+
+; Should the window be resizable by the user in windowed mode?
+resizable=true
+
+; Upscaling filter for the direct3d9* renderers
+; Possible values: 0 = nearest-neighbor, 1 = bilinear, 2 = bicubic, 3 = lanczos (bicubic/lanczos only support 16/32bit color depth games)
+d3d9_filter=2
+
+; Enable upscale hack for high resolution patches (Supports C&C1, Red Alert 1 and KKND Xtreme)
+vhack=false
+
+; Where should screenshots be saved
+screenshotdir=.\Screenshots\
+
+; Switch between windowed/borderless modes with alt+enter rather than windowed/fullscreen modes
+toggle_borderless=false
+
+; Switch between windowed/fullscreen upscaled modes with alt+enter rather than windowed/fullscreen modes
+toggle_upscaled=false
+
+
+
+; ### Compatibility settings ###
+; Use the following settings in case there are any issues with the game
+
+
+; Hide WM_ACTIVATEAPP and WM_NCACTIVATE messages to prevent problems on alt+tab
+noactivateapp=false
+
+; Max game ticks per second, possible values: -1 = disabled, -2 = refresh rate, 0 = emulate 60hz vblank, 1-1000 = custom game speed
+; Note: Can be used to slow down a too fast running game, fix flickering or too fast animations
+; Note: Usually one of the following values will work: 60 / 30 / 25 / 20 / 15 (lower value = slower game speed)
+maxgameticks=0
+
+; Force minimum FPS, possible values: 0 = disabled, -1 = use 'maxfps=' value, -2 = same as -1 but force full redraw, 1-1000 = custom FPS
+; Note: Set this to a low value such as 5 or 10 if some parts of the game are not being displayed (e.g. menus or loading screens)
+minfps=0
+
+; Disable fullscreen-exclusive mode for the direct3d9*/opengl* renderers
+; Note: Can be used in case some GUI elements like buttons/textboxes/videos/etc.. are invisible
+nonexclusive=false
+
+; Force CPU0 affinity, avoids crashes/freezing, *might* have a performance impact
+; Note: Disable this if the game is not running smooth or there are sound issues
+singlecpu=false
+
+; Available resolutions, possible values: 0 = Small list, 1 = Very small list, 2 = Full list
+; Note: Set this to 2 if your chosen resolution is not working or does not show up in the list
+; Note: Set this to 1 if the game is crashing on startup
+resolutions=0
+
+; Child window handling, possible values: 0 = Disabled, 1 = Display top left, 2 = Display top left + repaint, 3 = Hide
+; Note: Disables upscaling if a child window was detected (to ensure the game is fully playable, may look weird though)
+fixchilds=2
+
+; Enable one of the following settings if your cursor doesn't work properly when upscaling is enabled
+hook_peekmessage=false
+hook_getmessage=false
+
+
+; Undocumented settings - You may or may not change these (You should rather focus on the settings above)
+releasealt=false
+game_handles_close=false
+fixnotresponding=false
+hook=4
+guard_lines=200
+max_resolutions=0
+limit_bltfast=false
+lock_surfaces=false
+allow_wmactivate=false
+flipclear=false
+fixmousehook=false
+rgb555=false
+no_dinput_hook=false
+refresh_rate=0
+anti_aliased_fonts_min_size=13
+custom_width=0
+custom_height=0
+min_font_size=0
+direct3d_passthrough=false
+
+
+
+; ### Hotkeys ###
+; Use the following settings to configure your hotkeys, 0x00 = disabled
+; Virtual-Key Codes: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
+
+
+; Switch between windowed and fullscreen mode = [Alt] + ???
+keytogglefullscreen=0x0D
+
+; Maximize window = [Alt] + ???
+keytogglemaximize=0x22
+
+; Unlock cursor 1 = [Ctrl] + ???
+keyunlockcursor1=0x09
+
+; Unlock cursor 2 = [Right Alt] + ???
+keyunlockcursor2=0xA3
+
+; Screenshot
+keyscreenshot=0x2C
+
+
+
+; ### Config program settings ###
+; The following settings are for cnc-ddraw config.exe
+
+
+; cnc-ddraw config program language, possible values: auto, english, chinese, german, spanish, russian, hungarian, french, italian
+configlang=auto
+
+; cnc-ddraw config program theme, possible values: Windows10, Cobalt XEMedia
+configtheme=Windows10
+
+; Hide the 'Compatibility Settings' tab in cnc-ddraw config
+hide_compat_tab=false
+
+; Allow the users to 'Restore default settings' via cnc-ddraw config
+allow_reset=true
+
+
+
+; ### Game specific settings ###
+; The following settings override all settings shown above, section name = executable name
+
+
+; Atrox
+[Atrox]
+fixchilds=0
+allow_wmactivate=true
+
+; Atomic Bomberman
+[BM]
+maxgameticks=60
+
+; Age of Empires
+[empires]
+nonexclusive=true
+adjmouse=true
+resolutions=2
+
+; Age of Empires: The Rise of Rome
+[empiresx]
+nonexclusive=true
+adjmouse=true
+resolutions=2
+
+; Age of Empires II
+[EMPIRES2]
+nonexclusive=true
+adjmouse=true
+
+; Age of Empires II: The Conquerors
+[age2_x1]
+nonexclusive=true
+adjmouse=true
+
+; American Conquest / Cossacks
+[DMCR]
+resolutions=2
+guard_lines=300
+minfps=-2
+
+; Age of Wonders 2
+[AoW2]
+resolutions=2
+renderer=opengl
+singlecpu=false
+
+; Age of Wonders 2
+[AoW2Compat]
+resolutions=2
+renderer=opengl
+singlecpu=false
+
+; Age of Wonders 2 Config Tool
+[aow2Setup]
+resolutions=2
+
+; Age of Wonders: Shadow Magic
+[AoWSM]
+resolutions=2
+renderer=opengl
+singlecpu=false
+
+; Age of Wonders: Shadow Magic
+[AoWSMCompat]
+resolutions=2
+renderer=opengl
+singlecpu=false
+
+; Age of Wonders: Shadow Magic Config Tool
+[AoWSMSetup]
+resolutions=2
+
+; Anstoss 3
+[anstoss3]
+renderer=gdi
+adjmouse=true
+
+; Anno 1602
+[1602]
+adjmouse=true
+
+; Alien Nations
+[AN]
+adjmouse=true
+
+; Atlantis
+[ATLANTIS]
+renderer=opengl
+maxgameticks=60
+
+; Airline Tycoon Deluxe
+[AT]
+fixchilds=0
+
+; Baldur's Gate II
+; Note: 'Use 3D Acceleration' must be disabled and 'Full Screen' must be enabled in BGConfig.exe
+[BGMain]
+resolutions=2
+
+; BALDR FORCE EXE
+[BaldrForce]
+noactivateapp=true
+
+; Blade & Sword
+[comeon]
+maxgameticks=60
+fixchilds=3
+
+; Blood II - The Chosen / Shogo - Mobile Armor Division
+[Client]
+checkfile=.\SOUND.REZ
+noactivateapp=true
+
+; Carmageddon
+[CARMA95]
+noactivateapp=true
+flipclear=true
+
+; Carmageddon
+[CARM95]
+noactivateapp=true
+flipclear=true
+
+; Carmageddon 2
+[Carma2_SW]
+noactivateapp=true
+
+; Captain Claw
+[claw]
+adjmouse=true
+noactivateapp=true
+nonexclusive=true
+
+; Command & Conquer: Sole Survivor
+[SOLE]
+maxgameticks=120
+maxfps=60
+minfps=-1
+
+; Command & Conquer Gold - CnCNet
+[cnc95]
+maxfps=125
+
+; Command & Conquer Gold
+[C&C95]
+maxgameticks=120
+maxfps=60
+minfps=-1
+
+; Command & Conquer: Red Alert - CnCNet
+[ra95-spawn]
+maxfps=125
+
+; Command & Conquer: Red Alert
+[ra95]
+maxgameticks=120
+maxfps=60
+minfps=-1
+
+; Command & Conquer: Red Alert
+[ra95_Mod-Launcher]
+maxgameticks=120
+maxfps=60
+minfps=-1
+
+; Command & Conquer: Red Alert
+[ra95p]
+maxfps=60
+minfps=-1
+
+; Command & Conquer: Tiberian Sun / Command & Conquer: Red Alert 2
+[game]
+checkfile=.\blowfish.dll
+tshack=true
+noactivateapp=true
+adjmouse=true
+maxfps=60
+minfps=-1
+maintas=false
+boxing=false
+
+; Command & Conquer: Tiberian Sun Demo
+[SUN]
+noactivateapp=true
+tshack=true
+adjmouse=true
+maxfps=60
+minfps=-1
+maintas=false
+boxing=false
+
+; Command & Conquer: Tiberian Sun - CnCNet
+[ts-spawn]
+noactivateapp=true
+tshack=true
+adjmouse=true
+maxfps=60
+minfps=-1
+maintas=false
+boxing=false
+
+; Command & Conquer: Red Alert 2 - XWIS
+[ra2]
+noactivateapp=true
+tshack=true
+maxfps=60
+minfps=-1
+maintas=false
+boxing=false
+
+; Command & Conquer: Red Alert 2 - XWIS
+[Red Alert 2]
+noactivateapp=true
+tshack=true
+maxfps=60
+minfps=-1
+maintas=false
+boxing=false
+
+; Command & Conquer: Red Alert 2: Yuri's Revenge
+[gamemd]
+noactivateapp=true
+tshack=true
+maxfps=60
+minfps=-1
+maintas=false
+boxing=false
+
+; Command & Conquer: Red Alert 2: Yuri's Revenge - ?ModExe?
+[ra2md]
+noactivateapp=true
+tshack=true
+maxfps=60
+minfps=-1
+maintas=false
+boxing=false
+
+; Command & Conquer: Red Alert 2: Yuri's Revenge - CnCNet
+[gamemd-spawn]
+noactivateapp=true
+tshack=true
+maxfps=60
+minfps=-1
+maintas=false
+boxing=false
+
+; Command & Conquer: Red Alert 2: Yuri's Revenge - XWIS
+[Yuri's Revenge]
+noactivateapp=true
+tshack=true
+maxfps=60
+minfps=-1
+maintas=false
+boxing=false
+
+; Commandos
+[comandos]
+maxgameticks=-1
+
+; Commandos
+[comandos_w10]
+maxgameticks=-1
+
+; Caesar III
+[c3]
+nonexclusive=true
+adjmouse=true
+
+; Chris Sawyer's Locomotion
+[LOCO]
+adjmouse=true
+
+; Cultures 2
+[Cultures2]
+adjmouse=true
+
+; Cultures 2 MP
+[Cultures2MP]
+adjmouse=true
+
+; Close Combat 2: A Bridge Too Far
+[cc2]
+adjmouse=true
+nonexclusive=true
+
+; Close Combat 3: The Russian Front
+[cc3]
+adjmouse=true
+nonexclusive=true
+
+; Close Combat 4: The Battle of the Bulge
+[cc4]
+adjmouse=true
+nonexclusive=true
+
+; Close Combat 5: Invasion: Normandy
+[cc5]
+adjmouse=true
+nonexclusive=true
+
+; Call To Power 2
+[ctp2]
+maintas=false
+boxing=false
+
+; Corsairs Gold
+[corsairs]
+adjmouse=true
+
+; Divine Divinity
+[div]
+resolutions=2
+singlecpu=false
+
+; Dragon Throne: Battle of Red Cliffs
+[AdSanguo]
+maxgameticks=60
+noactivateapp=true
+limit_bltfast=true
+
+; Dark Reign: The Future of War
+[DKReign]
+maxgameticks=60
+
+; Dungeon Keeper 2
+[DKII]
+maxgameticks=60
+noactivateapp=true
+
+; Deadlock 2
+[DEADLOCK]
+fixchilds=0
+adjmouse=false
+maintas=false
+boxing=false
+
+; Diablo
+[Diablo]
+devmode=true
+
+; Diablo: Hellfire
+[hellfire]
+devmode=true
+
+; Escape Velocity Nova
+[EV Nova]
+hook_peekmessage=true
+rgb555=true
+keytogglefullscreen=0x46
+adjmouse=true
+
+; Economic War
+[EcoW]
+maxgameticks=60
+fixnotresponding=true
+
+; Enemy Infestation
+[EI]
+hook_getmessage=true
+
+; Fairy Tale About Father Frost, Ivan and Nastya
+[mrazik]
+guard_lines=0
+
+; Future Cop - L.A.P.D.
+[FCopLAPD]
+nonexclusive=true
+adjmouse=true
+
+; G-Police
+[GPOLICE]
+maxgameticks=60
+
+; Gangsters: Organized Crime
+[gangsters]
+adjmouse=true
+nonexclusive=true
+
+; Grand Theft Auto
+[Grand Theft Auto]
+singlecpu=false
+
+; Grand Theft Auto: London 1969
+[gta_uk]
+singlecpu=false
+
+; Grand Theft Auto: London 1961
+[Gta_61]
+singlecpu=false
+
+; Gruntz
+[GRUNTZ]
+adjmouse=true
+noactivateapp=true
+nonexclusive=true
+
+; Heroes of Might and Magic II: The Succession Wars
+[HEROES2W]
+adjmouse=true
+
+; Heroes of Might and Magic III
+[Heroes3]
+game_handles_close=true
+
+; Heroes of Might and Magic III HD Mod
+[Heroes3 HD]
+game_handles_close=true
+
+; Hard Truck: Road to Victory
+[htruck]
+maxgameticks=25
+renderer=opengl
+noactivateapp=true
+
+; Icewind Dale 2
+; Note: 'Full Screen' must be enabled in Config.exe
+; Note: 1070x602 is the lowest possible 16:9 resolution for the Widescreen patch (600/601 height will crash)
+[iwd2]
+resolutions=2
+custom_width=1070
+custom_height=602
+
+; Invictus
+[Invictus]
+adjmouse=true
+renderer=opengl
+
+; Interstate 76
+[i76]
+adjmouse=true
+
+; Infantry Online
+[infantry]
+devmode=true
+resolutions=2
+infantryhack=true
+max_resolutions=90
+
+; Jagged Alliance 2
+[ja2]
+singlecpu=false
+fixmousehook=true
+noactivateapp=true
+releasealt=true
+
+; Jagged Alliance 2: Unfinished Business
+[JA2UB]
+singlecpu=false
+fixmousehook=true
+noactivateapp=true
+releasealt=true
+
+; Jagged Alliance 2: Wildfire
+[WF6]
+singlecpu=false
+fixmousehook=true
+noactivateapp=true
+releasealt=true
+
+; Jagged Alliance 2 - UC mod
+[JA2_UC]
+singlecpu=false
+fixmousehook=true
+noactivateapp=true
+releasealt=true
+
+; Jagged Alliance 2 - Vengeance Reloaded mod
+[JA2_Vengeance]
+singlecpu=false
+fixmousehook=true
+noactivateapp=true
+releasealt=true
+
+; Jedi Knight Dark Forces 2
+[JK]
+direct3d_passthrough=true
+
+; Kings Quest 8
+[Mask]
+renderer=opengl
+
+; Konung
+[konung]
+fixchilds=0
+
+; Konung 2
+[Konung2]
+fixchilds=0
+
+; KKND Xtreme (With high resolution patch)
+[KKNDgame]
+vhack=true
+
+; KKND2: Krossfire
+[KKND2]
+noactivateapp=true
+
+; Lionheart
+[Lionheart]
+hook_peekmessage=true
+
+; Majesty Gold
+[Majesty]
+minfps=-2
+
+; Majesty Gold HD
+[MajestyHD]
+adjmouse=true
+
+; Majesty Gold HD
+[MajestyHD - Old]
+adjmouse=true
+
+; Mech Warrior 3
+[Mech3]
+nonexclusive=true
+
+; Moorhuhn 2
+[Moorhuhn2]
+releasealt=true
+
+; New Robinson
+[ROBY]
+adjmouse=true
+hook_peekmessage=true
+
+; Nox
+[NOX]
+checkfile=.\NOX.ICD
+renderer=direct3d9
+nonexclusive=false
+windowed=false
+maxgameticks=125
+
+; Nox Reloaded
+[NoxReloaded]
+maxgameticks=125
+
+; Nox GOG
+[Game/2]
+checkfile=.\nox.cfg
+maxgameticks=125
+
+; Outlaws
+[olwin]
+noactivateapp=true
+maxgameticks=60
+adjmouse=true
+renderer=gdi
+
+; Pharaoh
+[Pharaoh]
+adjmouse=true
+
+; Pax Imperia
+[Pax Imperia]
+nonexclusive=true
+
+; Railroad Tycoon II
+[RT2]
+adjmouse=true
+
+; ROAD RASH
+[RoadRash]
+adjmouse=true
+fixchilds=1
+
+; Sim Copter
+[SimCopter]
+nonexclusive=true
+
+; Settlers 3
+[s3]
+nonexclusive=true
+
+; Star Trek - Armada
+[Armada]
+armadahack=true
+nonexclusive=true
+adjmouse=true
+maintas=false
+boxing=false
+
+; Star Wars: Galactic Battlegrounds
+[battlegrounds]
+nonexclusive=true
+adjmouse=true
+
+; Star Wars: Galactic Battlegrounds: Clone Campaigns
+[battlegrounds_x1]
+nonexclusive=true
+adjmouse=true
+
+; Starcraft
+[StarCraft]
+game_handles_close=true
+
+; Space Rangers
+[Rangers]
+hook_peekmessage=true
+
+; Stronghold Crusader HD
+[Stronghold Crusader]
+resolutions=2
+stronghold_hack=true
+adjmouse=true
+
+; Stronghold Crusader Extreme HD
+[Stronghold_Crusader_Extreme]
+resolutions=2
+stronghold_hack=true
+adjmouse=true
+
+; Stronghold HD
+[Stronghold]
+resolutions=2
+stronghold_hack=true
+adjmouse=true
+
+; Sim City 3000
+[SC3]
+minfps=-2
+
+; Shadow Watch
+[sw]
+adjmouse=true
+
+; Shadow Flare
+[ShadowFlare]
+nonexclusive=true
+adjmouse=true
+maintas=false
+boxing=false
+
+; Total Annihilation (Unofficial Beta Patch v3.9.02)
+[TotalA]
+max_resolutions=32
+lock_surfaces=true
+singlecpu=false
+
+; Total Annihilation Replay Viewer (Unofficial Beta Patch v3.9.02)
+[Viewer]
+max_resolutions=32
+lock_surfaces=true
+singlecpu=false
+
+; Total Annihilation: Kingdoms
+[Kingdoms]
+game_handles_close=true
+max_resolutions=32
+
+; Three Kingdoms: Fate of the Dragon
+[sanguo]
+maxgameticks=60
+noactivateapp=true
+limit_bltfast=true
+
+; RollerCoaster Tycoon
+[rct]
+no_dinput_hook=true
+singlecpu=false
+maxfps=0
+adjmouse=true
+
+; Twisted Metal
+[TWISTED]
+nonexclusive=true
+maxgameticks=25
+minfps=5
+
+; Twisted Metal 2
+[Tm2]
+nonexclusive=true
+maxgameticks=60
+adjmouse=true
+fixchilds=1
+maintas=false
+boxing=false
+
+; Tzar: The Burden of the Crown
+; Note: Must set 'DIRECTXDEVICE=0' in 'Tzar.ini'
+[Tzar]
+adjmouse=true
+
+; Uprising
+[uprising]
+adjmouse=true
+
+; Uprising 2
+[Uprising 2]
+renderer=opengl
+adjmouse=true
+
+; Vermeer
+[vermeer]
+adjmouse=true
+vermeer_hack=true
+
+; Wizardry 8
+[Wiz8]
+fixmousehook=true
+noactivateapp=true
+releasealt=true
+
+; Worms Armageddon
+[WA]
+adjmouse=true
+width=0
+height=0
+resizable=false
+
+; War Wind
+[WW]
+minfps=-1
+
+; Zeus and Poseidon
+[Zeus]
+adjmouse=true
+
diff --git a/app/src/main/assets/dxwrapper/cnc-ddraw/ddraw.tzst b/app/src/main/assets/dxwrapper/cnc-ddraw/ddraw.tzst
new file mode 100644
index 0000000..92975eb
Binary files /dev/null and b/app/src/main/assets/dxwrapper/cnc-ddraw/ddraw.tzst differ
diff --git a/app/src/main/assets/dxwrapper/d8vk-1.0.tzst b/app/src/main/assets/dxwrapper/d8vk-1.0.tzst
new file mode 100644
index 0000000..5847fdc
Binary files /dev/null and b/app/src/main/assets/dxwrapper/d8vk-1.0.tzst differ
diff --git a/app/src/main/assets/dxwrapper/dxvk-0.96.tzst b/app/src/main/assets/dxwrapper/dxvk-0.96.tzst
new file mode 100644
index 0000000..0a17575
Binary files /dev/null and b/app/src/main/assets/dxwrapper/dxvk-0.96.tzst differ
diff --git a/app/src/main/assets/dxwrapper/dxvk-1.10.3.tzst b/app/src/main/assets/dxwrapper/dxvk-1.10.3.tzst
new file mode 100644
index 0000000..6581b37
Binary files /dev/null and b/app/src/main/assets/dxwrapper/dxvk-1.10.3.tzst differ
diff --git a/app/src/main/assets/dxwrapper/dxvk-1.5.5.tzst b/app/src/main/assets/dxwrapper/dxvk-1.5.5.tzst
new file mode 100644
index 0000000..e900429
Binary files /dev/null and b/app/src/main/assets/dxwrapper/dxvk-1.5.5.tzst differ
diff --git a/app/src/main/assets/dxwrapper/dxvk-2.2.tzst b/app/src/main/assets/dxwrapper/dxvk-2.2.tzst
new file mode 100644
index 0000000..b1b9be7
Binary files /dev/null and b/app/src/main/assets/dxwrapper/dxvk-2.2.tzst differ
diff --git a/app/src/main/assets/dxwrapper/wined3d-7.8.tzst b/app/src/main/assets/dxwrapper/wined3d-7.8.tzst
new file mode 100644
index 0000000..5f44286
Binary files /dev/null and b/app/src/main/assets/dxwrapper/wined3d-7.8.tzst differ
diff --git a/app/src/main/assets/dxwrapper/wined3d-8.14.tzst b/app/src/main/assets/dxwrapper/wined3d-8.14.tzst
new file mode 100644
index 0000000..91e5c34
Binary files /dev/null and b/app/src/main/assets/dxwrapper/wined3d-8.14.tzst differ
diff --git a/app/src/main/assets/gpu_names.json b/app/src/main/assets/gpu_names.json
new file mode 100644
index 0000000..dab847d
--- /dev/null
+++ b/app/src/main/assets/gpu_names.json
@@ -0,0 +1,57 @@
+[
+ {
+ "name" : "6800GT",
+ "deviceID" : 65,
+ "vendorID" : 4318
+ },
+ {
+ "name" : "7800GT",
+ "deviceID" : 146,
+ "vendorID" : 4318
+ },
+ {
+ "name" : "8800GT",
+ "deviceID" : 401,
+ "vendorID" : 4318
+ },
+ {
+ "name" : "9600GT",
+ "deviceID" : 1570,
+ "vendorID" : 4318
+ },
+ {
+ "name" : "9800GT",
+ "deviceID" : 1556,
+ "vendorID" : 4318
+ },
+ {
+ "name" : "GeForce 2",
+ "deviceID" : 272,
+ "vendorID" : 4318
+ },
+ {
+ "name" : "GeForce 3",
+ "deviceID" : 512,
+ "vendorID" : 4318
+ },
+ {
+ "name" : "GeForce 256",
+ "deviceID" : 256,
+ "vendorID" : 4318
+ },
+ {
+ "name" : "GTX 470",
+ "deviceID" : 1741,
+ "vendorID" : 4318
+ },
+ {
+ "name" : "GTX 1070",
+ "deviceID" : 7041,
+ "vendorID" : 4318
+ },
+ {
+ "name" : "Intel HD 4000",
+ "deviceID" : 358,
+ "vendorID" : 32902
+ }
+]
\ No newline at end of file
diff --git a/app/src/main/assets/graphics_driver/llvmpipe-23.1.6.tzst b/app/src/main/assets/graphics_driver/llvmpipe-23.1.6.tzst
new file mode 100644
index 0000000..a24b863
Binary files /dev/null and b/app/src/main/assets/graphics_driver/llvmpipe-23.1.6.tzst differ
diff --git a/app/src/main/assets/graphics_driver/turnip-23.1.6.tzst b/app/src/main/assets/graphics_driver/turnip-23.1.6.tzst
new file mode 100644
index 0000000..79c5320
Binary files /dev/null and b/app/src/main/assets/graphics_driver/turnip-23.1.6.tzst differ
diff --git a/app/src/main/assets/graphics_driver/turnip-23.3.0.tzst b/app/src/main/assets/graphics_driver/turnip-23.3.0.tzst
new file mode 100644
index 0000000..5e518e9
Binary files /dev/null and b/app/src/main/assets/graphics_driver/turnip-23.3.0.tzst differ
diff --git a/app/src/main/assets/graphics_driver/turnip-24.0.0.tzst b/app/src/main/assets/graphics_driver/turnip-24.0.0.tzst
new file mode 100644
index 0000000..0ad6a0d
Binary files /dev/null and b/app/src/main/assets/graphics_driver/turnip-24.0.0.tzst differ
diff --git a/app/src/main/assets/graphics_driver/virgl-22.1.7.tzst b/app/src/main/assets/graphics_driver/virgl-22.1.7.tzst
new file mode 100644
index 0000000..eab9709
Binary files /dev/null and b/app/src/main/assets/graphics_driver/virgl-22.1.7.tzst differ
diff --git a/app/src/main/assets/graphics_driver/zink-22.2.2.tzst b/app/src/main/assets/graphics_driver/zink-22.2.2.tzst
new file mode 100644
index 0000000..ff61d4d
Binary files /dev/null and b/app/src/main/assets/graphics_driver/zink-22.2.2.tzst differ
diff --git a/app/src/main/assets/inputcontrols/icons/0.png b/app/src/main/assets/inputcontrols/icons/0.png
new file mode 100644
index 0000000..508ae1e
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/0.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/1.png b/app/src/main/assets/inputcontrols/icons/1.png
new file mode 100644
index 0000000..5f17fd3
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/1.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/10.png b/app/src/main/assets/inputcontrols/icons/10.png
new file mode 100644
index 0000000..b2942b8
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/10.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/11.png b/app/src/main/assets/inputcontrols/icons/11.png
new file mode 100644
index 0000000..69a4a95
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/11.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/12.png b/app/src/main/assets/inputcontrols/icons/12.png
new file mode 100644
index 0000000..0b93253
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/12.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/13.png b/app/src/main/assets/inputcontrols/icons/13.png
new file mode 100644
index 0000000..0c35939
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/13.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/14.png b/app/src/main/assets/inputcontrols/icons/14.png
new file mode 100644
index 0000000..bb96019
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/14.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/2.png b/app/src/main/assets/inputcontrols/icons/2.png
new file mode 100644
index 0000000..6828c0a
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/2.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/3.png b/app/src/main/assets/inputcontrols/icons/3.png
new file mode 100644
index 0000000..8907233
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/3.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/4.png b/app/src/main/assets/inputcontrols/icons/4.png
new file mode 100644
index 0000000..a933119
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/4.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/5.png b/app/src/main/assets/inputcontrols/icons/5.png
new file mode 100644
index 0000000..b2a4652
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/5.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/6.png b/app/src/main/assets/inputcontrols/icons/6.png
new file mode 100644
index 0000000..b8cbaac
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/6.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/7.png b/app/src/main/assets/inputcontrols/icons/7.png
new file mode 100644
index 0000000..d77a563
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/7.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/8.png b/app/src/main/assets/inputcontrols/icons/8.png
new file mode 100644
index 0000000..f854efa
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/8.png differ
diff --git a/app/src/main/assets/inputcontrols/icons/9.png b/app/src/main/assets/inputcontrols/icons/9.png
new file mode 100644
index 0000000..465134c
Binary files /dev/null and b/app/src/main/assets/inputcontrols/icons/9.png differ
diff --git a/app/src/main/assets/inputcontrols/profiles/controls-1.icp b/app/src/main/assets/inputcontrols/profiles/controls-1.icp
new file mode 100644
index 0000000..f1ad644
--- /dev/null
+++ b/app/src/main/assets/inputcontrols/profiles/controls-1.icp
@@ -0,0 +1 @@
+{"id":1,"name":"RTS","cursorSpeed":1,"elements":[{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_DEL","NONE","NONE","NONE"],"scale":1,"x":0.12745098769664764,"y":0.35555556416511536,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_DOWN","NONE","NONE","NONE"],"scale":1,"x":0.12745098769664764,"y":0.7555555701255798,"toggleSwitch":false,"text":"","iconId":5},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_RIGHT","NONE","NONE","NONE"],"scale":1,"x":0.12745098769664764,"y":0.6222222447395325,"toggleSwitch":false,"text":"","iconId":4},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_LEFT","NONE","NONE","NONE"],"scale":1,"x":0.06862745434045792,"y":0.6222222447395325,"toggleSwitch":false,"text":"","iconId":2},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_UP","NONE","NONE","NONE"],"scale":1,"x":0.12745098769664764,"y":0.4888888895511627,"toggleSwitch":false,"text":"","iconId":3},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_ESC","NONE","NONE","NONE"],"scale":1,"x":0.06862745434045792,"y":0.35555556416511536,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"SQUARE","bindings":["MOUSE_RIGHT_BUTTON","NONE","NONE","NONE"],"scale":1,"x":0.8721405267715454,"y":0.7555555701255798,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_TAB","NONE","NONE","NONE"],"scale":1,"x":0.06862745434045792,"y":0.4888888895511627,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_SHIFT","NONE","NONE","NONE"],"scale":1,"x":0.06862745434045792,"y":0.7555555701255798,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_CTRL","NONE","NONE","NONE"],"scale":1,"x":0.06862745434045792,"y":0.8888888955116272,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_ALT","NONE","NONE","NONE"],"scale":1,"x":0.12745098769664764,"y":0.8888888955116272,"toggleSwitch":false,"text":"","iconId":0},{"type":"RANGE_BUTTON","shape":"CIRCLE","bindings":["NONE","NONE","NONE","NONE"],"scale":1,"x":0.1568627506494522,"y":0.08888889104127884,"toggleSwitch":false,"text":"","iconId":0,"range":"FROM_0_TO_9"},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_SPACE","NONE","NONE","NONE"],"scale":1,"x":0.8721405267715454,"y":0.8888888955116272,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_BKSP","NONE","NONE","NONE"],"scale":1,"x":0.9309640526771545,"y":0.7555555701255798,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"SQUARE","bindings":["KEY_ENTER","NONE","NONE","NONE"],"scale":1,"x":0.9309640526771545,"y":0.8888888955116272,"toggleSwitch":false,"text":"","iconId":0},{"type":"RANGE_BUTTON","shape":"CIRCLE","bindings":["NONE","NONE","NONE","NONE"],"scale":1,"x":0.843137264251709,"y":0.08888889104127884,"toggleSwitch":false,"text":"","iconId":0}]}
\ No newline at end of file
diff --git a/app/src/main/assets/inputcontrols/profiles/controls-2.icp b/app/src/main/assets/inputcontrols/profiles/controls-2.icp
new file mode 100644
index 0000000..d88d799
--- /dev/null
+++ b/app/src/main/assets/inputcontrols/profiles/controls-2.icp
@@ -0,0 +1 @@
+{"id":2,"name":"Template (12 buttons)","cursorSpeed":1,"elements":[{"type":"D_PAD","shape":"CIRCLE","bindings":["KEY_W","KEY_D","KEY_S","KEY_A"],"scale":1,"x":0.10784313827753067,"y":0.7333333492279053,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["NONE","NONE","NONE","NONE"],"scale":1,"x":0.8133170008659363,"y":0.7333333492279053,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["NONE","NONE","NONE","NONE"],"scale":1,"x":0.8721405267715454,"y":0.6000000238418579,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["NONE","NONE","NONE","NONE"],"scale":1,"x":0.8721405267715454,"y":0.8666666746139526,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["NONE","NONE","NONE","NONE"],"scale":1,"x":0.9309640526771545,"y":0.7333333492279053,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"RECT","bindings":["NONE","NONE","NONE","NONE"],"scale":1,"x":0.8235294222831726,"y":0.08888889104127884,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"RECT","bindings":["NONE","NONE","NONE","NONE"],"scale":1,"x":0.9215686321258545,"y":0.08888889104127884,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["NONE","NONE","NONE","NONE"],"scale":0.85,"x":0.06862745434045792,"y":0.4444444477558136,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"RECT","bindings":["NONE","NONE","NONE","NONE"],"scale":1,"x":0.0784313753247261,"y":0.08888889104127884,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"RECT","bindings":["NONE","NONE","NONE","NONE"],"scale":1,"x":0.1764705926179886,"y":0.08888889104127884,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["NONE","NONE","NONE","NONE"],"scale":0.85,"x":0.9309640526771545,"y":0.4444444477558136,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","bindings":["NONE","NONE","NONE","NONE"],"scale":0.85,"x":0.538807213306427,"y":0.9111111164093018,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","bindings":["NONE","NONE","NONE","NONE"],"scale":0.85,"x":0.46078431606292725,"y":0.9111111164093018,"toggleSwitch":false,"text":"","iconId":0}]}
\ No newline at end of file
diff --git a/app/src/main/assets/patches.tzst b/app/src/main/assets/patches.tzst
new file mode 100644
index 0000000..81f2596
Binary files /dev/null and b/app/src/main/assets/patches.tzst differ
diff --git a/app/src/main/assets/pulseaudio.tzst b/app/src/main/assets/pulseaudio.tzst
new file mode 100644
index 0000000..884f0ff
Binary files /dev/null and b/app/src/main/assets/pulseaudio.tzst differ
diff --git a/app/src/main/assets/wine_startmenu.json b/app/src/main/assets/wine_startmenu.json
new file mode 100644
index 0000000..7772fd2
--- /dev/null
+++ b/app/src/main/assets/wine_startmenu.json
@@ -0,0 +1,57 @@
+[
+ {
+ "name" : "Programs",
+ "children" : [
+ {
+ "name" : "Internet Explorer",
+ "path" : "C:/windows/system32/iexplore.exe"
+ },
+ {
+ "name" : "Notepad",
+ "path" : "C:/windows/notepad.exe"
+ },
+ {
+ "name" : "Wordpad",
+ "path" : "C:/windows/system32/wordpad.exe"
+ },
+ {
+ "name" : "Games",
+ "children" : [
+ {
+ "name" : "WineMine",
+ "path" : "C:/windows/system32/winemine.exe"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name" : "System Tools",
+ "children" : [
+ {
+ "name" : "Computer",
+ "path" : "C:/windows/wfm.exe"
+ },
+ {
+ "name" : "Task Manager",
+ "path" : "C:/windows/system32/taskmgr.exe"
+ },
+ {
+ "name" : "Registry Editor",
+ "path" : "C:/windows/regedit.exe"
+ },
+ {
+ "name" : "Command Prompt",
+ "path" : "C:/windows/system32/cmd.exe"
+ },
+ {
+ "name" : "Wine Mono Installer",
+ "path" : "Z:/opt/resources/winemono.bat"
+ },
+ {
+ "name" : "Wine Configuration",
+ "path" : "C:/windows/system32/winecfg.exe"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..75bc5c6
--- /dev/null
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 3.10.2)
+
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror -Wno-unused-function")
+
+add_library(winlator SHARED
+ drawable.c
+ gpu_image.c
+ sysvshared_memory.c
+ xconnector_epoll.c)
+
+target_link_libraries(winlator
+ log
+ android
+ jnigraphics
+ EGL
+ GLESv2
+ GLESv3)
\ No newline at end of file
diff --git a/app/src/main/cpp/drawable.c b/app/src/main/cpp/drawable.c
new file mode 100644
index 0000000..2f88902
--- /dev/null
+++ b/app/src/main/cpp/drawable.c
@@ -0,0 +1,228 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define WHITE 0xffffff
+#define BLACK 0x000000
+#define printf(...) __android_log_print(ANDROID_LOG_DEBUG, "System.out", __VA_ARGS__);
+
+enum GCFunction {GCF_CLEAR, GCF_AND, GCF_AND_REVERSE, GCF_COPY, GCF_AND_INVERTED, GCF_NO_OP, GCF_XOR, GCF_OR, GCF_NOR, GCF_EQUIV, GCF_INVERT, GCF_OR_REVERSE, GCF_COPY_INVERTED, GCF_OR_INVERTED, GCF_NAND, GCF_SET};
+
+static int packColor(int8_t r, int8_t g, int8_t b) {
+ return ((r & 0xff00) << 8) | (g & 0xff00) | (b >> 8);
+}
+
+static void unpackColor(int color, uint8_t *rgba) {
+ rgba[2] = (color >> 16) & 255;
+ rgba[1] = (color >> 8) & 255;
+ rgba[0] = color & 255;
+ rgba[3] = 255;
+}
+
+static int8_t getBit(uint8_t *line, int x) {
+ uint8_t mask = (1 << (x & 7));
+ line += (x >> 3);
+ return (*line & mask) ? 1 : 0;
+}
+
+static int getBitmapBytePad(int width) {
+ return ((width + 32 - 1) >> 5) << 2;
+}
+
+static int setPixelOp(int srcColor, int dstColor, enum GCFunction gcFunction) {
+ switch (gcFunction) {
+ case GCF_CLEAR :
+ return BLACK;
+ case GCF_AND :
+ return srcColor & dstColor;
+ case GCF_AND_REVERSE :
+ return srcColor & ~dstColor;
+ case GCF_COPY :
+ return srcColor;
+ case GCF_AND_INVERTED :
+ return ~srcColor & dstColor;
+ case GCF_XOR :
+ return srcColor ^ dstColor;
+ case GCF_OR :
+ return srcColor | dstColor;
+ case GCF_NOR :
+ return ~srcColor & ~dstColor;
+ case GCF_EQUIV :
+ return ~srcColor ^ dstColor;
+ case GCF_INVERT :
+ return ~dstColor;
+ case GCF_OR_REVERSE :
+ return srcColor | ~dstColor;
+ case GCF_COPY_INVERTED :
+ return ~srcColor;
+ case GCF_OR_INVERTED :
+ return ~srcColor | dstColor;
+ case GCF_NAND :
+ return ~srcColor | ~dstColor;
+ case GCF_SET :
+ return WHITE;
+ case GCF_NO_OP :
+ default:
+ return dstColor;
+ }
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_xserver_Drawable_drawBitmap(JNIEnv *env, jclass obj,
+ jshort width, jshort height, jobject srcData,
+ jobject dstData) {
+ uint8_t *srcDataAddr = (*env)->GetDirectBufferAddress(env, srcData);
+ int *dstDataAddr = (*env)->GetDirectBufferAddress(env, dstData);
+
+ int stride = getBitmapBytePad(width);
+ for (int16_t y = 0, x; y < height; y++) {
+ for (x = 0; x < width; x++) *dstDataAddr++ = getBit(srcDataAddr, x) ? WHITE : BLACK;
+ srcDataAddr += stride;
+ }
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_xserver_Drawable_copyArea(JNIEnv *env, jclass obj, jshort srcX,
+ jshort srcY, jshort dstX, jshort dstY,
+ jshort width, jshort height, jshort srcStride,
+ jshort dstStride, jobject srcData,
+ jobject dstData) {
+ uint8_t *srcDataAddr = (*env)->GetDirectBufferAddress(env, srcData);
+ uint8_t *dstDataAddr = (*env)->GetDirectBufferAddress(env, dstData);
+ int64_t srcLength = (*env)->GetDirectBufferCapacity(env, srcData);
+ int64_t dstLength = (*env)->GetDirectBufferCapacity(env, dstData);
+
+ if (srcX != 0 || srcY != 0 || dstX != 0 || dstY != 0 || srcLength != dstLength) {
+ int copyAmount = width * 4;
+ for (int16_t y = 0; y < height; y++) {
+ memcpy(dstDataAddr + (dstX + (y + dstY) * dstStride) * 4, srcDataAddr + (srcX + (y + srcY) * srcStride) * 4, copyAmount);
+ }
+ }
+ else memcpy(dstDataAddr, srcDataAddr, dstLength);
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_xserver_Drawable_copyAreaOp(JNIEnv *env, jclass obj, jshort srcX,
+ jshort srcY, jshort dstX, jshort dstY,
+ jshort width, jshort height, jshort srcStride,
+ jshort dstStride, jobject srcData,
+ jobject dstData, int gcFunction) {
+ uint8_t *srcDataAddr = (*env)->GetDirectBufferAddress(env, srcData);
+ uint8_t *dstDataAddr = (*env)->GetDirectBufferAddress(env, dstData);
+
+ for (int16_t y = 0; y < height; y++) {
+ for (int16_t x = 0; x < width; x++) {
+ int i = (x + srcX + (y + srcY) * srcStride) * 4;
+ int j = (x + dstX + (y + dstY) * dstStride) * 4;
+ int srcColor = (srcDataAddr[i+0] << 16) | (srcDataAddr[i+1] << 8) | srcDataAddr[i+2];
+ int dstColor = (dstDataAddr[j+0] << 16) | (dstDataAddr[j+1] << 8) | dstDataAddr[j+2];
+
+ dstColor = setPixelOp(srcColor, dstColor, gcFunction);
+
+ dstDataAddr[j+0] = (dstColor >> 16) & 0xff;
+ dstDataAddr[j+1] = (dstColor >> 8) & 0xff;
+ dstDataAddr[j+2] = dstColor & 0xff;
+ }
+ }
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_xserver_Drawable_fillRect(JNIEnv *env, jclass obj, jshort x, jshort y,
+ jshort width, jshort height, jint color, jshort stride,
+ jobject data) {
+ uint8_t *dataAddr = (*env)->GetDirectBufferAddress(env, data);
+
+ uint8_t rgba[4];
+ unpackColor(color, rgba);
+
+ int rowSize = width * 4;
+ uint8_t *row = malloc(rowSize);
+
+ for (int i = 0; i < rowSize; i += 4) memcpy(row + i, rgba, 4);
+ for (int16_t i = 0; i < height; i++) {
+ memcpy(dataAddr + (x + (i + y) * stride) * 4, row, rowSize);
+ }
+
+ free(row);
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_xserver_Drawable_drawLine(JNIEnv *env, jclass obj, jshort x0, jshort y0,
+ jshort x1, jshort y1, jint color, jshort lineWidth,
+ jshort stride, jobject data) {
+ uint8_t *dataAddr = (*env)->GetDirectBufferAddress(env, data);
+ int dx = abs(x1-x0);
+ int dy = -abs(y1-y0);
+ int8_t sx = x0 < x1 ? 1 : -1;
+ int8_t sy = y0 < y1 ? 1 : -1;
+ int e1 = dx + dy, e2;
+
+ uint8_t rgba[4];
+ unpackColor(color, rgba);
+
+ int rowSize = lineWidth * 4;
+ uint8_t *row = malloc(lineWidth * 4);
+
+ int16_t i;
+ for (i = 0; i < rowSize; i += 4) memcpy(row + i, rgba, 4);
+
+ while (true) {
+ for (i = 0; i < lineWidth; i++) memcpy(dataAddr + (x0 + (i + y0) * stride) * 4, row, rowSize);
+ if (x0 == x1 && y0 == y1) break;
+
+ e2 = e1 * 2;
+ if (e2 >= dy) {
+ e1 += dy;
+ x0 += sx;
+ }
+ if (e2 <= dx) {
+ e1 += dx;
+ y0 += sy;
+ }
+ }
+
+ free(row);
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_xserver_Drawable_drawAlphaMaskedBitmap(JNIEnv *env, jclass obj,
+ jbyte foreRed, jbyte foreGreen,
+ jbyte foreBlue, jbyte backRed,
+ jbyte backGreen, jbyte backBlue,
+ jobject srcData, jobject maskData,
+ jobject dstData) {
+ int *srcDataAddr = (*env)->GetDirectBufferAddress(env, srcData);
+ int *maskDataAddr = (*env)->GetDirectBufferAddress(env, maskData);
+ int *dstDataAddr = (*env)->GetDirectBufferAddress(env, dstData);
+
+ int foreColor = packColor(foreRed, foreGreen, foreBlue);
+ int backColor = packColor(backRed, backGreen, backBlue);
+
+ int dstLength = (*env)->GetDirectBufferCapacity(env, dstData) / 4;
+ for (int i = 0; i < dstLength; i++) {
+ dstDataAddr[i] = maskDataAddr[i] == WHITE ? (srcDataAddr[i] == WHITE ? foreColor : backColor) | 0xff000000 : 0x00000000;
+ }
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_xserver_Drawable_fromBitmap(JNIEnv *env, jclass obj, jobject bitmap,
+ jobject data) {
+ char *dataAddr = (*env)->GetDirectBufferAddress(env, data);
+
+ AndroidBitmapInfo info;
+ uint8_t *pixels;
+
+ AndroidBitmap_getInfo(env, bitmap, &info);
+ AndroidBitmap_lockPixels(env, bitmap, (void**)&pixels);
+
+ for (int i = 0, size = info.width * info.height * 4; i < size; i++) {
+ memcpy(dataAddr + i, pixels + i, 4);
+ }
+
+ AndroidBitmap_unlockPixels(env, bitmap);
+}
\ No newline at end of file
diff --git a/app/src/main/cpp/gpu_image.c b/app/src/main/cpp/gpu_image.c
new file mode 100644
index 0000000..757b5d0
--- /dev/null
+++ b/app/src/main/cpp/gpu_image.c
@@ -0,0 +1,99 @@
+#include
+#include
+#include
+
+#define EGL_EGLEXT_PROTOTYPES
+#define GL_GLEXT_PROTOTYPES
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define printf(...) __android_log_print(ANDROID_LOG_DEBUG, "System.out", __VA_ARGS__);
+#define HAL_PIXEL_FORMAT_BGRA_8888 5
+
+EGLImageKHR createImageKHR(AHardwareBuffer* hardwareBuffer, int textureId) {
+ const EGLint attribList[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE};
+
+ AHardwareBuffer_acquire(hardwareBuffer);
+
+ EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID(hardwareBuffer);
+ if (!clientBuffer) return NULL;
+
+ EGLDisplay eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ EGLImageKHR imageKHR = eglCreateImageKHR(eglDisplay, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, clientBuffer, attribList);
+ if (!imageKHR) return NULL;
+
+ glBindTexture(GL_TEXTURE_2D, textureId);
+ glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, imageKHR);
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ return imageKHR;
+}
+
+AHardwareBuffer* createHardwareBuffer(int width, int height) {
+ AHardwareBuffer_Desc buffDesc = {};
+ buffDesc.width = width;
+ buffDesc.height = height;
+ buffDesc.layers = 1;
+ buffDesc.usage = AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN;
+ buffDesc.format = HAL_PIXEL_FORMAT_BGRA_8888;
+
+ AHardwareBuffer *hardwareBuffer = NULL;
+ AHardwareBuffer_allocate(&buffDesc, &hardwareBuffer);
+
+ return hardwareBuffer;
+}
+
+JNIEXPORT jlong JNICALL
+Java_com_winlator_renderer_GPUImage_createHardwareBuffer(JNIEnv *env, jclass obj, jshort width,
+ jshort height) {
+ return (jlong)createHardwareBuffer(width, height);
+}
+
+JNIEXPORT jlong JNICALL
+Java_com_winlator_renderer_GPUImage_createImageKHR(JNIEnv *env, jclass obj,
+ jlong hardwareBufferPtr, jint textureId) {
+ return (jlong)createImageKHR((AHardwareBuffer*)hardwareBufferPtr, textureId);
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_renderer_GPUImage_destroyHardwareBuffer(JNIEnv *env, jclass obj,
+ jlong hardwareBufferPtr) {
+ AHardwareBuffer* hardwareBuffer = (AHardwareBuffer*)hardwareBufferPtr;
+ if (hardwareBuffer) {
+ AHardwareBuffer_unlock(hardwareBuffer, NULL);
+ AHardwareBuffer_release(hardwareBuffer);
+ }
+}
+
+JNIEXPORT jobject JNICALL
+Java_com_winlator_renderer_GPUImage_lockHardwareBuffer(JNIEnv *env, jclass obj,
+ jlong hardwareBufferPtr) {
+ AHardwareBuffer* hardwareBuffer = (AHardwareBuffer*)hardwareBufferPtr;
+ void *virtualAddr;
+ AHardwareBuffer_lock(hardwareBuffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, -1, NULL, &virtualAddr);
+
+ AHardwareBuffer_Desc buffDesc;
+ AHardwareBuffer_describe(hardwareBuffer, &buffDesc);
+
+ jclass cls = (*env)->GetObjectClass(env, obj);
+ jmethodID setStride = (*env)->GetMethodID(env, cls, "setStride", "(S)V");
+ (*env)->CallVoidMethod(env, obj, setStride, (jshort)buffDesc.stride);
+
+ jlong size = buffDesc.stride * buffDesc.height * 4;
+ return (*env)->NewDirectByteBuffer(env, virtualAddr, size);
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_renderer_GPUImage_destroyImageKHR(JNIEnv *env, jclass obj, jlong imageKHRPtr) {
+
+ EGLImageKHR imageKHR = (EGLImageKHR)imageKHRPtr;
+ if (imageKHR) {
+ EGLDisplay eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ eglDestroyImageKHR(eglDisplay, imageKHR);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/cpp/sysvshared_memory.c b/app/src/main/cpp/sysvshared_memory.c
new file mode 100644
index 0000000..b9749cd
--- /dev/null
+++ b/app/src/main/cpp/sysvshared_memory.c
@@ -0,0 +1,88 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define __u32 uint32_t
+#include
+
+#define printf(...) __android_log_print(ANDROID_LOG_DEBUG, "System.out", __VA_ARGS__);
+
+static int ashmemCreateRegion(const char* name, int64_t size) {
+ int fd = open("/dev/ashmem", O_RDWR);
+ if (fd < 0) return -1;
+
+ char nameBuffer[ASHMEM_NAME_LEN] = {0};
+ strncpy(nameBuffer, name, sizeof(nameBuffer));
+ nameBuffer[sizeof(nameBuffer) - 1] = 0;
+
+ int ret = ioctl(fd, ASHMEM_SET_NAME, nameBuffer);
+ if (ret < 0) goto error;
+
+ ret = ioctl(fd, ASHMEM_SET_SIZE, size);
+ if (ret < 0) goto error;
+
+ return fd;
+error:
+ close(fd);
+ return -1;
+}
+
+static int memfd_create(const char *name, unsigned int flags) {
+#ifdef __NR_memfd_create
+ return syscall(__NR_memfd_create, name, flags);
+#else
+ return -1;
+#endif
+}
+
+JNIEXPORT jint JNICALL
+Java_com_winlator_sysvshm_SysVSharedMemory_ashmemCreateRegion(JNIEnv *env, jobject obj, jint index,
+ jlong size) {
+ char name[32];
+ sprintf(name, "sysvshm-%d", index);
+ return ashmemCreateRegion(name, size);
+}
+
+JNIEXPORT jobject JNICALL
+Java_com_winlator_sysvshm_SysVSharedMemory_mapSHMSegment(JNIEnv *env, jobject obj, jint fd, jlong size, jint offset, jboolean readonly) {
+ char *data = mmap(NULL, size, readonly ? PROT_READ : PROT_WRITE | PROT_READ, MAP_SHARED, fd, offset);
+ if (data == MAP_FAILED) return NULL;
+ return (*env)->NewDirectByteBuffer(env, data, size);
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_sysvshm_SysVSharedMemory_unmapSHMSegment(JNIEnv *env, jobject obj, jobject data,
+ jlong size) {
+ char *dataAddr = (*env)->GetDirectBufferAddress(env, data);
+ munmap(dataAddr, size);
+}
+
+JNIEXPORT jint JNICALL
+Java_com_winlator_sysvshm_SysVSharedMemory_createMemoryFd(JNIEnv *env, jclass obj, jstring name,
+ jint size) {
+ const char *namePtr = (*env)->GetStringUTFChars(env, name, 0);
+
+ int fd = memfd_create(namePtr, MFD_ALLOW_SEALING);
+ (*env)->ReleaseStringUTFChars(env, name, namePtr);
+
+ if (fd < 0) return -1;
+
+ int res = ftruncate(fd, size);
+ if (res < 0) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
\ No newline at end of file
diff --git a/app/src/main/cpp/xconnector_epoll.c b/app/src/main/cpp/xconnector_epoll.c
new file mode 100644
index 0000000..126fab3
--- /dev/null
+++ b/app/src/main/cpp/xconnector_epoll.c
@@ -0,0 +1,215 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define printf(...) __android_log_print(ANDROID_LOG_DEBUG, "System.out", __VA_ARGS__);
+#define MAX_EVENTS 10
+#define MAX_FDS 32
+
+struct epoll_event events[MAX_EVENTS];
+
+JNIEXPORT jint JNICALL
+Java_com_winlator_xconnector_XConnectorEpoll_createAFUnixSocket(JNIEnv *env, jobject obj,
+ jstring path) {
+ int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd < 0) return -1;
+
+ struct sockaddr_un serverAddr;
+ memset(&serverAddr, 0, sizeof(serverAddr));
+ serverAddr.sun_family = AF_LOCAL;
+
+ const char *pathPtr = (*env)->GetStringUTFChars(env, path, 0);
+
+ int addrLength = sizeof(sa_family_t) + strlen(pathPtr);
+ strncpy(serverAddr.sun_path, pathPtr, sizeof(serverAddr.sun_path) - 1);
+
+ (*env)->ReleaseStringUTFChars(env, path, pathPtr);
+
+ unlink(serverAddr.sun_path);
+ if (bind(fd, (struct sockaddr*) &serverAddr, addrLength) < 0) goto error;
+ if (listen(fd, MAX_EVENTS) < 0) goto error;
+
+ return fd;
+ error:
+ close(fd);
+ return -1;
+}
+
+JNIEXPORT jint JNICALL
+Java_com_winlator_xconnector_XConnectorEpoll_createEpollFd(JNIEnv *env, jobject obj) {
+ return epoll_create(MAX_EVENTS);
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_xconnector_XConnectorEpoll_closeFd(JNIEnv *env, jobject obj, jint fd) {
+ close(fd);
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_winlator_xconnector_XConnectorEpoll_doEpollIndefinitely(JNIEnv *env, jobject obj,
+ jint epollFd, jint serverFd,
+ jboolean addClientToEpoll) {
+ jclass cls = (*env)->GetObjectClass(env, obj);
+ jmethodID handleNewConnection = (*env)->GetMethodID(env, cls, "handleNewConnection", "(I)V");
+ jmethodID handleExistingConnection = (*env)->GetMethodID(env, cls, "handleExistingConnection", "(I)V");
+
+ int numFds = epoll_wait(epollFd, events, MAX_EVENTS, -1);
+ for (int i = 0; i < numFds; i++) {
+ if (events[i].data.fd == serverFd) {
+ int clientFd = accept(serverFd, NULL, NULL);
+ if (clientFd >= 0) {
+ if (addClientToEpoll) {
+ struct epoll_event event;
+ event.data.fd = clientFd;
+ event.events = EPOLLIN;
+
+ if (epoll_ctl(epollFd, EPOLL_CTL_ADD, clientFd, &event) >= 0) {
+ (*env)->CallVoidMethod(env, obj, handleNewConnection, clientFd);
+ }
+ }
+ else (*env)->CallVoidMethod(env, obj, handleNewConnection, clientFd);
+ }
+ }
+ else if (events[i].events & EPOLLIN) {
+ (*env)->CallVoidMethod(env, obj, handleExistingConnection, events[i].data.fd);
+ }
+ }
+
+ return numFds >= 0;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_winlator_xconnector_XConnectorEpoll_addFdToEpoll(JNIEnv *env, jobject obj,
+ jint epollFd,
+ jint fd) {
+ struct epoll_event event;
+ event.data.fd = fd;
+ event.events = EPOLLIN;
+ if (epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &event) < 0) return JNI_FALSE;
+ return JNI_TRUE;
+}
+
+JNIEXPORT void JNICALL
+Java_com_winlator_xconnector_XConnectorEpoll_removeFdFromEpoll(JNIEnv *env, jobject obj,
+ jint epollFd, jint fd) {
+ epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, NULL);
+}
+
+JNIEXPORT jint JNICALL
+Java_com_winlator_xconnector_ClientSocket_read(JNIEnv *env, jobject obj, jint fd, jobject data,
+ jint offset, jint length) {
+ char *dataAddr = (*env)->GetDirectBufferAddress(env, data);
+ return read(fd, dataAddr + offset, length);
+}
+
+JNIEXPORT jint JNICALL
+Java_com_winlator_xconnector_ClientSocket_write(JNIEnv *env, jobject obj, jint fd, jobject data,
+ jint length) {
+ char *dataAddr = (*env)->GetDirectBufferAddress(env, data);
+ return write(fd, dataAddr, length);
+}
+
+JNIEXPORT jint JNICALL
+Java_com_winlator_xconnector_XConnectorEpoll_createEventFd(JNIEnv *env, jobject obj) {
+ return eventfd(0, EFD_NONBLOCK);
+}
+
+JNIEXPORT jint JNICALL
+Java_com_winlator_xconnector_ClientSocket_recvAncillaryMsg(JNIEnv *env, jobject obj, jint clientFd, jobject data,
+ jint offset, jint length) {
+ char *dataAddr = (*env)->GetDirectBufferAddress(env, data);
+
+ struct iovec iovmsg = {.iov_base = dataAddr + offset, .iov_len = length};
+ struct {
+ struct cmsghdr align;
+ int fds[MAX_FDS];
+ } ctrlmsg;
+
+ struct msghdr msg = {
+ .msg_name = NULL,
+ .msg_namelen = 0,
+ .msg_iov = &iovmsg,
+ .msg_iovlen = 1,
+ .msg_control = &ctrlmsg,
+ .msg_controllen = sizeof(struct cmsghdr) + MAX_FDS * sizeof(int)
+ };
+
+ int size = recvmsg(clientFd, &msg, 0);
+
+ if (size >= 0) {
+ struct cmsghdr *cmsg;
+ for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+ if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
+ int numFds = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
+ if (numFds > 0) {
+ jclass cls = (*env)->GetObjectClass(env, obj);
+ jmethodID addAncillaryFd = (*env)->GetMethodID(env, cls, "addAncillaryFd", "(I)V");
+ for (int i = 0; i < numFds; i++) {
+ int ancillaryFd = ((int*)CMSG_DATA(cmsg))[i];
+ (*env)->CallVoidMethod(env, obj, addAncillaryFd, ancillaryFd);
+ }
+ }
+ }
+ }
+ }
+ return size;
+}
+
+JNIEXPORT jint JNICALL
+Java_com_winlator_xconnector_ClientSocket_sendAncillaryMsg(JNIEnv *env, jobject obj, jint clientFd,
+ jobject data, jint length, jint ancillaryFd) {
+ char *dataAddr = (*env)->GetDirectBufferAddress(env, data);
+
+ struct iovec iovmsg = {.iov_base = dataAddr, .iov_len = length};
+ struct {
+ struct cmsghdr align;
+ int fds[1];
+ } ctrlmsg;
+
+ struct msghdr msg = {
+ .msg_name = NULL,
+ .msg_namelen = 0,
+ .msg_iov = &iovmsg,
+ .msg_iovlen = 1,
+ .msg_flags = 0,
+ .msg_control = &ctrlmsg,
+ .msg_controllen = sizeof(struct cmsghdr) + sizeof(int)
+ };
+
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = msg.msg_controllen;
+ ((int*)CMSG_DATA(cmsg))[0] = ancillaryFd;
+
+ return sendmsg(clientFd, &msg, 0);
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_winlator_xconnector_XConnectorEpoll_waitForSocketRead(JNIEnv *env, jobject obj, jint clientFd, jint shutdownFd) {
+ struct pollfd pfds[2];
+ pfds[0].fd = clientFd;
+ pfds[0].events = POLLIN;
+
+ pfds[1].fd = shutdownFd;
+ pfds[1].events = POLLIN;
+
+ int res = poll(pfds, 2, -1);
+ if (res < 0 || (pfds[1].revents & POLLIN)) return JNI_FALSE;
+
+ if (pfds[0].revents & POLLIN) {
+ jclass cls = (*env)->GetObjectClass(env, obj);
+ jmethodID handleExistingConnection = (*env)->GetMethodID(env, cls, "handleExistingConnection", "(I)V");
+ (*env)->CallVoidMethod(env, obj, handleExistingConnection, clientFd);
+ }
+ return JNI_TRUE;
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/ContainerDetailFragment.java b/app/src/main/java/com/winlator/ContainerDetailFragment.java
new file mode 100644
index 0000000..4ea1bdd
--- /dev/null
+++ b/app/src/main/java/com/winlator/ContainerDetailFragment.java
@@ -0,0 +1,522 @@
+package com.winlator;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceManager;
+
+import com.winlator.box86_64.Box86_64Preset;
+import com.winlator.box86_64.Box86_64PresetManager;
+import com.winlator.container.Container;
+import com.winlator.container.ContainerManager;
+import com.winlator.contentdialog.AddEnvVarDialog;
+import com.winlator.core.AppUtils;
+import com.winlator.core.Callback;
+import com.winlator.core.EnvVars;
+import com.winlator.core.FileUtils;
+import com.winlator.core.PreloaderDialog;
+import com.winlator.core.StringUtils;
+import com.winlator.core.WineInfo;
+import com.winlator.core.WineRegistryEditor;
+import com.winlator.core.WineUtils;
+import com.winlator.widget.CPUListView;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+public class ContainerDetailFragment extends Fragment {
+ private ContainerManager manager;
+ private final int containerId;
+ private Container container;
+ private PreloaderDialog preloaderDialog;
+ private JSONArray gpuNames;
+ private Callback openDirectoryCallback;
+ private final String[] defaultDLLOverrides = {"d3d8", "d3d9", "d3d10", "d3d10_1", "d3d10core", "d3d11", "d3d12", "d3d12core", "ddraw", "dxgi", "wined3d"};
+
+ public ContainerDetailFragment() {
+ this(0);
+ }
+
+ public ContainerDetailFragment(int containerId) {
+ this.containerId = containerId;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(false);
+ preloaderDialog = new PreloaderDialog(getActivity());
+
+ try {
+ gpuNames = new JSONArray(FileUtils.readString(getContext(), "gpu_names.json"));
+ }
+ catch (JSONException e) {}
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == MainActivity.OPEN_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+ if (data != null) {
+ String path = FileUtils.getFilePathFromUri(data.getData());
+ if (path != null && openDirectoryCallback != null) openDirectoryCallback.call(path);
+ }
+ openDirectoryCallback = null;
+ }
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle(container != null ? R.string.edit_container : R.string.new_container);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup root, @Nullable Bundle savedInstanceState) {
+ final Context context = getContext();
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ final View view = inflater.inflate(R.layout.container_detail_fragment, root, false);
+ manager = new ContainerManager(context);
+ container = containerId > 0 ? manager.getContainerById(containerId) : null;
+ boolean editContainer = container != null;
+
+ final EditText etName = view.findViewById(R.id.ETName);
+
+ if (editContainer) {
+ etName.setText(container.getName());
+ }
+ else etName.setText(getString(R.string.container)+"-"+manager.getNextContainerId());
+
+ final ArrayList wineInfos = WineUtils.getInstalledWineInfos(context);
+ final Spinner sWineVersion = view.findViewById(R.id.SWineVersion);
+ if (wineInfos.size() > 1) {
+ sWineVersion.setEnabled(!editContainer);
+ view.findViewById(R.id.LLWineVersion).setVisibility(View.VISIBLE);
+ sWineVersion.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, wineInfos));
+ if (editContainer) AppUtils.setSpinnerSelectionFromValue(sWineVersion, WineInfo.fromIdentifier(getContext(), container.getWineVersion()).toString());
+ }
+
+ loadScreenSizeSpinner(view, editContainer ? container.getScreenSize() : Container.DEFAULT_SCREEN_SIZE);
+
+ final Spinner sGraphicsDriver = view.findViewById(R.id.SGraphicsDriver);
+ final Spinner sDXWrapper = view.findViewById(R.id.SDXWrapper);
+ loadGraphicsDriverSpinner(sGraphicsDriver, sDXWrapper);
+
+ view.findViewById(R.id.BTHelpDXWrapper).setOnClickListener((v) -> AppUtils.showHelpBox(context, v, R.string.dxwrapper_help_content));
+
+ Spinner sAudioDriver = view.findViewById(R.id.SAudioDriver);
+ AppUtils.setSpinnerSelectionFromIdentifier(sAudioDriver, editContainer ? container.getAudioDriver() : Container.DEFAULT_AUDIO_DRIVER);
+
+ final CheckBox cbShowFPS = view.findViewById(R.id.CBShowFPS);
+ cbShowFPS.setChecked(editContainer && container.isShowFPS());
+
+ final CheckBox cbStopServicesOnStartup = view.findViewById(R.id.CBStopServicesOnStartup);
+ cbStopServicesOnStartup.setChecked(editContainer && container.isStopServicesOnStartup());
+
+ final Spinner sBox86Preset = view.findViewById(R.id.SBox86Preset);
+ Box86_64PresetManager.loadSpinner("box86", sBox86Preset, editContainer ? container.getBox86Preset() : preferences.getString("box86_preset", Box86_64Preset.COMPATIBILITY));
+
+ final Spinner sBox64Preset = view.findViewById(R.id.SBox64Preset);
+ Box86_64PresetManager.loadSpinner("box64", sBox64Preset, editContainer ? container.getBox64Preset() : preferences.getString("box64_preset", Box86_64Preset.COMPATIBILITY));
+
+ final CPUListView cpuListView = view.findViewById(R.id.CPUListView);
+ if (editContainer) cpuListView.setCheckedCPUList(container.getCPUList());
+
+ createWineRegistryKeysTab(view);
+ createDXComponentsTab(view);
+ createEnvVarsTab(view);
+ createDrivesTab(view);
+
+ view.findViewById(R.id.BTAddEnvVar).setOnClickListener((v) -> (new AddEnvVarDialog(context, view)).show());
+ AppUtils.setupTabLayout(view, R.id.TabLayout, R.id.LLTabWineRegistryKeys, R.id.LLTabDXComponents, R.id.LLTabEnvVars, R.id.LLTabDrives, R.id.LLTabAdvanced);
+
+ view.findViewById(R.id.BTConfirm).setOnClickListener((v) -> {
+ try {
+ String name = etName.getText().toString();
+ String screenSize = getScreenSize(view);
+ String envVars = getEnvVars(view);
+ String graphicsDriver = StringUtils.parseIdentifier(sGraphicsDriver.getSelectedItem());
+ String dxwrapper = StringUtils.parseIdentifier(sDXWrapper.getSelectedItem());
+ String audioDriver = StringUtils.parseIdentifier(sAudioDriver.getSelectedItem());
+ String dxcomponents = getDXComponents(view);
+ String drives = getDrives(view);
+ boolean showFPS = cbShowFPS.isChecked();
+ String cpuList = cpuListView.getCheckedCPUListAsString();
+ boolean stopServicesOnStartup = cbStopServicesOnStartup.isChecked();
+ String box86Preset = Box86_64PresetManager.getSpinnerSelectedId(sBox86Preset);
+ String box64Preset = Box86_64PresetManager.getSpinnerSelectedId(sBox64Preset);
+
+ if (editContainer) {
+ container.setName(name);
+ container.setScreenSize(screenSize);
+ container.setEnvVars(envVars);
+ container.setCPUList(cpuList);
+ container.setGraphicsDriver(graphicsDriver);
+ container.setDXWrapper(dxwrapper);
+ container.setAudioDriver(audioDriver);
+ container.setDXComponents(dxcomponents);
+ container.setDrives(drives);
+ container.setShowFPS(showFPS);
+ container.setStopServicesOnStartup(stopServicesOnStartup);
+ container.setBox86Preset(box86Preset);
+ container.setBox64Preset(box64Preset);
+ container.saveData();
+ saveWineRegistryKeys(view);
+ getActivity().onBackPressed();
+ }
+ else {
+ JSONObject data = new JSONObject();
+ data.put("name", name);
+ data.put("screenSize", screenSize);
+ data.put("envVars", envVars);
+ data.put("cpuList", cpuList);
+ data.put("graphicsDriver", graphicsDriver);
+ data.put("dxwrapper", dxwrapper);
+ data.put("audioDriver", audioDriver);
+ data.put("dxcomponents", dxcomponents);
+ data.put("drives", drives);
+ data.put("showFPS", showFPS);
+ data.put("stopServicesOnStartup", stopServicesOnStartup);
+ data.put("box86Preset", box86Preset);
+ data.put("box64Preset", box64Preset);
+
+ if (wineInfos.size() > 1) {
+ data.put("wineVersion", wineInfos.get(sWineVersion.getSelectedItemPosition()).identifier());
+ }
+
+ preloaderDialog.show(R.string.creating_container);
+ manager.createContainerAsync(data, (container) -> {
+ if (container != null) {
+ this.container = container;
+ saveWineRegistryKeys(view);
+ }
+ preloaderDialog.close();
+ getActivity().onBackPressed();
+ });
+ }
+ }
+ catch (JSONException e) {}
+ });
+ return view;
+ }
+
+ private void createEnvVarsTab(View view) {
+ final LinearLayout parent = view.findViewById(R.id.LLEnvVars);
+ final View emptyTextView = view.findViewById(R.id.TVEnvVarsEmptyText);
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ final EnvVars envVars = new EnvVars(container != null ? container.getEnvVars() : Container.DEFAULT_ENV_VARS);
+
+ for (String name : envVars) {
+ final View itemView = inflater.inflate(R.layout.env_vars_list_item, parent, false);
+ ((TextView)itemView.findViewById(R.id.TextView)).setText(name);
+ ((EditText)itemView.findViewById(R.id.EditText)).setText(envVars.get(name));
+
+ itemView.findViewById(R.id.BTRemove).setOnClickListener((v) -> {
+ parent.removeView(itemView);
+ if (parent.getChildCount() == 0) emptyTextView.setVisibility(View.VISIBLE);
+ });
+ parent.addView(itemView);
+ }
+
+ if (envVars.isEmpty()) emptyTextView.setVisibility(View.VISIBLE);
+ }
+
+ private void saveWineRegistryKeys(View view) {
+ File userRegFile = new File(container.getRootDir(), ".wine/user.reg");
+ try (WineRegistryEditor registryEditor = new WineRegistryEditor(userRegFile)) {
+ Spinner sCSMT = view.findViewById(R.id.SCSMT);
+ registryEditor.setDwordValue("Software\\Wine\\Direct3D", "csmt", sCSMT.getSelectedItemPosition() != 0 ? 3 : 0);
+
+ Spinner sGPUName = view.findViewById(R.id.SGPUName);
+ try {
+ JSONObject gpuName = gpuNames.getJSONObject(sGPUName.getSelectedItemPosition());
+ registryEditor.setDwordValue("Software\\Wine\\Direct3D", "VideoPciDeviceID", gpuName.getInt("deviceID"));
+ registryEditor.setDwordValue("Software\\Wine\\Direct3D", "VideoPciVendorID", gpuName.getInt("vendorID"));
+ }
+ catch (JSONException e) {}
+
+ Spinner sOffscreenRenderingMode = view.findViewById(R.id.SOffscreenRenderingMode);
+ registryEditor.setStringValue("Software\\Wine\\Direct3D", "OffScreenRenderingMode", sOffscreenRenderingMode.getSelectedItem().toString().toLowerCase(Locale.ENGLISH));
+
+ Spinner sStrictShaderMath = view.findViewById(R.id.SStrictShaderMath);
+ registryEditor.setDwordValue("Software\\Wine\\Direct3D", "strict_shader_math", sStrictShaderMath.getSelectedItemPosition());
+
+ Spinner sVideoMemorySize = view.findViewById(R.id.SVideoMemorySize);
+ registryEditor.setStringValue("Software\\Wine\\Direct3D", "VideoMemorySize", sVideoMemorySize.getSelectedItem().toString());
+
+ Spinner sMouseWarpOverride = view.findViewById(R.id.SMouseWarpOverride);
+ registryEditor.setStringValue("Software\\Wine\\DirectInput", "MouseWarpOverride", sMouseWarpOverride.getSelectedItem().toString().toLowerCase(Locale.ENGLISH));
+
+ registryEditor.setStringValue("Software\\Wine\\Direct3D", "shader_backend", "glsl");
+ registryEditor.setStringValue("Software\\Wine\\Direct3D", "UseGLSL", "enabled");
+
+ final String dllOverridesKey = "Software\\Wine\\DllOverrides";
+ for (String name : defaultDLLOverrides) registryEditor.setStringValue(dllOverridesKey, name, "native,builtin");
+ }
+ }
+
+ private void createWineRegistryKeysTab(View view) {
+ Context context = getContext();
+ File containerDir = container != null ? container.getRootDir() : null;
+ File userRegFile = new File(containerDir, ".wine/user.reg");
+
+ try (WineRegistryEditor registryEditor = new WineRegistryEditor(userRegFile)) {
+ List stateList = Arrays.asList(context.getString(R.string.disable), context.getString(R.string.enable));
+ Spinner sCSMT = view.findViewById(R.id.SCSMT);
+ sCSMT.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, stateList));
+ sCSMT.setSelection(registryEditor.getDwordValue("Software\\Wine\\Direct3D", "csmt", 3) != 0 ? 1 : 0);
+
+ Spinner sGPUName = view.findViewById(R.id.SGPUName);
+ loadGPUNameSpinner(sGPUName, registryEditor.getDwordValue("Software\\Wine\\Direct3D", "VideoPciDeviceID", 1556));
+
+ List offscreenRenderingModeList = Arrays.asList("Backbuffer", "FBO");
+ Spinner sOffscreenRenderingMode = view.findViewById(R.id.SOffscreenRenderingMode);
+ sOffscreenRenderingMode.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, offscreenRenderingModeList));
+ AppUtils.setSpinnerSelectionFromValue(sOffscreenRenderingMode, registryEditor.getStringValue("Software\\Wine\\Direct3D", "OffScreenRenderingMode", "fbo"));
+
+ Spinner sStrictShaderMath = view.findViewById(R.id.SStrictShaderMath);
+ sStrictShaderMath.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, stateList));
+ sStrictShaderMath.setSelection(Math.min(registryEditor.getDwordValue("Software\\Wine\\Direct3D", "strict_shader_math", 1), 1));
+
+ Spinner sVideoMemorySize = view.findViewById(R.id.SVideoMemorySize);
+ String videoMemorySize = registryEditor.getStringValue("Software\\Wine\\Direct3D", "VideoMemorySize", "2048");
+ loadVideoMemorySizeSpinner(sVideoMemorySize, videoMemorySize);
+
+ List mouseWarpOverrideList = Arrays.asList(context.getString(R.string.disable), context.getString(R.string.enable), context.getString(R.string.force));
+ Spinner sMouseWarpOverride = view.findViewById(R.id.SMouseWarpOverride);
+ sMouseWarpOverride.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, mouseWarpOverrideList));
+ AppUtils.setSpinnerSelectionFromValue(sMouseWarpOverride, registryEditor.getStringValue("Software\\Wine\\DirectInput", "MouseWarpOverride", "disable"));
+ }
+ }
+
+ private void loadGPUNameSpinner(Spinner spinner, int selectedDeviceID) {
+ List values = new ArrayList<>();
+ int selectedPosition = 0;
+
+ try {
+ for (int i = 0; i < gpuNames.length(); i++) {
+ JSONObject item = gpuNames.getJSONObject(i);
+ if (item.getInt("deviceID") == selectedDeviceID) selectedPosition = i;
+ values.add(item.getString("name"));
+ }
+ }
+ catch (JSONException e) {}
+
+ spinner.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, values));
+ spinner.setSelection(selectedPosition);
+ }
+
+ private void loadVideoMemorySizeSpinner(Spinner spinner, String selectedValue) {
+ List values = new ArrayList<>();
+ int selectedPosition = 0;
+
+ for (int i = 5, j = 0; i <= 12; i++, j++) {
+ String value = String.valueOf((int)Math.pow(2, i));
+ if (value.equals(selectedValue)) selectedPosition = j;
+ values.add(value);
+ }
+
+ spinner.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, values));
+ spinner.setSelection(selectedPosition);
+ }
+
+ private String getEnvVars(View view) {
+ LinearLayout parent = view.findViewById(R.id.LLEnvVars);
+ EnvVars envVars = new EnvVars();
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ View child = parent.getChildAt(i);
+ String name = ((TextView)child.findViewById(R.id.TextView)).getText().toString();
+ String value = ((EditText)child.findViewById(R.id.EditText)).getText().toString().trim();
+ if (!value.isEmpty()) envVars.put(name, value);
+ }
+ return envVars.toString();
+ }
+
+ private String getScreenSize(View view) {
+ Spinner sScreenSize = view.findViewById(R.id.SScreenSize);
+ String value = sScreenSize.getSelectedItem().toString();
+ if (value.equalsIgnoreCase("custom")) {
+ value = Container.DEFAULT_SCREEN_SIZE;
+ String strWidth = ((EditText)view.findViewById(R.id.ETScreenWidth)).getText().toString().trim();
+ String strHeight = ((EditText)view.findViewById(R.id.ETScreenHeight)).getText().toString().trim();
+ if (strWidth.matches("[0-9]+") && strHeight.matches("[0-9]+")) {
+ int width = Integer.parseInt(strWidth);
+ int height = Integer.parseInt(strHeight);
+ if ((width % 2) == 0 && (height % 2) == 0) return width+"x"+height;
+ }
+ }
+ return StringUtils.parseIdentifier(value);
+ }
+
+ private void loadScreenSizeSpinner(View view, String selectedValue) {
+ final Spinner sScreenSize = view.findViewById(R.id.SScreenSize);
+
+ final LinearLayout llCustomScreenSize = view.findViewById(R.id.LLCustomScreenSize);
+ sScreenSize.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ String value = sScreenSize.getItemAtPosition(position).toString();
+ llCustomScreenSize.setVisibility(value.equalsIgnoreCase("custom") ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+
+ boolean found = AppUtils.setSpinnerSelectionFromIdentifier(sScreenSize, selectedValue);
+ if (!found) {
+ AppUtils.setSpinnerSelectionFromValue(sScreenSize, "custom");
+ String[] screenSize = selectedValue.split("x");
+ ((EditText)view.findViewById(R.id.ETScreenWidth)).setText(screenSize[0]);
+ ((EditText)view.findViewById(R.id.ETScreenHeight)).setText(screenSize[1]);
+ }
+ }
+
+ private void loadGraphicsDriverSpinner(Spinner sGraphicsDriver, Spinner sDXWrapper) {
+ final Context context = getContext();
+ final String[] dxwrapperEntries = context.getResources().getStringArray(R.array.dxwrapper_entries);
+
+ Runnable update = () -> {
+ String graphicsDriver = StringUtils.parseIdentifier(sGraphicsDriver.getSelectedItem());
+ boolean useDXVK = graphicsDriver.equals("turnip-zink");
+
+ ArrayList items = new ArrayList<>();
+ for (String value : dxwrapperEntries) if (useDXVK || (!value.startsWith("DXVK") && !value.startsWith("D8VK"))) items.add(value);
+ sDXWrapper.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, items.toArray(new String[0])));
+
+ AppUtils.setSpinnerSelectionFromIdentifier(sDXWrapper, container != null ? container.getDXWrapper() : Container.DEFAULT_DXWRAPPER);
+ };
+
+ sGraphicsDriver.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ update.run();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+
+ String selectedGraphicsDriver = container != null ? container.getGraphicsDriver() : Container.DEFAULT_GRAPHICS_DRIVER;
+ AppUtils.setSpinnerSelectionFromIdentifier(sGraphicsDriver, selectedGraphicsDriver);
+
+ update.run();
+ }
+
+ private String getDXComponents(View view) {
+ ViewGroup parent = view.findViewById(R.id.LLTabDXComponents);
+ int childCount = parent.getChildCount();
+ String[] dxcomponents = new String[childCount];
+
+ for (int i = 0; i < childCount; i++) {
+ View child = parent.getChildAt(i);
+ Spinner spinner = child.findViewById(R.id.Spinner);
+ dxcomponents[i] = child.getTag().toString()+"="+spinner.getSelectedItemPosition();
+ }
+ return String.join(",", dxcomponents);
+ }
+
+ private void createDXComponentsTab(View view) {
+ final String[] dxcomponents = (container != null ? container.getDXComponents() : Container.DEFAULT_DXCOMPONENTS).split(",");
+
+ Context context = getContext();
+ LayoutInflater inflater = LayoutInflater.from(context);
+ ViewGroup parent = view.findViewById(R.id.LLTabDXComponents);
+
+ for (String dxcomponent : dxcomponents) {
+ String[] parts = dxcomponent.split("=");
+ View itemView = inflater.inflate(R.layout.dxcomponent_list_item, parent, false);
+ ((TextView)itemView.findViewById(R.id.TextView)).setText(StringUtils.getString(context, parts[0]));
+ ((Spinner)itemView.findViewById(R.id.Spinner)).setSelection(Integer.parseInt(parts[1]), false);
+ itemView.setTag(parts[0]);
+ parent.addView(itemView);
+ }
+ }
+
+ private String getDrives(View view) {
+ LinearLayout parent = view.findViewById(R.id.LLDrives);
+ String drives = "";
+
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ View child = parent.getChildAt(i);
+ Spinner spinner = child.findViewById(R.id.Spinner);
+ EditText editText = child.findViewById(R.id.EditText);
+ String path = editText.getText().toString().trim();
+ if (!path.isEmpty()) drives += spinner.getSelectedItem()+path;
+ }
+ return drives;
+ }
+
+ private void createDrivesTab(View view) {
+ Context context = getContext();
+
+ final LinearLayout parent = view.findViewById(R.id.LLDrives);
+ final View emptyTextView = view.findViewById(R.id.TVDrivesEmptyText);
+ LayoutInflater inflater = LayoutInflater.from(context);
+ final String drives = container != null ? container.getDrives() : Container.DEFAULT_DRIVES;
+ final String[] driveLetters = new String[Container.MAX_DRIVE_LETTERS];
+ for (int i = 0; i < driveLetters.length; i++) driveLetters[i] = ((char)(i + 68))+":";
+
+ Callback addItem = (drive) -> {
+ final View itemView = inflater.inflate(R.layout.drive_list_item, parent, false);
+ Spinner spinner = itemView.findViewById(R.id.Spinner);
+ spinner.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, driveLetters));
+ AppUtils.setSpinnerSelectionFromValue(spinner, drive[0]+":");
+
+ final EditText editText = itemView.findViewById(R.id.EditText);
+ editText.setText(drive[1]);
+
+ itemView.findViewById(R.id.BTSearch).setOnClickListener((v) -> {
+ openDirectoryCallback = (path) -> {
+ drive[1] = path;
+ editText.setText(path);
+ };
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.fromFile(Environment.getExternalStorageDirectory()));
+ getActivity().startActivityFromFragment(this, intent, MainActivity.OPEN_DIRECTORY_REQUEST_CODE);
+ });
+
+ itemView.findViewById(R.id.BTRemove).setOnClickListener((v) -> {
+ parent.removeView(itemView);
+ if (parent.getChildCount() == 0) emptyTextView.setVisibility(View.VISIBLE);
+ });
+ parent.addView(itemView);
+ };
+ for (String[] drive : Container.drivesIterator(drives)) addItem.call(drive);
+
+ view.findViewById(R.id.BTAddDrive).setOnClickListener((v) -> {
+ if (parent.getChildCount() >= Container.MAX_DRIVE_LETTERS) return;
+ final String nextDriveLetter = String.valueOf(driveLetters[parent.getChildCount()].charAt(0));
+ addItem.call(new String[]{nextDriveLetter, ""});
+ });
+
+ if (drives.isEmpty()) emptyTextView.setVisibility(View.VISIBLE);
+ }
+}
diff --git a/app/src/main/java/com/winlator/ContainersFragment.java b/app/src/main/java/com/winlator/ContainersFragment.java
new file mode 100644
index 0000000..dd892c3
--- /dev/null
+++ b/app/src/main/java/com/winlator/ContainersFragment.java
@@ -0,0 +1,250 @@
+package com.winlator;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+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 android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.progressindicator.CircularProgressIndicator;
+import com.winlator.container.Container;
+import com.winlator.container.ContainerManager;
+import com.winlator.core.Callback;
+import com.winlator.core.FileUtils;
+import com.winlator.core.PreloaderDialog;
+import com.winlator.core.StringUtils;
+import com.winlator.contentdialog.ContentDialog;
+import com.winlator.xenvironment.ImageFs;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class ContainersFragment extends Fragment {
+ private RecyclerView recyclerView;
+ private TextView emptyTextView;
+ private ContainerManager manager;
+ private PreloaderDialog preloaderDialog;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ preloaderDialog = new PreloaderDialog(getActivity());
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ manager = new ContainerManager(getContext());
+ loadContainersList();
+ ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle(R.string.containers);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ FrameLayout frameLayout = (FrameLayout)inflater.inflate(R.layout.containers_fragment, container, false);
+ recyclerView = frameLayout.findViewById(R.id.RecyclerView);
+ emptyTextView = frameLayout.findViewById(R.id.TVEmptyText);
+ recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext()));
+ recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));
+ return frameLayout;
+ }
+
+ private void loadContainersList() {
+ ArrayList containers = manager.getContainers();
+ recyclerView.setAdapter(new ContainersAdapter(containers));
+ if (containers.isEmpty()) emptyTextView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.containers_menu, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ if (menuItem.getItemId() == R.id.containers_menu_add) {
+ if (!ImageFs.find(getContext()).isValid()) return false;
+ FragmentManager fragmentManager = getParentFragmentManager();
+ fragmentManager.beginTransaction()
+ .addToBackStack(null)
+ .replace(R.id.FLFragmentContainer, new ContainerDetailFragment())
+ .commit();
+ return true;
+ }
+ else return super.onOptionsItemSelected(menuItem);
+ }
+
+ private class ContainersAdapter extends RecyclerView.Adapter {
+ private final List data;
+
+ private class ViewHolder extends RecyclerView.ViewHolder {
+ private final ImageButton menuButton;
+ private final ImageView imageView;
+ private final TextView title;
+
+ private ViewHolder(View view) {
+ super(view);
+ this.imageView = view.findViewById(R.id.ImageView);
+ this.title = view.findViewById(R.id.TVTitle);
+ this.menuButton = view.findViewById(R.id.BTMenu);
+ }
+ }
+
+ public ContainersAdapter(List data) {
+ this.data = data;
+ }
+
+ @Override
+ public final ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.container_list_item, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(final ViewHolder holder, int position) {
+ final Container item = data.get(position);
+ holder.imageView.setImageResource(R.drawable.icon_container);
+ holder.title.setText(item.getName());
+ holder.menuButton.setOnClickListener((view) -> showListItemMenu(view, item));
+ }
+
+ @Override
+ public final int getItemCount() {
+ return data.size();
+ }
+
+ private void showListItemMenu(View anchorView, Container container) {
+ final Context context = getContext();
+ PopupMenu listItemMenu = new PopupMenu(context, anchorView);
+ listItemMenu.inflate(R.menu.container_popup_menu);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) listItemMenu.setForceShowIcon(true);
+
+ listItemMenu.setOnMenuItemClickListener((menuItem) -> {
+ switch (menuItem.getItemId()) {
+ case R.id.container_run:
+ Intent intent = new Intent(context, XServerDisplayActivity.class);
+ intent.putExtra("container_id", container.id);
+ context.startActivity(intent);
+ break;
+ case R.id.container_edit:
+ FragmentManager fragmentManager = getParentFragmentManager();
+ fragmentManager.beginTransaction()
+ .addToBackStack(null)
+ .replace(R.id.FLFragmentContainer, new ContainerDetailFragment(container.id))
+ .commit();
+ break;
+ case R.id.container_duplicate:
+ ContentDialog.confirm(getContext(), R.string.do_you_want_to_duplicate_this_container, () -> {
+ preloaderDialog.show(R.string.duplicating_container);
+ manager.duplicateContainerAsync(container, () -> {
+ preloaderDialog.close();
+ loadContainersList();
+ });
+ });
+ break;
+ case R.id.container_remove:
+ ContentDialog.confirm(getContext(), R.string.do_you_want_to_remove_this_container, () -> {
+ preloaderDialog.show(R.string.removing_container);
+ manager.removeContainerAsync(container, () -> {
+ preloaderDialog.close();
+ loadContainersList();
+ });
+ });
+ break;
+ case R.id.container_info:
+ showStorageInfoDialog(container);
+ break;
+ }
+ return true;
+ });
+ listItemMenu.show();
+ }
+ }
+
+ private void showStorageInfoDialog(final Container container) {
+ final Activity activity = getActivity();
+ ContentDialog dialog = new ContentDialog(activity, R.layout.container_storage_info_dialog);
+ dialog.setTitle(R.string.storage_info);
+ dialog.setIcon(R.drawable.icon_info);
+
+ AtomicLong driveCSize = new AtomicLong();
+ driveCSize.set(0);
+
+ AtomicLong cacheSize = new AtomicLong();
+ cacheSize.set(0);
+
+ AtomicLong totalSize = new AtomicLong();
+ totalSize.set(0);
+
+ final TextView tvDriveCSize = dialog.findViewById(R.id.TVDriveCSize);
+ final TextView tvCacheSize = dialog.findViewById(R.id.TVCacheSize);
+ final TextView tvTotalSize = dialog.findViewById(R.id.TVTotalSize);
+ final TextView tvUsedSpace = dialog.findViewById(R.id.TVUsedSpace);
+ final CircularProgressIndicator circularProgressIndicator = dialog.findViewById(R.id.CircularProgressIndicator);
+
+ final long internalStorageSize = FileUtils.getInternalStorageSize();
+
+ Runnable updateUI = () -> {
+ tvDriveCSize.setText(StringUtils.formatBytes(driveCSize.get()));
+ tvCacheSize.setText(StringUtils.formatBytes(cacheSize.get()));
+ tvTotalSize.setText(StringUtils.formatBytes(totalSize.get()));
+
+ int progress = (int)(((double)totalSize.get() / internalStorageSize) * 100);
+ tvUsedSpace.setText(progress+"%");
+ circularProgressIndicator.setProgress(progress, true);
+ };
+
+ File rootDir = container.getRootDir();
+ final File driveCDir = new File(rootDir, ".wine/drive_c");
+ final File cacheDir = new File(rootDir, ".cache");
+ AtomicLong lastTime = new AtomicLong(System.currentTimeMillis());
+
+ final Callback onAddSize = (size) -> {
+ totalSize.addAndGet(size);
+ long currTime = System.currentTimeMillis();
+ int elapsedTime = (int)(currTime - lastTime.get());
+ if (elapsedTime > 30) {
+ activity.runOnUiThread(updateUI);
+ lastTime.set(currTime);
+ }
+ };
+
+ FileUtils.getSizeAsync(driveCDir, (size) -> {
+ driveCSize.addAndGet(size);
+ onAddSize.call(size);
+ });
+
+ FileUtils.getSizeAsync(cacheDir, (size) -> {
+ cacheSize.addAndGet(size);
+ onAddSize.call(size);
+ });
+
+ ((TextView)dialog.findViewById(R.id.BTCancel)).setText(R.string.clear_cache);
+ dialog.setOnCancelCallback(() -> FileUtils.clear(cacheDir));
+
+ dialog.show();
+ }
+}
diff --git a/app/src/main/java/com/winlator/ControlsEditorActivity.java b/app/src/main/java/com/winlator/ControlsEditorActivity.java
new file mode 100644
index 0000000..760e6ea
--- /dev/null
+++ b/app/src/main/java/com/winlator/ControlsEditorActivity.java
@@ -0,0 +1,323 @@
+package com.winlator;
+
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupWindow;
+import android.widget.RadioGroup;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.winlator.inputcontrols.Binding;
+import com.winlator.inputcontrols.ControlElement;
+import com.winlator.inputcontrols.ControlsProfile;
+import com.winlator.inputcontrols.InputControlsManager;
+import com.winlator.math.Mathf;
+import com.winlator.core.AppUtils;
+import com.winlator.core.FileUtils;
+import com.winlator.core.UnitUtils;
+import com.winlator.widget.InputControlsView;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+public class ControlsEditorActivity extends AppCompatActivity implements View.OnClickListener {
+ private InputControlsView inputControlsView;
+ private ControlsProfile profile;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ AppUtils.hideSystemUI(this);
+ setContentView(R.layout.controls_editor_activity);
+
+ inputControlsView = new InputControlsView(this);
+ inputControlsView.setEditMode(true);
+ inputControlsView.setOverlayOpacity(0.6f);
+
+ profile = InputControlsManager.loadProfile(this, ControlsProfile.getProfileFile(this, getIntent().getIntExtra("profile_id", 0)));
+ ((TextView)findViewById(R.id.TVProfileName)).setText(profile.getName());
+ inputControlsView.setProfile(profile);
+
+ FrameLayout container = findViewById(R.id.FLContainer);
+ container.addView(inputControlsView, 0);
+
+ container.findViewById(R.id.BTAddElement).setOnClickListener(this);
+ container.findViewById(R.id.BTRemoveElement).setOnClickListener(this);
+ container.findViewById(R.id.BTElementSettings).setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.BTAddElement:
+ if (!inputControlsView.addElement()) {
+ AppUtils.showToast(this, R.string.no_profile_selected);
+ }
+ break;
+ case R.id.BTRemoveElement:
+ if (!inputControlsView.removeElement()) {
+ AppUtils.showToast(this, R.string.no_control_element_selected);
+ }
+ break;
+ case R.id.BTElementSettings:
+ ControlElement selectedElement = inputControlsView.getSelectedElement();
+ if (selectedElement != null) {
+ showControlElementSettings(v);
+ }
+ else AppUtils.showToast(this, R.string.no_control_element_selected);
+ break;
+ }
+ }
+
+ private void showControlElementSettings(View anchorView) {
+ final ControlElement element = inputControlsView.getSelectedElement();
+ View view = LayoutInflater.from(this).inflate(R.layout.control_element_settings, null);
+
+ final Runnable updateLayout = () -> {
+ ControlElement.Type type = element.getType();
+ view.findViewById(R.id.LLShape).setVisibility(View.GONE);
+ view.findViewById(R.id.CBToggleSwitch).setVisibility(View.GONE);
+ view.findViewById(R.id.LLCustomTextIcon).setVisibility(View.GONE);
+ view.findViewById(R.id.LLRangeOptions).setVisibility(View.GONE);
+
+ if (type == ControlElement.Type.BUTTON) {
+ view.findViewById(R.id.LLShape).setVisibility(View.VISIBLE);
+ view.findViewById(R.id.CBToggleSwitch).setVisibility(View.VISIBLE);
+ view.findViewById(R.id.LLCustomTextIcon).setVisibility(View.VISIBLE);
+ }
+ else if (type == ControlElement.Type.RANGE_BUTTON) {
+ view.findViewById(R.id.LLRangeOptions).setVisibility(View.VISIBLE);
+ }
+
+ loadBindingSpinners(element, view);
+ };
+
+ loadTypeSpinner(element, view.findViewById(R.id.SType), updateLayout);
+ loadShapeSpinner(element, view.findViewById(R.id.SShape));
+ loadRangeSpinner(element, view.findViewById(R.id.SRange));
+
+ RadioGroup rgOrientation = view.findViewById(R.id.RGOrientation);
+ rgOrientation.check(element.getOrientation() == 1 ? R.id.RBVertical : R.id.RBHorizontal);
+ rgOrientation.setOnCheckedChangeListener((group, checkedId) -> {
+ element.setOrientation((byte)(checkedId == R.id.RBVertical ? 1 : 0));
+ profile.save();
+ inputControlsView.invalidate();
+ });
+
+ final TextView tvScale = view.findViewById(R.id.TVScale);
+ SeekBar sbScale = view.findViewById(R.id.SBScale);
+ sbScale.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ tvScale.setText(progress+"%");
+ if (fromUser) {
+ progress = (int)Mathf.roundTo(progress, 5);
+ seekBar.setProgress(progress);
+ element.setScale(progress / 100.0f);
+ profile.save();
+ inputControlsView.invalidate();
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {}
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {}
+ });
+ sbScale.setProgress((int)(element.getScale() * 100));
+
+ CheckBox cbToggleSwitch = view.findViewById(R.id.CBToggleSwitch);
+ cbToggleSwitch.setChecked(element.isToggleSwitch());
+ cbToggleSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ element.setToggleSwitch(isChecked);
+ profile.save();
+ });
+
+ final EditText etCustomText = view.findViewById(R.id.ETCustomText);
+ etCustomText.setText(element.getText());
+ final LinearLayout llIconList = view.findViewById(R.id.LLIconList);
+ loadIcons(llIconList, element.getIconId());
+
+ updateLayout.run();
+
+ PopupWindow popupWindow = AppUtils.showPopupWindow(anchorView, view, 340, 0);
+ popupWindow.setOnDismissListener(() -> {
+ String text = etCustomText.getText().toString().trim();
+ byte iconId = 0;
+ for (int i = 0; i < llIconList.getChildCount(); i++) {
+ View child = llIconList.getChildAt(i);
+ if (child.isSelected()) {
+ iconId = (byte)child.getTag();
+ break;
+ }
+ }
+
+ element.setText(text);
+ element.setIconId(iconId);
+ profile.save();
+ inputControlsView.invalidate();
+ });
+ }
+
+ private void loadTypeSpinner(final ControlElement element, Spinner spinner, Runnable callback) {
+ spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, ControlElement.Type.names()));
+ spinner.setSelection(element.getType().ordinal(), false);
+ spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ element.setType(ControlElement.Type.values()[position]);
+ profile.save();
+ callback.run();
+ inputControlsView.invalidate();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+ }
+
+ private void loadShapeSpinner(final ControlElement element, Spinner spinner) {
+ spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, ControlElement.Shape.names()));
+ spinner.setSelection(element.getShape().ordinal(), false);
+ spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ element.setShape(ControlElement.Shape.values()[position]);
+ profile.save();
+ inputControlsView.invalidate();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+ }
+
+ private void loadBindingSpinners(ControlElement element, View view) {
+ LinearLayout container = view.findViewById(R.id.LLBindings);
+ container.removeAllViews();
+
+ ControlElement.Type type = element.getType();
+ if (type == ControlElement.Type.BUTTON) {
+ loadBindingSpinner(element, container, 0, R.string.binding);
+ }
+ else if (type == ControlElement.Type.D_PAD || type == ControlElement.Type.STICK) {
+ loadBindingSpinner(element, container, 0, R.string.binding_up);
+ loadBindingSpinner(element, container, 1, R.string.binding_right);
+ loadBindingSpinner(element, container, 2, R.string.binding_down);
+ loadBindingSpinner(element, container, 3, R.string.binding_left);
+ }
+ }
+
+ private void loadBindingSpinner(final ControlElement element, LinearLayout container, final int index, int titleResId) {
+ View view = LayoutInflater.from(this).inflate(R.layout.binding_field, container, false);
+ ((TextView)view.findViewById(R.id.TVTitle)).setText(titleResId);
+ final Spinner sBindingType = view.findViewById(R.id.SBindingType);
+ final Spinner sBinding = view.findViewById(R.id.SBinding);
+
+ Runnable update = () -> {
+ String[] bindingEntries = sBindingType.getSelectedItemPosition() == 0 ? Binding.keyboardBindingLabels() : Binding.mouseBindingLabels();
+ sBinding.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, bindingEntries));
+ AppUtils.setSpinnerSelectionFromValue(sBinding, element.getBindingAt(index).toString());
+ };
+
+ sBindingType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ update.run();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+ sBindingType.setSelection(element.getBindingAt(index).isKeyboard() ? 0 : 1, false);
+
+ sBinding.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ boolean isKeyboard = sBindingType.getSelectedItemPosition() == 0;
+ Binding binding = isKeyboard ? Binding.keyboardBindingValues()[position] : Binding.mouseBindingValues()[position];
+ if (binding != element.getBindingAt(index)) {
+ element.setBindingAt(index, binding);
+ profile.save();
+ inputControlsView.invalidate();
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+
+ update.run();
+ container.addView(view);
+ }
+
+ private void loadRangeSpinner(final ControlElement element, Spinner spinner) {
+ spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, ControlElement.Range.names()));
+ spinner.setSelection(element.getRange().ordinal(), false);
+ spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ element.setRange(ControlElement.Range.values()[position]);
+ profile.save();
+ inputControlsView.invalidate();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+ }
+
+ private void loadIcons(final LinearLayout parent, byte selectedId) {
+ byte[] iconIds = new byte[0];
+ try {
+ String[] filenames = getAssets().list("inputcontrols/icons/");
+ iconIds = new byte[filenames.length];
+ for (int i = 0; i < filenames.length; i++) {
+ iconIds[i] = Byte.parseByte(FileUtils.getBasename(filenames[i]));
+ }
+ }
+ catch (IOException e) {}
+
+ Arrays.sort(iconIds);
+
+ int size = (int)UnitUtils.dpToPx(40);
+ int margin = (int)UnitUtils.dpToPx(2);
+ int padding = (int)UnitUtils.dpToPx(4);
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(size, size);
+ params.setMargins(margin, 0, margin, 0);
+
+ for (final byte id : iconIds) {
+ ImageView imageView = new ImageView(this);
+ imageView.setLayoutParams(params);
+ imageView.setPadding(padding, padding, padding, padding);
+ imageView.setBackgroundResource(R.drawable.icon_background);
+ imageView.setTag(id);
+ imageView.setSelected(id == selectedId);
+ imageView.setOnClickListener((v) -> {
+ for (int i = 0; i < parent.getChildCount(); i++) parent.getChildAt(i).setSelected(false);
+ imageView.setSelected(true);
+ });
+
+ try (InputStream is = getAssets().open("inputcontrols/icons/"+id+".png")) {
+ imageView.setImageBitmap(BitmapFactory.decodeStream(is));
+ }
+ catch (IOException e) {}
+
+ parent.addView(imageView);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/ExternalControllerBindingsActivity.java b/app/src/main/java/com/winlator/ExternalControllerBindingsActivity.java
new file mode 100644
index 0000000..9e35fca
--- /dev/null
+++ b/app/src/main/java/com/winlator/ExternalControllerBindingsActivity.java
@@ -0,0 +1,264 @@
+package com.winlator;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.winlator.core.AppUtils;
+import com.winlator.inputcontrols.Binding;
+import com.winlator.inputcontrols.ControlsProfile;
+import com.winlator.inputcontrols.ExternalController;
+import com.winlator.inputcontrols.ExternalControllerBinding;
+import com.winlator.inputcontrols.InputControlsManager;
+import com.winlator.math.Mathf;
+
+public class ExternalControllerBindingsActivity extends AppCompatActivity {
+ private TextView emptyTextView;
+ private ControlsProfile profile;
+ private ExternalController controller;
+ private RecyclerView recyclerView;
+ private ControllerBindingsAdapter adapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.external_controller_bindings_activity);
+
+ Intent intent = getIntent();
+ int profileId = intent.getIntExtra("profile_id", 0);
+ profile = InputControlsManager.loadProfile(this, ControlsProfile.getProfileFile(this, profileId));
+ String controllerId = intent.getStringExtra("controller_id");
+
+ controller = profile.getController(controllerId);
+ if (controller == null) {
+ controller = profile.addController(controllerId);
+ profile.save();
+ }
+
+ Toolbar toolbar = findViewById(R.id.Toolbar);
+ toolbar.setTitle(controller.getName());
+ setSupportActionBar(toolbar);
+
+ ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeAsUpIndicator(R.drawable.icon_action_bar_back);
+
+ emptyTextView = findViewById(R.id.TVEmptyText);
+ recyclerView = findViewById(R.id.RecyclerView);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
+ recyclerView.setAdapter(adapter = new ControllerBindingsAdapter());
+ updateEmptyTextView();
+ }
+
+ private void updateControllerBinding(int keyCode, Binding binding) {
+ if (keyCode == KeyEvent.KEYCODE_UNKNOWN) return;
+ ExternalControllerBinding controllerBinding = controller.getControllerBinding(keyCode);
+ int position;
+ if (controllerBinding == null) {
+ controllerBinding = new ExternalControllerBinding();
+ controllerBinding.setKeyCode(keyCode);
+ controllerBinding.setBinding(binding);
+
+ controller.addControllerBinding(controllerBinding);
+ profile.save();
+ adapter.notifyDataSetChanged();
+ updateEmptyTextView();
+ position = controller.getPosition(controllerBinding);
+ }
+ else animateItemView(position = controller.getPosition(controllerBinding));
+ recyclerView.scrollToPosition(position);
+ }
+
+ private void processJoystickInput(MotionEvent event, int historyPos) {
+ int keyCode = KeyEvent.KEYCODE_UNKNOWN;
+ byte sign;
+ Binding binding = Binding.NONE;
+
+ final int[] axes = {MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ, MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y};
+ for (int axis : axes) {
+ if ((sign = Mathf.sign(ExternalController.getCenteredAxis(event, axis, historyPos))) != 0) {
+ if (axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_Z) {
+ binding = sign > 0 ? Binding.MOUSE_MOVE_RIGHT : Binding.MOUSE_MOVE_LEFT;
+ }
+ else if (axis == MotionEvent.AXIS_Y || axis == MotionEvent.AXIS_RZ) {
+ binding = sign > 0 ? Binding.MOUSE_MOVE_DOWN : Binding.MOUSE_MOVE_UP;
+ }
+ else if (axis == MotionEvent.AXIS_HAT_X) {
+ binding = sign > 0 ? Binding.KEY_D : Binding.KEY_A;
+ }
+ else if (axis == MotionEvent.AXIS_HAT_Y) {
+ binding = sign > 0 ? Binding.KEY_S : Binding.KEY_W;
+ }
+
+ keyCode = ExternalControllerBinding.getKeyCodeForAxis(axis, sign);
+ break;
+ }
+ }
+
+ updateControllerBinding(keyCode, binding);
+ }
+
+ private void processTriggerButton(MotionEvent event) {
+ if (event.getAxisValue(MotionEvent.AXIS_LTRIGGER) == 1 || event.getAxisValue(MotionEvent.AXIS_BRAKE) == 1) {
+ updateControllerBinding(ExternalControllerBinding.AXIS_LTRIGGER, Binding.NONE);
+ }
+ if (event.getAxisValue(MotionEvent.AXIS_RTRIGGER) == 1 || event.getAxisValue(MotionEvent.AXIS_GAS) == 1) {
+ updateControllerBinding(ExternalControllerBinding.AXIS_RTRIGGER, Binding.NONE);
+ }
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (event.getDeviceId() == controller.getDeviceId()) {
+ if (ExternalController.isDPadDevice(event)) {
+ processTriggerButton(event);
+ processJoystickInput(event, -1);
+ return true;
+ }
+ else if (ExternalController.isJoystickDevice(event)) {
+ int historySize = event.getHistorySize();
+ for (int i = 0; i < historySize; i++) processJoystickInput(event, i);
+ processJoystickInput(event, -1);
+ return true;
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if ((event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD &&
+ event.getDeviceId() == controller.getDeviceId() && event.getRepeatCount() == 0) {
+ updateControllerBinding(keyCode, Binding.NONE);
+ return true;
+ }
+ else return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ finish();
+ return true;
+ }
+
+ private class ControllerBindingsAdapter extends RecyclerView.Adapter {
+ private class ViewHolder extends RecyclerView.ViewHolder {
+ private final ImageButton removeButton;
+ private final TextView title;
+ private final Spinner bindingType;
+ private final Spinner binding;
+
+ private ViewHolder(View view) {
+ super(view);
+ this.title = view.findViewById(R.id.TVTitle);
+ this.bindingType = view.findViewById(R.id.SBindingType);
+ this.binding = view.findViewById(R.id.SBinding);
+ this.removeButton = view.findViewById(R.id.BTRemove);
+ }
+ }
+
+ @Override
+ public final ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.external_controller_binding_list_item, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ final ExternalControllerBinding item = controller.getControllerBindingAt(position);
+ holder.title.setText(item.toString());
+ loadBindingSpinner(holder, item);
+ holder.removeButton.setOnClickListener((view) -> {
+ controller.removeControllerBinding(item);
+ profile.save();
+ notifyDataSetChanged();
+ updateEmptyTextView();
+ });
+ }
+
+ @Override
+ public final int getItemCount() {
+ return controller.getControllerBindingCount();
+ }
+
+ private void loadBindingSpinner(ViewHolder holder, final ExternalControllerBinding item) {
+ final Context $this = ExternalControllerBindingsActivity.this;
+
+ Runnable update = () -> {
+ String[] bindingEntries = holder.bindingType.getSelectedItemPosition() == 0 ? Binding.keyboardBindingLabels() : Binding.mouseBindingLabels();
+ holder.binding.setAdapter(new ArrayAdapter<>($this, android.R.layout.simple_spinner_dropdown_item, bindingEntries));
+ AppUtils.setSpinnerSelectionFromValue(holder.binding, item.getBinding().toString());
+ };
+
+ holder.bindingType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ update.run();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+ holder.bindingType.setSelection(item.getBinding().isKeyboard() ? 0 : 1, false);
+
+ holder.binding.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ boolean isKeyboard = holder.bindingType.getSelectedItemPosition() == 0;
+ Binding binding = isKeyboard ? Binding.keyboardBindingValues()[position] : Binding.mouseBindingValues()[position];
+ if (binding != item.getBinding()) {
+ item.setBinding(binding);
+ profile.save();
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+
+ update.run();
+ }
+ }
+
+ private void updateEmptyTextView() {
+ emptyTextView.setVisibility(adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
+ }
+
+ private void animateItemView(int position) {
+ final ControllerBindingsAdapter.ViewHolder holder = (ControllerBindingsAdapter.ViewHolder)recyclerView.findViewHolderForAdapterPosition(position);
+ if (holder != null) {
+ final int color = ContextCompat.getColor(this, R.color.colorAccent);
+ final ValueAnimator animator = ValueAnimator.ofFloat(0.4f, 0.0f);
+ animator.setDuration(200);
+ animator.setInterpolator(new AccelerateDecelerateInterpolator());
+ animator.addUpdateListener((animation) -> {
+ float alpha = (float)animation.getAnimatedValue();
+ holder.itemView.setBackgroundColor(Color.argb((int)(alpha * 255), Color.red(color), Color.green(color), Color.blue(color)));
+ });
+ animator.start();
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/InputControlsFragment.java b/app/src/main/java/com/winlator/InputControlsFragment.java
new file mode 100644
index 0000000..522b766
--- /dev/null
+++ b/app/src/main/java/com/winlator/InputControlsFragment.java
@@ -0,0 +1,366 @@
+package com.winlator;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.ColorStateList;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.core.widget.ImageViewCompat;
+import androidx.fragment.app.Fragment;
+
+import com.winlator.core.AppUtils;
+import com.winlator.core.Callback;
+import com.winlator.core.FileUtils;
+import com.winlator.core.HttpUtils;
+import com.winlator.inputcontrols.ControlsProfile;
+import com.winlator.inputcontrols.ExternalController;
+import com.winlator.inputcontrols.InputControlsManager;
+import com.winlator.math.Mathf;
+import com.winlator.contentdialog.ContentDialog;
+import com.winlator.widget.InputControlsView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class InputControlsFragment extends Fragment {
+ private static final String INPUT_CONTROLS_URL = "https://raw.githubusercontent.com/brunodev85/winlator/main/input_controls/%s";
+ private InputControlsManager manager;
+ private ControlsProfile currentProfile;
+ private Runnable updateLayout;
+ private Callback importProfileCallback;
+ private final int selectedProfileId;
+
+ public InputControlsFragment(int selectedProfileId) {
+ this.selectedProfileId = selectedProfileId;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(false);
+ manager = new InputControlsManager(getContext());
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle(R.string.input_controls);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == MainActivity.OPEN_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+ try {
+ ControlsProfile importedProfile = manager.importProfile(new JSONObject(FileUtils.readString(getContext(), data.getData())));
+ if (importProfileCallback != null) importProfileCallback.call(importedProfile);
+ }
+ catch (Exception e) {
+ AppUtils.showToast(getContext(), R.string.unable_to_import_profile);
+ }
+ importProfileCallback = null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.input_controls_fragment, container, false);
+ final Context context = getContext();
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+ currentProfile = selectedProfileId > 0 ? manager.getProfile(selectedProfileId) : null;
+
+ final Spinner sProfile = view.findViewById(R.id.SProfile);
+ loadProfileSpinner(sProfile);
+
+ final TextView tvCursorSpeed = view.findViewById(R.id.TVCursorSpeed);
+ final SeekBar sbCursorSpeed = view.findViewById(R.id.SBCursorSpeed);
+ sbCursorSpeed.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ tvCursorSpeed.setText(progress+"%");
+ if (currentProfile != null) {
+ currentProfile.setCursorSpeed(progress / 100.0f);
+ currentProfile.save();
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {}
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {}
+ });
+
+ updateLayout = () -> {
+ if (currentProfile != null) {
+ sbCursorSpeed.setProgress((int)(currentProfile.getCursorSpeed() * 100));
+ }
+ else sbCursorSpeed.setProgress(100);
+ loadExternalControllers(view);
+ };
+
+ updateLayout.run();
+
+ final TextView tvUiOpacity = view.findViewById(R.id.TVUiOpacity);
+ SeekBar sbUiOpacity = view.findViewById(R.id.SBOverlayOpacity);
+ sbUiOpacity.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ tvUiOpacity.setText(progress+"%");
+ if (fromUser) {
+ progress = (int)Mathf.roundTo(progress, 5);
+ seekBar.setProgress(progress);
+ preferences.edit().putFloat("overlay_opacity", progress / 100.0f).apply();
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {}
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {}
+ });
+ sbUiOpacity.setProgress((int)(preferences.getFloat("overlay_opacity", InputControlsView.DEFAULT_OVERLAY_OPACITY) * 100));
+
+ view.findViewById(R.id.BTAddProfile).setOnClickListener((v) -> ContentDialog.prompt(context, R.string.profile_name, null, (name) -> {
+ currentProfile = manager.createProfile(name);
+ loadProfileSpinner(sProfile);
+ updateLayout.run();
+ }));
+
+ view.findViewById(R.id.BTEditProfile).setOnClickListener((v) -> {
+ if (currentProfile != null) {
+ ContentDialog.prompt(context, R.string.profile_name, currentProfile.getName(), (name) -> {
+ currentProfile.setName(name);
+ currentProfile.save();
+ loadProfileSpinner(sProfile);
+ });
+ }
+ else AppUtils.showToast(context, R.string.no_profile_selected);
+ });
+
+ view.findViewById(R.id.BTDuplicateProfile).setOnClickListener((v) -> {
+ if (currentProfile != null) {
+ ContentDialog.confirm(context, R.string.do_you_want_to_duplicate_this_profile, () -> {
+ currentProfile = manager.duplicateProfile(currentProfile);
+ loadProfileSpinner(sProfile);
+ updateLayout.run();
+ });
+ }
+ else AppUtils.showToast(context, R.string.no_profile_selected);
+ });
+
+ view.findViewById(R.id.BTRemoveProfile).setOnClickListener((v) -> {
+ if (currentProfile != null) {
+ ContentDialog.confirm(context, R.string.do_you_want_to_remove_this_profile, () -> {
+ manager.removeProfile(currentProfile);
+ currentProfile = null;
+ loadProfileSpinner(sProfile);
+ updateLayout.run();
+ });
+ }
+ else AppUtils.showToast(context, R.string.no_profile_selected);
+ });
+
+ view.findViewById(R.id.BTImportProfile).setOnClickListener((v) -> {
+ android.widget.PopupMenu popupMenu = new PopupMenu(context, v);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) popupMenu.setForceShowIcon(true);
+ popupMenu.inflate(R.menu.open_file_popup_menu);
+ popupMenu.setOnMenuItemClickListener((menuItem) -> {
+ int itemId = menuItem.getItemId();
+ if (itemId == R.id.open_file) {
+ openProfileFile(sProfile);
+ }
+ else if (itemId == R.id.download_file) {
+ downloadProfileList(sProfile);
+ }
+ return true;
+ });
+ popupMenu.show();
+ });
+
+ view.findViewById(R.id.BTExportProfile).setOnClickListener((v) -> {
+ if (currentProfile != null) {
+ File exportedFile = manager.exportProfile(currentProfile);
+ if (exportedFile != null) {
+ String path = exportedFile.getPath().substring(exportedFile.getPath().indexOf(Environment.DIRECTORY_DOWNLOADS));
+ AppUtils.showToast(context, context.getString(R.string.profile_exported_to)+" "+path);
+ }
+ }
+ else AppUtils.showToast(context, R.string.no_profile_selected);
+ });
+
+ view.findViewById(R.id.BTControlsEditor).setOnClickListener((v) -> {
+ if (currentProfile != null) {
+ Intent intent = new Intent(context, ControlsEditorActivity.class);
+ intent.putExtra("profile_id", currentProfile.id);
+ startActivity(intent);
+ }
+ else AppUtils.showToast(context, R.string.no_profile_selected);
+ });
+
+ return view;
+ }
+
+ private void openProfileFile(Spinner sProfile) {
+ importProfileCallback = (importedProfile) -> {
+ currentProfile = importedProfile;
+ loadProfileSpinner(sProfile);
+ updateLayout.run();
+ };
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("*/*");
+ getActivity().startActivityFromFragment(this, intent, MainActivity.OPEN_FILE_REQUEST_CODE);
+ }
+
+ private void downloadSelectedProfiles(final Spinner sProfile, String[] items, final ArrayList positions) {
+ final MainActivity activity = (MainActivity)getActivity();
+ activity.preloaderDialog.show(R.string.downloading_file);
+ currentProfile = null;
+ final AtomicInteger processedItemCount = new AtomicInteger();
+
+ for (int position : positions) {
+ HttpUtils.download(String.format(INPUT_CONTROLS_URL, items[position]), (content) -> {
+ try {
+ if (content != null) manager.importProfile(new JSONObject(content));
+ }
+ catch (JSONException e) {}
+ if (processedItemCount.incrementAndGet() == positions.size()) {
+ activity.runOnUiThread(() -> {
+ activity.preloaderDialog.close();
+ loadProfileSpinner(sProfile);
+ updateLayout.run();
+ });
+ }
+ });
+ }
+ }
+
+ private void downloadProfileList(final Spinner sProfile) {
+ final MainActivity activity = (MainActivity)getActivity();
+ activity.preloaderDialog.show(R.string.loading);
+ HttpUtils.download(String.format(INPUT_CONTROLS_URL, "index.txt"), (content) -> activity.runOnUiThread(() -> {
+ activity.preloaderDialog.close();
+ if (content != null) {
+ final String[] items = content.split("\n");
+ ContentDialog.showMultipleChoiceList(activity, R.string.import_profile, items, (positions) -> {
+ if (!positions.isEmpty()) {
+ ContentDialog.confirm(activity, R.string.do_you_want_to_download_the_selected_profiles, () -> downloadSelectedProfiles(sProfile, items, positions));
+ }
+ });
+ }
+ else AppUtils.showToast(activity, R.string.unable_to_load_profile_list);
+ }));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (updateLayout != null) updateLayout.run();
+ }
+
+ private void loadProfileSpinner(Spinner spinner) {
+ final ArrayList profiles = manager.getProfiles();
+ ArrayList values = new ArrayList<>();
+ values.add("-- "+getString(R.string.select_profile)+" --");
+
+ int selectedPosition = 0;
+ for (int i = 0; i < profiles.size(); i++) {
+ ControlsProfile profile = profiles.get(i);
+ if (profile == currentProfile) selectedPosition = i + 1;
+ values.add(profile.getName());
+ }
+
+ spinner.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, values));
+ spinner.setSelection(selectedPosition, false);
+ spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ currentProfile = position > 0 ? profiles.get(position - 1) : null;
+ updateLayout.run();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+ }
+
+ private void loadExternalControllers(final View view) {
+ LinearLayout container = view.findViewById(R.id.LLExternalControllers);
+ container.removeAllViews();
+ Context context = getContext();
+ LayoutInflater inflater = LayoutInflater.from(context);
+ ArrayList connectedControllers = ExternalController.getControllers();
+
+ ArrayList controllers = currentProfile != null ? currentProfile.loadControllers() : new ArrayList<>();
+ for (ExternalController controller : connectedControllers) {
+ if (!controllers.contains(controller)) controllers.add(controller);
+ }
+
+ if (!controllers.isEmpty()) {
+ view.findViewById(R.id.TVEmptyText).setVisibility(View.GONE);
+ String bindingsText = context.getString(R.string.bindings);
+ for (final ExternalController controller : controllers) {
+ View itemView = inflater.inflate(R.layout.external_controller_list_item, container, false);
+ ((TextView)itemView.findViewById(R.id.TVTitle)).setText(controller.getName());
+
+ int controllerBindingCount = controller.getControllerBindingCount();
+ ((TextView)itemView.findViewById(R.id.TVSubtitle)).setText(controllerBindingCount+" "+bindingsText);
+
+ ImageView imageView = itemView.findViewById(R.id.ImageView);
+ int tintColor = controller.isConnected() ? ContextCompat.getColor(context, R.color.colorAccent) : 0xffe57373;
+ ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor));
+
+ if (controllerBindingCount > 0) {
+ ImageButton removeButton = itemView.findViewById(R.id.BTRemove);
+ removeButton.setVisibility(View.VISIBLE);
+ removeButton.setOnClickListener((v) -> ContentDialog.confirm(getContext(), R.string.do_you_want_to_remove_this_controller, () -> {
+ currentProfile.removeController(controller);
+ currentProfile.save();
+ loadExternalControllers(view);
+ }));
+ }
+
+ itemView.setOnClickListener((v) -> {
+ if (currentProfile != null) {
+ Intent intent = new Intent(getContext(), ExternalControllerBindingsActivity.class);
+ intent.putExtra("profile_id", currentProfile.id);
+ intent.putExtra("controller_id", controller.getId());
+ startActivity(intent);
+ }
+ else AppUtils.showToast(getContext(), R.string.no_profile_selected);
+ });
+
+ container.addView(itemView);
+ }
+ }
+ else view.findViewById(R.id.TVEmptyText).setVisibility(View.VISIBLE);
+ }
+}
diff --git a/app/src/main/java/com/winlator/MainActivity.java b/app/src/main/java/com/winlator/MainActivity.java
new file mode 100644
index 0000000..786bfd6
--- /dev/null
+++ b/app/src/main/java/com/winlator/MainActivity.java
@@ -0,0 +1,215 @@
+package com.winlator;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.method.LinkMovementMethod;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.view.GravityCompat;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.google.android.material.navigation.NavigationView;
+import com.winlator.core.Callback;
+import com.winlator.core.OBBImageInstaller;
+import com.winlator.core.PreloaderDialog;
+import com.winlator.contentdialog.ContentDialog;
+import com.winlator.xenvironment.ImageFs;
+
+import java.util.List;
+
+public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
+ public static final byte DEBUG_LEVEL = 0; // FIXME set 0 to disable
+ public static final @IntRange(from = 1, to = 19) byte CONTAINER_PATTERN_COMPRESSION_LEVEL = 9;
+ public static final byte PERMISSION_WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 1;
+ public static final byte OPEN_FILE_REQUEST_CODE = 2;
+ public static final byte EDIT_INPUT_CONTROLS_REQUEST_CODE = 3;
+ public static final byte OPEN_DIRECTORY_REQUEST_CODE = 4;
+ private DrawerLayout drawerLayout;
+ public final PreloaderDialog preloaderDialog = new PreloaderDialog(this);
+ private boolean editInputControls = false;
+ private int selectedProfileId;
+ private Callback openFileCallback;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity);
+
+ drawerLayout = findViewById(R.id.DrawerLayout);
+ NavigationView navigationView = findViewById(R.id.NavigationView);
+ navigationView.setNavigationItemSelectedListener(this);
+
+ setSupportActionBar(findViewById(R.id.Toolbar));
+ ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ Intent intent = getIntent();
+ editInputControls = intent.getBooleanExtra("edit_input_controls", false);
+ if (editInputControls) {
+ selectedProfileId = intent.getIntExtra("selected_profile_id", 0);
+ actionBar.setHomeAsUpIndicator(R.drawable.icon_action_bar_back);
+ onNavigationItemSelected(navigationView.getMenu().findItem(R.id.main_menu_input_controls));
+ navigationView.setCheckedItem(R.id.main_menu_input_controls);
+ }
+ else {
+ int selectedMenuItemId = intent.getIntExtra("selected_menu_item_id", 0);
+ int menuItemId = selectedMenuItemId > 0 ? selectedMenuItemId : R.id.main_menu_containers;
+
+ actionBar.setHomeAsUpIndicator(R.drawable.icon_action_bar_menu);
+ onNavigationItemSelected(navigationView.getMenu().findItem(menuItemId));
+ navigationView.setCheckedItem(menuItemId);
+ if (!requestAppPermissions()) OBBImageInstaller.installIfNeeded(this);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == PERMISSION_WRITE_EXTERNAL_STORAGE_REQUEST_CODE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ OBBImageInstaller.installIfNeeded(this);
+ }
+ else finish();
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == MainActivity.OPEN_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+ if (openFileCallback != null) {
+ openFileCallback.call(data.getData());
+ openFileCallback = null;
+ }
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ List fragments = fragmentManager.getFragments();
+ for (Fragment fragment : fragments) {
+ if (fragment instanceof ContainersFragment && fragment.isVisible()) {
+ finish();
+ return;
+ }
+ }
+
+ show(new ContainersFragment());
+ }
+
+ public void setOpenFileCallback(Callback openFileCallback) {
+ this.openFileCallback = openFileCallback;
+ }
+
+ private boolean requestAppPermissions() {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
+ ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) return false;
+
+ String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE};
+ ActivityCompat.requestPermissions(this, permissions, PERMISSION_WRITE_EXTERNAL_STORAGE_REQUEST_CODE);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ if (menuItem.getItemId() == R.id.containers_menu_add) {
+ return super.onOptionsItemSelected(menuItem);
+ }
+ else {
+ if (editInputControls) {
+ setResult(RESULT_OK);
+ finish();
+ }
+ else drawerLayout.openDrawer(GravityCompat.START);
+ return true;
+ }
+ }
+
+ @Override
+ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ if (fragmentManager.getBackStackEntryCount() > 0) {
+ fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
+
+ switch (item.getItemId()) {
+ case R.id.main_menu_shortcuts:
+ show(new ShortcutsFragment());
+ break;
+ case R.id.main_menu_containers:
+ show(new ContainersFragment());
+ break;
+ case R.id.main_menu_input_controls:
+ show(new InputControlsFragment(selectedProfileId));
+ break;
+ case R.id.main_menu_settings:
+ show(new SettingsFragment());
+ break;
+ case R.id.main_menu_about:
+ showAboutDialog();
+ break;
+ }
+ return true;
+ }
+
+ private void show(Fragment fragment) {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ fragmentManager.beginTransaction()
+ .replace(R.id.FLFragmentContainer, fragment)
+ .commit();
+
+ drawerLayout.closeDrawer(GravityCompat.START);
+ }
+
+ private void showAboutDialog() {
+ ContentDialog dialog = new ContentDialog(this, R.layout.about_dialog);
+ dialog.findViewById(R.id.LLBottomBar).setVisibility(View.GONE);
+
+ try {
+ final PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+
+ TextView tvDeveloper = dialog.findViewById(R.id.TVDeveloper);
+ tvDeveloper.setText(Html.fromHtml("by BrunoSX (winlator.org)", Html.FROM_HTML_MODE_LEGACY));
+ tvDeveloper.setMovementMethod(LinkMovementMethod.getInstance());
+
+ ((TextView)dialog.findViewById(R.id.TVAppVersion)).setText(getString(R.string.app_version)+" "+pInfo.versionName);
+ ((TextView)dialog.findViewById(R.id.TVOBBImageVersion)).setText(getString(R.string.obb_image_version)+" "+ImageFs.find(this).getFormattedVersion());
+
+ String creditsAndThirdPartyAppsHTML = String.join("
",
+ "Ubuntu RootFs (Focal Fossa)",
+ "Wine (winehq.org)",
+ "Box86/Box64 by ptitseb",
+ "PRoot (proot-me.github.io)",
+ "Mesa3D (mesa3d.org)",
+ "DXVK (github.com/doitsujin/dxvk)",
+ "D8VK (github.com/AlpyneDreams/d8vk)",
+ "CNC DDraw (github.com/FunkyFr3sh/cnc-ddraw)"
+ );
+
+ TextView tvCreditsAndThirdPartyApps = dialog.findViewById(R.id.TVCreditsAndThirdPartyApps);
+ tvCreditsAndThirdPartyApps.setText(Html.fromHtml(creditsAndThirdPartyAppsHTML, Html.FROM_HTML_MODE_LEGACY));
+ tvCreditsAndThirdPartyApps.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ catch (PackageManager.NameNotFoundException e) {}
+
+ dialog.show();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/SettingsFragment.java b/app/src/main/java/com/winlator/SettingsFragment.java
new file mode 100644
index 0000000..ad0cb2b
--- /dev/null
+++ b/app/src/main/java/com/winlator/SettingsFragment.java
@@ -0,0 +1,354 @@
+package com.winlator;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.collection.ArrayMap;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.preference.PreferenceManager;
+
+import com.google.android.material.navigation.NavigationView;
+import com.winlator.box86_64.Box86_64Preset;
+import com.winlator.box86_64.Box86_64PresetManager;
+import com.winlator.container.Container;
+import com.winlator.container.ContainerManager;
+import com.winlator.box86_64.Box86_64EditPresetDialog;
+import com.winlator.core.AppUtils;
+import com.winlator.core.Callback;
+import com.winlator.core.FileUtils;
+import com.winlator.core.GPUInformation;
+import com.winlator.core.OBBImageInstaller;
+import com.winlator.core.PreloaderDialog;
+import com.winlator.core.StringUtils;
+import com.winlator.core.WineInfo;
+import com.winlator.core.WineUtils;
+import com.winlator.contentdialog.ContentDialog;
+import com.winlator.xenvironment.ImageFs;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Executors;
+
+public class SettingsFragment extends Fragment {
+ public static final String DEFAULT_BOX86_VERSION = "0.3.0";
+ public static final String DEFAULT_BOX64_VERSION = "0.2.5";
+ private Callback selectWineFileCallback;
+ private PreloaderDialog preloaderDialog;
+ private SharedPreferences preferences;
+
+ public static String getDefaultTurnipVersion(Context context) {
+ return GPUInformation.isAdreno6xx(context) ? "23.1.6" : "23.3.0";
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(false);
+ preloaderDialog = new PreloaderDialog(getActivity());
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle(R.string.settings);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == MainActivity.OPEN_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+ try {
+ if (selectWineFileCallback != null && data != null) selectWineFileCallback.call(data.getData());
+ }
+ catch (Exception e) {
+ AppUtils.showToast(getContext(), R.string.unable_to_import_profile);
+ }
+ selectWineFileCallback = null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.settings_fragment, container, false);
+ final Context context = getContext();
+ preferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+ final Spinner sBox86Version = view.findViewById(R.id.SBox86Version);
+ AppUtils.setSpinnerSelectionFromIdentifier(sBox86Version, preferences.getString("box86_version", DEFAULT_BOX86_VERSION));
+
+ final Spinner sBox64Version = view.findViewById(R.id.SBox64Version);
+ AppUtils.setSpinnerSelectionFromIdentifier(sBox64Version, preferences.getString("box64_version", DEFAULT_BOX64_VERSION));
+
+ final Spinner sBox86Preset = view.findViewById(R.id.SBox86Preset);
+ final Spinner sBox64Preset = view.findViewById(R.id.SBox64Preset);
+ loadBox86_64PresetSpinners(view, sBox86Preset, sBox64Preset);
+
+ final Spinner sTurnipVersion = view.findViewById(R.id.STurnipVersion);
+ AppUtils.setSpinnerSelectionFromIdentifier(sTurnipVersion, preferences.getString("turnip_version", getDefaultTurnipVersion(context)));
+
+ final CheckBox cbUseDRI3 = view.findViewById(R.id.CBUseDRI3);
+ cbUseDRI3.setChecked(preferences.getBoolean("use_dri3", true));
+
+ final TextView tvCursorSpeed = view.findViewById(R.id.TVCursorSpeed);
+ final SeekBar sbCursorSpeed = view.findViewById(R.id.SBCursorSpeed);
+ sbCursorSpeed.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ tvCursorSpeed.setText(progress+"%");
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {}
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {}
+ });
+ sbCursorSpeed.setProgress((int)(preferences.getFloat("cursor_speed", 1.0f) * 100));
+
+ loadInstalledWineList(view);
+
+ view.findViewById(R.id.BTSelectWineFile).setOnClickListener((v) -> {
+ selectWineFileCallback = (uri) -> {
+ preloaderDialog.show(R.string.preparing_installation);
+ WineUtils.extractWineFileForInstallAsync(context, uri, (wineDir) -> {
+ if (wineDir != null) {
+ WineUtils.findWineVersionAsync(context, wineDir, (wineInfo) -> {
+ preloaderDialog.closeOnUiThread();
+ if (wineInfo == null) {
+ AppUtils.showToast(context, R.string.unable_to_install_wine);
+ return;
+ }
+
+ getActivity().runOnUiThread(() -> showWineInstallOptionsDialog(wineInfo));
+ });
+ }
+ else {
+ AppUtils.showToast(context, R.string.unable_to_install_wine);
+ preloaderDialog.closeOnUiThread();
+ }
+ });
+ };
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("*/*");
+ getActivity().startActivityFromFragment(this, intent, MainActivity.OPEN_FILE_REQUEST_CODE);
+ });
+
+ final Runnable updateUI = () -> {
+ String obbImageVersion = ImageFs.find(context).getFormattedVersion();
+ ((TextView)view.findViewById(R.id.TVOBBImageVersion)).setText(context.getString(R.string.installed_version)+" "+obbImageVersion);
+ };
+ updateUI.run();
+
+ view.findViewById(R.id.BTInstallOBBImage).setOnClickListener((v) -> {
+ final MainActivity activity = (MainActivity)getActivity();
+ PopupMenu popupMenu = new PopupMenu(context, v);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) popupMenu.setForceShowIcon(true);
+ popupMenu.inflate(R.menu.open_file_popup_menu);
+ popupMenu.setOnMenuItemClickListener((menuItem) -> {
+ int itemId = menuItem.getItemId();
+ if (itemId == R.id.open_file) {
+ OBBImageInstaller.openFileForInstall(activity, updateUI);
+ }
+ else if (itemId == R.id.download_file) {
+ OBBImageInstaller.downloadFileForInstall(activity, updateUI);
+ }
+ return true;
+ });
+ popupMenu.show();
+ });
+
+ view.findViewById(R.id.BTConfirm).setOnClickListener((v) -> {
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putString("box86_version", StringUtils.parseIdentifier(sBox86Version.getSelectedItem()));
+ editor.putString("box64_version", StringUtils.parseIdentifier(sBox64Version.getSelectedItem()));
+ editor.putString("box86_preset", Box86_64PresetManager.getSpinnerSelectedId(sBox86Preset));
+ editor.putString("box64_preset", Box86_64PresetManager.getSpinnerSelectedId(sBox64Preset));
+ editor.putString("turnip_version", StringUtils.parseIdentifier(sTurnipVersion.getSelectedItem()));
+ editor.putBoolean("use_dri3", cbUseDRI3.isChecked());
+ editor.putFloat("cursor_speed", sbCursorSpeed.getProgress() / 100.0f);
+
+ if (editor.commit()) {
+ NavigationView navigationView = getActivity().findViewById(R.id.NavigationView);
+ navigationView.setCheckedItem(R.id.main_menu_containers);
+ FragmentManager fragmentManager = getParentFragmentManager();
+ fragmentManager.beginTransaction()
+ .replace(R.id.FLFragmentContainer, new ContainersFragment())
+ .commit();
+ }
+ });
+
+ return view;
+ }
+
+ private void loadBox86_64PresetSpinners(View view, final Spinner sBox86Preset, final Spinner sBox64Preset) {
+ final ArrayMap spinners = new ArrayMap() {{
+ put("box86", sBox86Preset);
+ put("box64", sBox64Preset);
+ }};
+ final Context context = getContext();
+
+ Callback updateSpinner = (prefix) -> {
+ Box86_64PresetManager.loadSpinner(prefix, spinners.get(prefix), preferences.getString(prefix+"_preset", Box86_64Preset.COMPATIBILITY));
+ };
+
+ Callback onAddPreset = (prefix) -> {
+ Box86_64EditPresetDialog dialog = new Box86_64EditPresetDialog(context, prefix, null);
+ dialog.setOnConfirmCallback(() -> updateSpinner.call(prefix));
+ dialog.show();
+ };
+
+ Callback onEditPreset = (prefix) -> {
+ Box86_64EditPresetDialog dialog = new Box86_64EditPresetDialog(context, prefix, Box86_64PresetManager.getSpinnerSelectedId(spinners.get(prefix)));
+ dialog.setOnConfirmCallback(() -> updateSpinner.call(prefix));
+ dialog.show();
+ };
+
+ Callback onDuplicatePreset = (prefix) -> ContentDialog.confirm(context, R.string.do_you_want_to_duplicate_this_preset, () -> {
+ Spinner spinner = spinners.get(prefix);
+ Box86_64PresetManager.duplicatePreset(prefix, context, Box86_64PresetManager.getSpinnerSelectedId(spinner));
+ updateSpinner.call(prefix);
+ spinner.setSelection(spinner.getCount()-1);
+ });
+
+ Callback onRemovePreset = (prefix) -> {
+ final String presetId = Box86_64PresetManager.getSpinnerSelectedId(spinners.get(prefix));
+ if (!presetId.startsWith(Box86_64Preset.CUSTOM)) {
+ AppUtils.showToast(context, R.string.you_cannot_remove_this_preset);
+ return;
+ }
+ ContentDialog.confirm(context, R.string.do_you_want_to_remove_this_preset, () -> {
+ Box86_64PresetManager.removePreset(prefix, context, presetId);
+ updateSpinner.call(prefix);
+ });
+ };
+
+ updateSpinner.call("box86");
+ updateSpinner.call("box64");
+
+ view.findViewById(R.id.BTAddBox86Preset).setOnClickListener((v) -> onAddPreset.call("box86"));
+ view.findViewById(R.id.BTEditBox86Preset).setOnClickListener((v) -> onEditPreset.call("box86"));
+ view.findViewById(R.id.BTDuplicateBox86Preset).setOnClickListener((v) -> onDuplicatePreset.call("box86"));
+ view.findViewById(R.id.BTRemoveBox86Preset).setOnClickListener((v) -> onRemovePreset.call("box86"));
+
+ view.findViewById(R.id.BTAddBox64Preset).setOnClickListener((v) -> onAddPreset.call("box64"));
+ view.findViewById(R.id.BTEditBox64Preset).setOnClickListener((v) -> onEditPreset.call("box64"));
+ view.findViewById(R.id.BTDuplicateBox64Preset).setOnClickListener((v) -> onDuplicatePreset.call("box64"));
+ view.findViewById(R.id.BTRemoveBox64Preset).setOnClickListener((v) -> onRemovePreset.call("box64"));
+ }
+
+ private void removeInstalledWine(WineInfo wineInfo, Runnable onSuccess) {
+ final Activity activity = getActivity();
+ ContainerManager manager = new ContainerManager(activity);
+
+ ArrayList containers = manager.getContainers();
+ for (Container container : containers) {
+ if (container.getWineVersion().equals(wineInfo.identifier())) {
+ AppUtils.showToast(activity, R.string.unable_to_remove_this_wine_version);
+ return;
+ }
+ }
+
+ String suffix = wineInfo.fullVersion()+"-"+wineInfo.getArch();
+ File installedWineDir = ImageFs.find(activity).getInstalledWineDir();
+ File wineDir = new File(wineInfo.path);
+ File containerPatternFile = new File(installedWineDir, "container-pattern-"+suffix+".tzst");
+
+ if (!wineDir.isDirectory() || !containerPatternFile.isFile()) {
+ AppUtils.showToast(activity, R.string.unable_to_remove_this_wine_version);
+ return;
+ }
+
+ preloaderDialog.show(R.string.removing_wine);
+ Executors.newSingleThreadExecutor().execute(() -> {
+ FileUtils.delete(wineDir);
+ FileUtils.delete(containerPatternFile);
+ preloaderDialog.closeOnUiThread();
+ if (onSuccess != null) activity.runOnUiThread(onSuccess);
+ });
+ }
+
+ private void loadInstalledWineList(final View view) {
+ Context context = getContext();
+ LinearLayout container = view.findViewById(R.id.LLInstalledWineList);
+ container.removeAllViews();
+ ArrayList wineInfos = WineUtils.getInstalledWineInfos(context);
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ for (final WineInfo wineInfo : wineInfos) {
+ View itemView = inflater.inflate(R.layout.installed_wine_list_item, container, false);
+ ((TextView)itemView.findViewById(R.id.TVTitle)).setText(wineInfo.toString());
+ if (wineInfo != WineInfo.MAIN_WINE_VERSION) {
+ View removeButton = itemView.findViewById(R.id.BTRemove);
+ removeButton.setVisibility(View.VISIBLE);
+ removeButton.setOnClickListener((v) -> {
+ ContentDialog.confirm(getContext(), R.string.do_you_want_to_remove_this_wine_version, () -> {
+ removeInstalledWine(wineInfo, () -> loadInstalledWineList(view));
+ });
+ });
+ }
+ container.addView(itemView);
+ }
+ }
+
+ private void installWine(final WineInfo wineInfo) {
+ Context context = getContext();
+ File installedWineDir = ImageFs.find(context).getInstalledWineDir();
+
+ File wineDir = new File(installedWineDir, wineInfo.identifier());
+ if (wineDir.isDirectory()) {
+ AppUtils.showToast(context, R.string.unable_to_install_wine);
+ return;
+ }
+
+ Intent intent = new Intent(context, XServerDisplayActivity.class);
+ intent.putExtra("generate_wineprefix", true);
+ intent.putExtra("wine_info", wineInfo);
+ context.startActivity(intent);
+ }
+
+ private void showWineInstallOptionsDialog(final WineInfo wineInfo) {
+ Context context = getContext();
+ ContentDialog dialog = new ContentDialog(context, R.layout.wine_install_options_dialog);
+ dialog.setCancelable(false);
+ dialog.setCanceledOnTouchOutside(false);
+ dialog.setTitle(R.string.install_wine);
+ dialog.setIcon(R.drawable.icon_wine);
+
+ EditText etVersion = dialog.findViewById(R.id.ETVersion);
+ etVersion.setText("Wine "+wineInfo.version+(wineInfo.subversion != null ? " ("+wineInfo.subversion+")" : ""));
+
+ Spinner sArch = dialog.findViewById(R.id.SArch);
+ List archList = wineInfo.isWin64() ? Arrays.asList("x86", "x86_64") : Arrays.asList("x86");
+ sArch.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, archList));
+ sArch.setSelection(archList.size()-1);
+
+ dialog.setOnConfirmCallback(() -> {
+ wineInfo.setArch(sArch.getSelectedItem().toString());
+ installWine(wineInfo);
+ });
+ dialog.show();
+ }
+}
diff --git a/app/src/main/java/com/winlator/ShortcutsFragment.java b/app/src/main/java/com/winlator/ShortcutsFragment.java
new file mode 100644
index 0000000..e7f8dbd
--- /dev/null
+++ b/app/src/main/java/com/winlator/ShortcutsFragment.java
@@ -0,0 +1,143 @@
+package com.winlator;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.winlator.container.ContainerManager;
+import com.winlator.container.Shortcut;
+import com.winlator.contentdialog.ContentDialog;
+import com.winlator.contentdialog.ShortcutSettingsDialog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ShortcutsFragment extends Fragment {
+ private RecyclerView recyclerView;
+ private TextView emptyTextView;
+ private ContainerManager manager;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ manager = new ContainerManager(getContext());
+ loadShortcutsList();
+ ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle(R.string.shortcuts);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ FrameLayout frameLayout = (FrameLayout)inflater.inflate(R.layout.shortcuts_fragment, container, false);
+ recyclerView = frameLayout.findViewById(R.id.RecyclerView);
+ emptyTextView = frameLayout.findViewById(R.id.TVEmptyText);
+ recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext()));
+ recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));
+ return frameLayout;
+ }
+
+ public void loadShortcutsList() {
+ ArrayList shortcuts = manager.loadShortcuts();
+ recyclerView.setAdapter(new ShortcutsAdapter(shortcuts));
+ if (shortcuts.isEmpty()) emptyTextView.setVisibility(View.VISIBLE);
+ }
+
+ private class ShortcutsAdapter extends RecyclerView.Adapter {
+ private final List data;
+
+ private class ViewHolder extends RecyclerView.ViewHolder {
+ private final ImageButton menuButton;
+ private final ImageView imageView;
+ private final TextView title;
+ private final TextView subtitle;
+ private final View innerArea;
+
+ private ViewHolder(View view) {
+ super(view);
+ this.imageView = view.findViewById(R.id.ImageView);
+ this.title = view.findViewById(R.id.TVTitle);
+ this.subtitle = view.findViewById(R.id.TVSubtitle);
+ this.menuButton = view.findViewById(R.id.BTMenu);
+ this.innerArea = view.findViewById(R.id.LLInnerArea);
+ }
+ }
+
+ public ShortcutsAdapter(List data) {
+ this.data = data;
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.shortcut_list_item, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ final Shortcut item = data.get(position);
+ if (item.icon != null) holder.imageView.setImageBitmap(item.icon);
+ holder.title.setText(item.name);
+ holder.subtitle.setText(item.container.getName());
+ holder.menuButton.setOnClickListener((v) -> showListItemMenu(v, item));
+ holder.innerArea.setOnClickListener((v) -> runFromShortcut(item));
+ }
+
+ @Override
+ public final int getItemCount() {
+ return data.size();
+ }
+
+ private void showListItemMenu(View anchorView, final Shortcut shortcut) {
+ final Context context = getContext();
+ PopupMenu listItemMenu = new PopupMenu(context, anchorView);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) listItemMenu.setForceShowIcon(true);
+
+ listItemMenu.inflate(R.menu.shortcut_popup_menu);
+ listItemMenu.setOnMenuItemClickListener((menuItem) -> {
+ int itemId = menuItem.getItemId();
+ if (itemId == R.id.shortcut_settings) {
+ (new ShortcutSettingsDialog(ShortcutsFragment.this, shortcut)).show();
+ }
+ else if (itemId == R.id.shortcut_remove) {
+ ContentDialog.confirm(context, R.string.do_you_want_to_remove_this_shortcut, () -> {
+ if (shortcut.file.delete() && shortcut.iconFile != null) shortcut.iconFile.delete();
+ loadShortcutsList();
+ });
+ }
+ return true;
+ });
+ listItemMenu.show();
+ }
+
+ private void runFromShortcut(Shortcut shortcut) {
+ Context context = getContext();
+ Intent intent = new Intent(context, XServerDisplayActivity.class);
+ intent.putExtra("container_id", shortcut.container.id);
+ intent.putExtra("shortcut_path", shortcut.file.getPath());
+ context.startActivity(intent);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/XServerDisplayActivity.java b/app/src/main/java/com/winlator/XServerDisplayActivity.java
new file mode 100644
index 0000000..4a19be7
--- /dev/null
+++ b/app/src/main/java/com/winlator/XServerDisplayActivity.java
@@ -0,0 +1,781 @@
+package com.winlator;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.FrameLayout;
+import android.widget.Spinner;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.view.GravityCompat;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.preference.PreferenceManager;
+
+import com.google.android.material.navigation.NavigationView;
+import com.winlator.container.Container;
+import com.winlator.container.ContainerManager;
+import com.winlator.container.Shortcut;
+import com.winlator.core.AppUtils;
+import com.winlator.core.CursorLocker;
+import com.winlator.core.EnvVars;
+import com.winlator.core.FileUtils;
+import com.winlator.core.OnExtractFileListener;
+import com.winlator.core.PreloaderDialog;
+import com.winlator.core.ProcessHelper;
+import com.winlator.core.TarZstdUtils;
+import com.winlator.core.WineInfo;
+import com.winlator.core.WineRegistryEditor;
+import com.winlator.core.WineStartMenuCreator;
+import com.winlator.core.WineUtils;
+import com.winlator.inputcontrols.ControlsProfile;
+import com.winlator.inputcontrols.ExternalController;
+import com.winlator.inputcontrols.InputControlsManager;
+import com.winlator.renderer.GLRenderer;
+import com.winlator.contentdialog.ContentDialog;
+import com.winlator.widget.InputControlsView;
+import com.winlator.widget.TouchpadView;
+import com.winlator.widget.XServerView;
+import com.winlator.winhandler.TaskManagerDialog;
+import com.winlator.winhandler.WinHandler;
+import com.winlator.xconnector.UnixSocketConfig;
+import com.winlator.xenvironment.ImageFs;
+import com.winlator.xenvironment.XEnvironment;
+import com.winlator.xenvironment.components.ALSAServerComponent;
+import com.winlator.xenvironment.components.EtcHostsFileUpdateComponent;
+import com.winlator.xenvironment.components.GuestProgramLauncherComponent;
+import com.winlator.xenvironment.components.PulseAudioComponent;
+import com.winlator.xenvironment.components.SysVSharedMemoryComponent;
+import com.winlator.xenvironment.components.VirGLRendererComponent;
+import com.winlator.xenvironment.components.XServerComponent;
+import com.winlator.xserver.ScreenInfo;
+import com.winlator.xserver.Window;
+import com.winlator.xserver.WindowManager;
+import com.winlator.xserver.XServer;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.concurrent.Executors;
+
+public class XServerDisplayActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
+ private XServerView xServerView;
+ private InputControlsView inputControlsView;
+ private TouchpadView touchpadView;
+ private XEnvironment environment;
+ private DrawerLayout drawerLayout;
+ private ContainerManager containerManager;
+ private Container container;
+ private XServer xServer;
+ private InputControlsManager inputControlsManager;
+ private ImageFs imageFs;
+ private Runnable editInputControlsCallback;
+ private Shortcut shortcut;
+ private String graphicsDriver = Container.DEFAULT_GRAPHICS_DRIVER;
+ private String audioDriver = Container.DEFAULT_AUDIO_DRIVER;
+ private WineInfo wineInfo;
+ private final EnvVars envVars = new EnvVars();
+ private boolean firstTimeBoot = false;
+ private SharedPreferences preferences;
+ private OnExtractFileListener onExtractFileListener;
+ private final WinHandler winHandler = new WinHandler();
+ private float globalCursorSpeed = 1.0f;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ AppUtils.hideSystemUI(this);
+ AppUtils.keepScreenOn(this);
+ setContentView(R.layout.xserver_display_activity);
+
+ final PreloaderDialog preloaderDialog = new PreloaderDialog(this);
+ preloaderDialog.show(R.string.starting_up);
+
+ preferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ drawerLayout = findViewById(R.id.DrawerLayout);
+ drawerLayout.setOnApplyWindowInsetsListener((view, windowInsets) -> windowInsets.replaceSystemWindowInsets(0, 0, 0, 0));
+ drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
+
+ NavigationView navigationView = findViewById(R.id.NavigationView);
+ navigationView.setNavigationItemSelectedListener(this);
+
+ imageFs = ImageFs.find(this);
+
+ String screenSize = Container.DEFAULT_SCREEN_SIZE;
+ if (!isGenerateWineprefix()) {
+ containerManager = new ContainerManager(this);
+ container = containerManager.getContainerById(getIntent().getIntExtra("container_id", 0));
+ containerManager.activateContainer(container);
+
+ firstTimeBoot = container.getExtra("appVersion").isEmpty();
+
+ String wineVersion = container.getWineVersion();
+ wineInfo = WineInfo.fromIdentifier(this, wineVersion);
+ if (wineInfo != WineInfo.MAIN_WINE_VERSION) imageFs.setWinePath(wineInfo.path);
+
+ String shortcutPath = getIntent().getStringExtra("shortcut_path");
+ if (shortcutPath != null && !shortcutPath.isEmpty()) shortcut = new Shortcut(container, new File(shortcutPath));
+
+ graphicsDriver = container.getGraphicsDriver();
+ audioDriver = container.getAudioDriver();
+ screenSize = container.getScreenSize();
+
+ if (shortcut != null) {
+ graphicsDriver = shortcut.getExtra("graphicsDriver", container.getGraphicsDriver());
+ audioDriver = shortcut.getExtra("audioDriver", container.getAudioDriver());
+ screenSize = shortcut.getExtra("screenSize", container.getScreenSize());
+ }
+
+ if (!wineInfo.isWin64()) {
+ onExtractFileListener = (destination, entryName) -> {
+ if (entryName.contains("system32")) return null;
+ return new File(destination, entryName.replace("syswow64", "system32"));
+ };
+ }
+ }
+
+ inputControlsManager = new InputControlsManager(this);
+ xServer = new XServer(new ScreenInfo(screenSize));
+ xServer.setWinHandler(winHandler);
+ xServer.windowManager.addOnWindowModificationListener(new WindowManager.OnWindowModificationListener() {
+ @Override
+ public void onUpdateWindowContent(Window window) {
+ if (window.getWidth() > 1) {
+ xServerView.getRenderer().setCursorVisible(true);
+ preloaderDialog.closeOnUiThread();
+ xServer.windowManager.removeOnWindowModificationListener(this);
+ }
+ }
+ });
+
+ setupUI();
+
+ Executors.newSingleThreadExecutor().execute(() -> {
+ if (!isGenerateWineprefix()) {
+ setupWineSystemFiles();
+ extractGraphicsDriverFiles();
+ changeWineAudioDriver();
+ }
+ setupXEnvironment();
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == MainActivity.EDIT_INPUT_CONTROLS_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+ if (editInputControlsCallback != null) {
+ editInputControlsCallback.run();
+ editInputControlsCallback = null;
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ xServerView.onResume();
+ if (environment != null) environment.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (environment != null) environment.onPause();
+ xServerView.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (environment != null) environment.stopEnvironmentComponents();
+ super.onDestroy();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (environment != null) {
+ if (!drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ drawerLayout.openDrawer(GravityCompat.START);
+ }
+ else drawerLayout.closeDrawers();
+ }
+ }
+
+ @Override
+ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.main_menu_keyboard:
+ AppUtils.showKeyboard(this);
+ drawerLayout.closeDrawers();
+ break;
+ case R.id.main_menu_input_controls:
+ showInputControlsDialog();
+ drawerLayout.closeDrawers();
+ break;
+ case R.id.main_menu_toggle_fullscreen:
+ xServerView.getRenderer().toggleFullscreen();
+ drawerLayout.closeDrawers();
+ break;
+ case R.id.main_menu_task_manager:
+ (new TaskManagerDialog(this)).show();
+ drawerLayout.closeDrawers();
+ break;
+ case R.id.main_menu_touchpad_help:
+ showTouchpadHelpDialog();
+ break;
+ case R.id.main_menu_exit:
+ exit();
+ break;
+ }
+ return true;
+ }
+
+ private void exit() {
+ if (environment != null) environment.stopEnvironmentComponents();
+ AppUtils.restartApplication(this);
+ }
+
+ private void setupWineSystemFiles() {
+ File rootDir = imageFs.getRootDir();
+ String appVersion = String.valueOf(AppUtils.getVersionCode(this));
+ String imgVersion = String.valueOf(imageFs.getVersion());
+ boolean containerDataChanged = false;
+
+ if (!container.getExtra("appVersion").equals(appVersion) || !container.getExtra("imgVersion").equals(imgVersion)) {
+ File pulseaudioDir = new File(getFilesDir(), "pulseaudio");
+ TarZstdUtils.extract(this, "patches.tzst", rootDir, onExtractFileListener);
+ TarZstdUtils.extract(this, "pulseaudio.tzst", pulseaudioDir);
+ WineUtils.applyRegistryKeyTweaks(this);
+ container.putExtra("appVersion", appVersion);
+ container.putExtra("imgVersion", imgVersion);
+ containerDataChanged = true;
+ }
+
+ String dxwrapper = shortcut != null ? shortcut.getExtra("dxwrapper", container.getDXWrapper()) : container.getDXWrapper();
+ if (!dxwrapper.equals(container.getExtra("dxwrapper"))) {
+ extractDXWrapperFiles(dxwrapper);
+ container.putExtra("dxwrapper", dxwrapper);
+ containerDataChanged = true;
+ }
+ if (dxwrapper.equals("cnc-ddraw")) envVars.put("CNC_DDRAW_CONFIG_FILE", "C:\\ProgramData\\cnc-ddraw\\ddraw.ini");
+
+ String dxcomponents = shortcut != null ? shortcut.getExtra("dxcomponents", container.getDXComponents()) : container.getDXComponents();
+ if (!dxcomponents.equals(container.getExtra("dxcomponents"))) {
+ extractDXComponentFiles();
+ container.putExtra("dxcomponents", dxcomponents);
+ containerDataChanged = true;
+ }
+
+ if (containerDataChanged) container.saveData();
+ WineStartMenuCreator.create(this, container);
+
+ WineUtils.createDosdevicesSymlinks(container);
+ }
+
+ private void setupXEnvironment() {
+ envVars.put("MESA_DEBUG", "silent");
+ envVars.put("MESA_NO_ERROR", "1");
+ envVars.put("WINEPREFIX", ImageFs.WINEPREFIX);
+ if (MainActivity.DEBUG_LEVEL <= 1) envVars.put("WINEDEBUG", "-all");
+
+ String rootPath = imageFs.getRootDir().getPath();
+
+ GuestProgramLauncherComponent guestProgramLauncherComponent = new GuestProgramLauncherComponent();
+
+ if (container != null) {
+ if (container.isStopServicesOnStartup()) winHandler.killProcess("services.exe");
+
+ String wineLoader = wineInfo.isWin64() ? "wine64" : "wine";
+ String guestExecutable = wineLoader+" explorer /desktop=shell,"+xServer.screenInfo+" "+getWineStartCommand();
+ guestProgramLauncherComponent.setGuestExecutable(guestExecutable);
+
+ if (container.isShowFPS()) {
+ envVars.put("GALLIUM_HUD", "simple,fps");
+ envVars.put("DXVK_HUD", "fps");
+ }
+ envVars.putAll(container.getEnvVars());
+ if (shortcut != null) envVars.putAll(shortcut.getExtra("envVars"));
+
+ ArrayList bindingPaths = new ArrayList<>();
+ for (String[] drive : container.drivesIterator()) bindingPaths.add(drive[1]);
+ guestProgramLauncherComponent.setBindingPaths(bindingPaths.toArray(new String[0]));
+ guestProgramLauncherComponent.setCpuList(container.getCPUList());
+ guestProgramLauncherComponent.setBox86Preset(shortcut != null ? shortcut.getExtra("box86Preset", container.getBox86Preset()) : container.getBox86Preset());
+ guestProgramLauncherComponent.setBox64Preset(shortcut != null ? shortcut.getExtra("box64Preset", container.getBox64Preset()) : container.getBox64Preset());
+ }
+
+ environment = new XEnvironment(this, imageFs);
+ environment.addComponent(new SysVSharedMemoryComponent(xServer, UnixSocketConfig.createSocket(rootPath, UnixSocketConfig.SYSVSHM_SERVER_PATH)));
+ environment.addComponent(new XServerComponent(xServer, UnixSocketConfig.createSocket(rootPath, UnixSocketConfig.XSERVER_PATH)));
+ environment.addComponent(new EtcHostsFileUpdateComponent());
+
+ if (audioDriver.equals("alsa")) {
+ envVars.put("ANDROID_ALSA_SERVER", UnixSocketConfig.ALSA_SERVER_PATH);
+ envVars.put("ANDROID_ASERVER_USE_SHM", "true");
+ environment.addComponent(new ALSAServerComponent(UnixSocketConfig.createSocket(rootPath, UnixSocketConfig.ALSA_SERVER_PATH)));
+ }
+ else if (audioDriver.equals("pulseaudio")) {
+ envVars.put("PULSE_SERVER", UnixSocketConfig.PULSE_SERVER_PATH);
+ environment.addComponent(new PulseAudioComponent(UnixSocketConfig.createSocket(rootPath, UnixSocketConfig.PULSE_SERVER_PATH)));
+ }
+
+ if (graphicsDriver.equals("virgl")) {
+ environment.addComponent(new VirGLRendererComponent(xServer, UnixSocketConfig.createSocket(rootPath, UnixSocketConfig.VIRGL_SERVER_PATH)));
+ }
+
+ guestProgramLauncherComponent.setEnvVars(envVars);
+ guestProgramLauncherComponent.setTerminationCallback((status) -> exit());
+ environment.addComponent(guestProgramLauncherComponent);
+
+ if (isGenerateWineprefix()) generateWineprefix();
+ environment.startEnvironmentComponents();
+
+ winHandler.start();
+ }
+
+ private void setupUI() {
+ FrameLayout container = findViewById(R.id.FLXServerDisplay);
+ xServerView = new XServerView(this, xServer);
+ final GLRenderer renderer = xServerView.getRenderer();
+ renderer.setCursorVisible(false);
+
+ if (shortcut != null) {
+ if (shortcut.getExtra("forceFullscreen", "0").equals("1")) renderer.setForceFullscreenWMClass(shortcut.wmClass);
+ renderer.setUnviewableWMClasses("explorer.exe");
+ }
+
+ xServer.setRenderer(renderer);
+ container.addView(xServerView);
+
+ globalCursorSpeed = preferences.getFloat("cursor_speed", 1.0f);
+ touchpadView = new TouchpadView(this, xServer);
+ touchpadView.setSensitivity(globalCursorSpeed);
+ touchpadView.setFourFingersTapCallback(() -> {
+ if (!drawerLayout.isDrawerOpen(GravityCompat.START)) drawerLayout.openDrawer(GravityCompat.START);
+ });
+ container.addView(touchpadView);
+
+ inputControlsView = new InputControlsView(this);
+ inputControlsView.setOverlayOpacity(preferences.getFloat("overlay_opacity", InputControlsView.DEFAULT_OVERLAY_OPACITY));
+ inputControlsView.setTouchpadView(touchpadView);
+ inputControlsView.setXServer(xServer);
+ inputControlsView.setVisibility(View.GONE);
+ container.addView(inputControlsView);
+
+ AppUtils.observeSoftKeyboardVisibility(drawerLayout, renderer::setScreenOffsetYRelativeToCursor);
+ }
+
+ private void showInputControlsDialog() {
+ final ContentDialog dialog = new ContentDialog(this, R.layout.input_controls_dialog);
+ dialog.setTitle(R.string.input_controls);
+ dialog.setIcon(R.drawable.icon_input_controls);
+
+ final Spinner sProfile = dialog.findViewById(R.id.SProfile);
+ Runnable loadProfileSpinner = () -> {
+ ArrayList profiles = inputControlsManager.getProfiles();
+ ArrayList profileItems = new ArrayList<>();
+ int selectedPosition = 0;
+ profileItems.add("-- "+getString(R.string.disabled)+" --");
+ for (int i = 0; i < profiles.size(); i++) {
+ ControlsProfile profile = profiles.get(i);
+ if (profile == inputControlsView.getProfile()) selectedPosition = i + 1;
+ profileItems.add(profile.getName());
+ }
+
+ sProfile.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, profileItems));
+ sProfile.setSelection(selectedPosition);
+ };
+ loadProfileSpinner.run();
+
+ final GLRenderer renderer = xServerView.getRenderer();
+ final CheckBox cbCursorVisible = dialog.findViewById(R.id.CBCursorVisible);
+ cbCursorVisible.setChecked(renderer.isCursorVisible());
+
+ final CheckBox cbLockCursor = dialog.findViewById(R.id.CBLockCursor);
+ cbLockCursor.setChecked(xServer.cursorLocker.getState() == CursorLocker.State.LOCKED);
+
+ final CheckBox cbShowTouchscreenControls = dialog.findViewById(R.id.CBShowTouchscreenControls);
+ cbShowTouchscreenControls.setChecked(inputControlsView.isShowTouchscreenControls());
+
+ dialog.findViewById(R.id.BTSettings).setOnClickListener((v) -> {
+ int position = sProfile.getSelectedItemPosition();
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.putExtra("edit_input_controls", true);
+ intent.putExtra("selected_profile_id", position > 0 ? inputControlsManager.getProfiles().get(position - 1).id : 0);
+ editInputControlsCallback = () -> {
+ hideInputControls();
+ inputControlsManager.loadProfiles();
+ loadProfileSpinner.run();
+ };
+ startActivityForResult(intent, MainActivity.EDIT_INPUT_CONTROLS_REQUEST_CODE);
+ });
+
+ dialog.setOnConfirmCallback(() -> {
+ renderer.setCursorVisible(cbCursorVisible.isChecked());
+ xServer.cursorLocker.setState(cbLockCursor.isChecked() ? CursorLocker.State.LOCKED : CursorLocker.State.CONFINED);
+ inputControlsView.setShowTouchscreenControls(cbShowTouchscreenControls.isChecked());
+ int position = sProfile.getSelectedItemPosition();
+ if (position > 0) {
+ showInputControls(inputControlsManager.getProfiles().get(position - 1));
+ }
+ else hideInputControls();
+ });
+
+ dialog.show();
+ }
+
+ private void showInputControls(ControlsProfile profile) {
+ inputControlsView.setVisibility(View.VISIBLE);
+ inputControlsView.requestFocus();
+ inputControlsView.setProfile(profile);
+
+ touchpadView.setSensitivity(profile.getCursorSpeed() * globalCursorSpeed);
+ touchpadView.setPointerButtonRightEnabled(false);
+
+ inputControlsView.invalidate();
+ }
+
+ private void hideInputControls() {
+ inputControlsView.setShowTouchscreenControls(true);
+ inputControlsView.setVisibility(View.GONE);
+ inputControlsView.setProfile(null);
+
+ touchpadView.setSensitivity(globalCursorSpeed);
+ touchpadView.setPointerButtonLeftEnabled(true);
+ touchpadView.setPointerButtonRightEnabled(true);
+
+ inputControlsView.invalidate();
+ }
+
+ private void extractGraphicsDriverFiles() {
+ File rootDir = imageFs.getRootDir();
+
+ FileUtils.delete(new File(rootDir, "/usr/lib/arm-linux-gnueabihf/libvulkan_freedreno.so"));
+ FileUtils.delete(new File(rootDir, "/usr/lib/aarch64-linux-gnu/libvulkan_freedreno.so"));
+ FileUtils.delete(new File(rootDir, "/usr/lib/arm-linux-gnueabihf/libGL.so.1.7.0"));
+ FileUtils.delete(new File(rootDir, "/usr/lib/aarch64-linux-gnu/libGL.so.1.7.0"));
+
+ switch (graphicsDriver) {
+ case "llvmpipe":
+ envVars.put("GALLIUM_DRIVER", "llvmpipe");
+ TarZstdUtils.extract(this, "graphics_driver/llvmpipe-23.1.6.tzst", rootDir);
+ break;
+ case "turnip-zink":
+ envVars.put("GALLIUM_DRIVER", "zink");
+ envVars.put("DXVK_STATE_CACHE_PATH", ImageFs.CACHE_PATH);
+ envVars.put("DXVK_LOG_LEVEL", "none");
+ envVars.put("TU_DEBUG", "noconform");
+ envVars.put("MESA_VK_WSI_PRESENT_MODE", "mailbox");
+ envVars.put("vblank_mode", "0");
+
+ boolean useDRI3 = preferences.getBoolean("use_dri3", true);
+ if (!useDRI3) {
+ envVars.put("MESA_VK_WSI_PRESENT_MODE", "immediate");
+ envVars.put("MESA_VK_WSI_DEBUG", "sw");
+ }
+
+ String turnipVersion = preferences.getString("turnip_version", SettingsFragment.getDefaultTurnipVersion(this));
+ TarZstdUtils.extract(this, "graphics_driver/turnip-"+turnipVersion+".tzst", rootDir);
+ TarZstdUtils.extract(this, "graphics_driver/zink-22.2.2.tzst", rootDir);
+ break;
+ case "virgl":
+ envVars.put("GALLIUM_DRIVER", "virpipe");
+ envVars.put("VIRGL_NO_READBACK", "true");
+ envVars.put("VIRGL_SERVER_PATH", UnixSocketConfig.VIRGL_SERVER_PATH);
+ envVars.put("MESA_EXTENSION_OVERRIDE", "-GL_EXT_vertex_array_bgra -GL_EXT_texture_sRGB_decode -GL_ARB_ES2_compatibility");
+ envVars.put("MESA_GL_VERSION_OVERRIDE", "3.1");
+ envVars.put("vblank_mode", "0");
+ TarZstdUtils.extract(this, "graphics_driver/virgl-22.1.7.tzst", rootDir);
+ break;
+ }
+ }
+
+ private void showTouchpadHelpDialog() {
+ ContentDialog dialog = new ContentDialog(this, R.layout.touchpad_help_dialog);
+ dialog.setTitle(R.string.touchpad_help);
+ dialog.setIcon(R.drawable.icon_help);
+ dialog.findViewById(R.id.BTCancel).setVisibility(View.GONE);
+ dialog.show();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return (!inputControlsView.onKeyEvent(event) && xServer.keyboard.onKeyEvent(event)) ||
+ (!ExternalController.isGameController(event.getDevice()) && super.dispatchKeyEvent(event));
+ }
+
+ private void generateWineprefix() {
+ Intent intent = getIntent();
+
+ final File rootDir = imageFs.getRootDir();
+ final File installedWineDir = imageFs.getInstalledWineDir();
+ wineInfo = intent.getParcelableExtra("wine_info");
+ envVars.put("WINEARCH", wineInfo.isWin64() ? "win64" : "win32");
+ imageFs.setWinePath(wineInfo.path);
+
+ final File containerPatternDir = new File(installedWineDir, "/preinstall/container-pattern");
+ if (containerPatternDir.isDirectory()) FileUtils.delete(containerPatternDir);
+ containerPatternDir.mkdirs();
+
+ File linkFile = new File(rootDir, ImageFs.HOME_PATH);
+ linkFile.delete();
+ FileUtils.symlink(".."+FileUtils.toRelativePath(rootDir.getPath(), containerPatternDir.getPath()), linkFile.getPath());
+
+ String wineLoader = wineInfo.isWin64() ? "wine64" : "wine";
+ GuestProgramLauncherComponent guestProgramLauncherComponent = environment.getComponent(GuestProgramLauncherComponent.class);
+ guestProgramLauncherComponent.setGuestExecutable(wineLoader+" explorer /desktop=shell,"+Container.DEFAULT_SCREEN_SIZE+" winecfg");
+
+ final PreloaderDialog preloaderDialog = new PreloaderDialog(this);
+ guestProgramLauncherComponent.setTerminationCallback((status) -> Executors.newSingleThreadExecutor().execute(() -> {
+ if (status > 0) {
+ AppUtils.showToast(this, R.string.unable_to_install_wine);
+ FileUtils.delete(new File(installedWineDir, "/preinstall"));
+ AppUtils.restartApplication(this);
+ return;
+ }
+
+ preloaderDialog.showOnUiThread(R.string.finishing_installation);
+ FileUtils.writeString(new File(rootDir, ImageFs.WINEPREFIX+"/.update-timestamp"), "disable\n");
+
+ File userDir = new File(rootDir, ImageFs.WINEPREFIX+"/drive_c/users/xuser");
+ File[] userFiles = userDir.listFiles();
+ if (userFiles != null) {
+ for (File userFile : userFiles) {
+ if (FileUtils.isSymlink(userFile)) {
+ String path = userFile.getPath();
+ userFile.delete();
+ (new File(path)).mkdirs();
+ }
+ }
+ }
+
+ String suffix = wineInfo.fullVersion()+"-"+wineInfo.getArch();
+ File containerPatternFile = new File(installedWineDir, "/preinstall/container-pattern-"+suffix+".tzst");
+ TarZstdUtils.compress(new File(rootDir, ImageFs.WINEPREFIX), containerPatternFile, MainActivity.CONTAINER_PATTERN_COMPRESSION_LEVEL);
+
+ if (!containerPatternFile.renameTo(new File(installedWineDir, containerPatternFile.getName())) ||
+ !(new File(wineInfo.path)).renameTo(new File(installedWineDir, wineInfo.identifier()))) {
+ containerPatternFile.delete();
+ }
+
+ FileUtils.delete(new File(installedWineDir, "/preinstall"));
+
+ preloaderDialog.closeOnUiThread();
+ AppUtils.restartApplication(this, R.id.main_menu_settings);
+ }));
+ }
+
+ private void extractDXWrapperFiles(String dxwrapper) {
+ final String[] dlls = {"d3d10.dll", "d3d10_1.dll", "d3d10core.dll", "d3d11.dll", "d3d8.dll", "d3d9.dll", "dxgi.dll", "ddraw.dll", "wined3d.dll"};
+ if (firstTimeBoot) cloneOriginalDllFiles(dlls);
+ File rootDir = imageFs.getRootDir();
+ File windowsDir = new File(rootDir, ImageFs.WINEPREFIX+"/drive_c/windows");
+
+ if (dxwrapper.equals("original-wined3d")) {
+ restoreOriginalDllFiles(dlls);
+ }
+ else if (dxwrapper.equals("cnc-ddraw")) {
+ restoreOriginalDllFiles(dlls);
+ File configFile = new File(rootDir, ImageFs.WINEPREFIX+"/drive_c/ProgramData/cnc-ddraw/ddraw.ini");
+ if (!configFile.isFile()) FileUtils.copy(this, "dxwrapper/cnc-ddraw/ddraw.ini", configFile);
+ File shadersDir = new File(rootDir, ImageFs.WINEPREFIX+"/drive_c/ProgramData/cnc-ddraw/Shaders");
+ if (!shadersDir.isDirectory()) FileUtils.copy(this, "dxwrapper/cnc-ddraw/Shaders", shadersDir);
+ TarZstdUtils.extract(this, "dxwrapper/cnc-ddraw/ddraw.tzst", windowsDir, onExtractFileListener);
+ }
+ else {
+ restoreOriginalDllFiles("ddraw.dll");
+ TarZstdUtils.extract(this, "dxwrapper/"+dxwrapper+".tzst", windowsDir, onExtractFileListener);
+ }
+ }
+
+ private void extractDXComponentFiles() {
+ File rootDir = imageFs.getRootDir();
+ File dxcomponentsDir = new File(rootDir, "/opt/resources/dxcomponents");
+ File windowsDir = new File(rootDir, ImageFs.WINEPREFIX+"/drive_c/windows");
+ File systemRegFile = new File(rootDir, ImageFs.WINEPREFIX+"/system.reg");
+
+ try {
+ JSONObject dxcomponentsJSONObject = new JSONObject(FileUtils.readString(this, "dxcomponents.json"));
+ ArrayList dlls = new ArrayList<>();
+ String dxcomponents = shortcut != null ? shortcut.getExtra("dxcomponents", container.getDXComponents()) : container.getDXComponents();
+
+ if (firstTimeBoot) {
+ for (String[] dxcomponent : Container.dxcomponentsIterator(dxcomponents)) {
+ JSONArray libNames = dxcomponentsJSONObject.getJSONArray(dxcomponent[0]);
+ for (int i = 0; i < libNames.length(); i++) {
+ String libName = libNames.getString(i);
+ dlls.add(!libName.endsWith(".exe") ? libName+".dll" : libName);
+ }
+ }
+
+ cloneOriginalDllFiles(dlls.toArray(new String[0]));
+ dlls.clear();
+ }
+
+ Iterator oldDXComponentsIter = Container.dxcomponentsIterator(container.getExtra("dxcomponents", dxcomponents)).iterator();
+
+ for (String[] dxcomponent : Container.dxcomponentsIterator(dxcomponents)) {
+ if (dxcomponent[1].equals(oldDXComponentsIter.next()[1])) continue;
+ String identifier = dxcomponent[0];
+ boolean useNative = dxcomponent[1].equals("1");
+
+ if (useNative) {
+ TarZstdUtils.extract(new File(dxcomponentsDir, identifier+".tzst"), windowsDir, onExtractFileListener);
+ }
+ else {
+ JSONArray libNames = dxcomponentsJSONObject.getJSONArray(identifier);
+ for (int i = 0; i < libNames.length(); i++) {
+ String libName = libNames.getString(i);
+ dlls.add(!libName.endsWith(".exe") ? libName+".dll" : libName);
+ }
+ }
+
+ if (identifier.equals("directsound")) {
+ try (WineRegistryEditor registryEditor = new WineRegistryEditor(systemRegFile)) {
+ final String key64 = "Software\\Classes\\CLSID\\{083863F1-70DE-11D0-BD40-00A0C911CE86}\\Instance\\{E30629D1-27E5-11CE-875D-00608CB78066}";
+ final String key32 = "Software\\Classes\\Wow6432Node\\CLSID\\{083863F1-70DE-11D0-BD40-00A0C911CE86}\\Instance\\{E30629D1-27E5-11CE-875D-00608CB78066}";
+
+ if (useNative) {
+ registryEditor.setStringValue(key32, "CLSID", "{E30629D1-27E5-11CE-875D-00608CB78066}");
+ registryEditor.setHexValue(key32, "FilterData", "02000000000080000100000000000000307069330200000000000000010000000000000000000000307479330000000038000000480000006175647300001000800000aa00389b710100000000001000800000aa00389b71");
+ registryEditor.setStringValue(key32, "FriendlyName", "Wave Audio Renderer");
+
+ registryEditor.setStringValue(key64, "CLSID", "{E30629D1-27E5-11CE-875D-00608CB78066}");
+ registryEditor.setHexValue(key64, "FilterData", "02000000000080000100000000000000307069330200000000000000010000000000000000000000307479330000000038000000480000006175647300001000800000aa00389b710100000000001000800000aa00389b71");
+ registryEditor.setStringValue(key64, "FriendlyName", "Wave Audio Renderer");
+ }
+ else {
+ registryEditor.removeKey(key32);
+ registryEditor.removeKey(key64);
+ }
+ }
+ }
+ }
+
+ if (!dlls.isEmpty()) restoreOriginalDllFiles(dlls.toArray(new String[0]));
+ WineUtils.overrideDXComponentDlls(this, container, dxcomponents);
+ }
+ catch (JSONException e) {}
+ }
+
+ private void restoreOriginalDllFiles(final String... dlls) {
+ File rootDir = imageFs.getRootDir();
+ File cacheDir = new File(rootDir, ImageFs.CACHE_PATH+"/original_dlls");
+ if (cacheDir.isDirectory()) {
+ File windowsDir = new File(rootDir, ImageFs.WINEPREFIX+"/drive_c/windows");
+ String[] dirnames = cacheDir.list();
+ int filesCopied = 0;
+
+ for (String dll : dlls) {
+ boolean success = false;
+ for (String dirname : dirnames) {
+ File srcFile = new File(cacheDir, dirname+"/"+dll);
+ File dstFile = new File(windowsDir, dirname+"/"+dll);
+ if (FileUtils.copy(srcFile, dstFile)) success = true;
+ }
+ if (success) filesCopied++;
+ }
+
+ if (filesCopied == dlls.length) return;
+ }
+
+ File containerPatternFile = containerManager.getContainerPatternFile(container.getWineVersion());
+ TarZstdUtils.extract(containerPatternFile, container.getRootDir(), (destination, entryName) -> {
+ if (entryName.contains("system32") || entryName.contains("syswow64")) {
+ for (String dll : dlls) {
+ if (entryName.endsWith("system32/"+dll) || entryName.endsWith("syswow64/"+dll)) {
+ return new File(destination, entryName);
+ }
+ }
+ }
+ return null;
+ });
+
+ cloneOriginalDllFiles(dlls);
+ }
+
+ private void cloneOriginalDllFiles(final String... dlls) {
+ File rootDir = imageFs.getRootDir();
+ File cacheDir = new File(rootDir, ImageFs.CACHE_PATH+"/original_dlls");
+ if (!cacheDir.isDirectory()) cacheDir.mkdirs();
+ File windowsDir = new File(rootDir, ImageFs.WINEPREFIX+"/drive_c/windows");
+ String[] dirnames = {"system32", "syswow64"};
+
+ for (String dll : dlls) {
+ for (String dirname : dirnames) {
+ File dllFile = new File(windowsDir, dirname+"/"+dll);
+ if (dllFile.isFile()) FileUtils.copy(dllFile, new File(cacheDir, dirname+"/"+dll));
+ }
+ }
+ }
+
+ private boolean isGenerateWineprefix() {
+ return getIntent().getBooleanExtra("generate_wineprefix", false);
+ }
+
+ private String getWineStartCommand() {
+ File tempDir = new File(container.getRootDir(), ".wine/drive_c/windows/temp");
+ FileUtils.clear(tempDir);
+
+ String args = "";
+ if (shortcut != null) {
+ if (shortcut.getExtra("singleCPU", "0").equals("1")) args += "/affinity "+ProcessHelper.getSingleCPUAffinityMask()+" ";
+ String execArgs = shortcut.getExtra("execArgs");
+ execArgs = !execArgs.isEmpty() ? " "+execArgs : "";
+
+ if (shortcut.path.endsWith(".lnk")) {
+ args += "\""+shortcut.path+"\""+execArgs;
+ }
+ else {
+ String exeDir = FileUtils.getDirname(shortcut.path);
+ String filename = FileUtils.getName(shortcut.path);
+ int dotIndex, spaceIndex;
+ if ((dotIndex = filename.lastIndexOf(".")) != -1 && (spaceIndex = filename.indexOf(" ", dotIndex)) != -1) {
+ execArgs = filename.substring(spaceIndex+1)+execArgs;
+ filename = filename.substring(0, spaceIndex);
+ }
+ args += "/dir "+exeDir.replace(" ", "\\ ")+" \""+filename+"\""+execArgs;
+ }
+ }
+ else args += "\"wfm.exe\"";
+
+ return "winhandler.exe "+args;
+ }
+
+ public XServer getXServer() {
+ return xServer;
+ }
+
+ public WinHandler getWinHandler() {
+ return winHandler;
+ }
+
+ private void changeWineAudioDriver() {
+ if (!audioDriver.equals(container.getExtra("audioDriver"))) {
+ File rootDir = imageFs.getRootDir();
+ File userRegFile = new File(rootDir, ImageFs.WINEPREFIX+"/user.reg");
+ try (WineRegistryEditor registryEditor = new WineRegistryEditor(userRegFile)) {
+ if (audioDriver.equals("alsa")) {
+ registryEditor.setStringValue("Software\\Wine\\Drivers", "Audio", "alsa");
+ }
+ else if (audioDriver.equals("pulseaudio")) {
+ registryEditor.setStringValue("Software\\Wine\\Drivers", "Audio", "pulse");
+ }
+ }
+ container.putExtra("audioDriver", audioDriver);
+ container.saveData();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/alsaserver/ALSAClient.java b/app/src/main/java/com/winlator/alsaserver/ALSAClient.java
new file mode 100644
index 0000000..39f651a
--- /dev/null
+++ b/app/src/main/java/com/winlator/alsaserver/ALSAClient.java
@@ -0,0 +1,167 @@
+package com.winlator.alsaserver;
+
+import android.media.AudioFormat;
+import android.media.AudioTrack;
+
+import com.winlator.sysvshm.SysVSharedMemory;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class ALSAClient {
+ public enum DataType {
+ U8(1), S16LE(2), S16BE(2), FLOATLE(4), FLOATBE(4);
+ public final byte byteCount;
+
+ DataType(int byteCount) {
+ this.byteCount = (byte)byteCount;
+ }
+ }
+ private DataType dataType = DataType.U8;
+ private AudioTrack audioTrack = null;
+ private byte channels = 2;
+ private int sampleRate = 0;
+ private int position;
+ private int bufferSize;
+ private int frameBytes;
+ private ByteBuffer buffer;
+
+ public void release() {
+ if (buffer != null) {
+ SysVSharedMemory.unmapSHMSegment(buffer, buffer.capacity());
+ buffer = null;
+ }
+
+ if (audioTrack != null) {
+ audioTrack.pause();
+ audioTrack.flush();
+ audioTrack.release();
+ audioTrack = null;
+ }
+ }
+
+ public void prepare() {
+ position = 0;
+ frameBytes = channels * dataType.byteCount;
+ release();
+
+ if (!isValidBufferSize()) return;
+
+ int encoding = AudioFormat.ENCODING_DEFAULT;
+ switch (dataType) {
+ case U8:
+ encoding = AudioFormat.ENCODING_PCM_8BIT;
+ break;
+ case S16LE:
+ case S16BE:
+ encoding = AudioFormat.ENCODING_PCM_16BIT;
+ break;
+ case FLOATLE:
+ case FLOATBE:
+ encoding = AudioFormat.ENCODING_PCM_FLOAT;
+ break;
+ }
+
+ int channelConfig = channels <= 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
+ AudioFormat format = new AudioFormat.Builder()
+ .setEncoding(encoding)
+ .setSampleRate(sampleRate)
+ .setChannelMask(channelConfig)
+ .build();
+
+ audioTrack = new AudioTrack.Builder()
+ .setAudioFormat(format)
+ .setBufferSizeInBytes(getBufferSizeInBytes())
+ .setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
+ .build();
+ audioTrack.play();
+ }
+
+ public void start() {
+ if (audioTrack != null && audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
+ audioTrack.play();
+ }
+ }
+
+ public void stop() {
+ if (audioTrack != null) {
+ audioTrack.stop();
+ audioTrack.flush();
+ }
+ }
+
+ public void pause() {
+ if (audioTrack != null) audioTrack.pause();
+ }
+
+ public void drain() {
+ if (audioTrack != null) audioTrack.flush();
+ }
+
+ public void writeDataToTrack(ByteBuffer data) {
+ if (dataType == DataType.S16LE || dataType == DataType.FLOATLE) {
+ data.order(ByteOrder.LITTLE_ENDIAN);
+ }
+ else if (dataType == DataType.S16BE || dataType == DataType.FLOATBE) {
+ data.order(ByteOrder.BIG_ENDIAN);
+ }
+
+ if (audioTrack != null) {
+ int bytesWritten = audioTrack.write(data, data.limit(), AudioTrack.WRITE_BLOCKING);
+ if (bytesWritten > 0) position += bytesWritten;
+ data.rewind();
+ }
+ }
+
+ public int pointer() {
+ return audioTrack != null ? position / frameBytes : 0;
+ }
+
+ public void setDataType(DataType dataType) {
+ this.dataType = dataType;
+ }
+
+ public void setChannels(int channels) {
+ this.channels = (byte)channels;
+ }
+
+ public void setSampleRate(int sampleRate) {
+ this.sampleRate = sampleRate;
+ }
+
+ public void setBufferSize(int bufferSize) {
+ this.bufferSize = bufferSize;
+ }
+
+ public ByteBuffer getBuffer() {
+ return buffer;
+ }
+
+ public void setBuffer(ByteBuffer buffer) {
+ this.buffer = buffer;
+ }
+
+ public DataType getDataType() {
+ return dataType;
+ }
+
+ public byte getChannels() {
+ return channels;
+ }
+
+ public int getSampleRate() {
+ return sampleRate;
+ }
+
+ public int getBufferSize() {
+ return bufferSize;
+ }
+
+ public int getBufferSizeInBytes() {
+ return bufferSize * frameBytes;
+ }
+
+ private boolean isValidBufferSize() {
+ return (getBufferSizeInBytes() % frameBytes == 0) && bufferSize > 0;
+ }
+}
diff --git a/app/src/main/java/com/winlator/alsaserver/ALSAClientConnectionHandler.java b/app/src/main/java/com/winlator/alsaserver/ALSAClientConnectionHandler.java
new file mode 100644
index 0000000..bb84eb5
--- /dev/null
+++ b/app/src/main/java/com/winlator/alsaserver/ALSAClientConnectionHandler.java
@@ -0,0 +1,17 @@
+package com.winlator.alsaserver;
+
+import com.winlator.xconnector.Client;
+import com.winlator.xconnector.ConnectionHandler;
+
+public class ALSAClientConnectionHandler implements ConnectionHandler {
+ @Override
+ public void handleNewConnection(Client client) {
+ client.createIOStreams();
+ client.setTag(new ALSAClient());
+ }
+
+ @Override
+ public void handleConnectionShutdown(Client client) {
+ ((ALSAClient)client.getTag()).release();
+ }
+}
diff --git a/app/src/main/java/com/winlator/alsaserver/ALSARequestHandler.java b/app/src/main/java/com/winlator/alsaserver/ALSARequestHandler.java
new file mode 100644
index 0000000..2454314
--- /dev/null
+++ b/app/src/main/java/com/winlator/alsaserver/ALSARequestHandler.java
@@ -0,0 +1,91 @@
+package com.winlator.alsaserver;
+
+import com.winlator.sysvshm.SysVSharedMemory;
+import com.winlator.xconnector.Client;
+import com.winlator.xconnector.RequestHandler;
+import com.winlator.xconnector.XConnectorEpoll;
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class ALSARequestHandler implements RequestHandler {
+ private int maxSHMemoryId = 0;
+
+ @Override
+ public boolean handleRequest(Client client) throws IOException {
+ ALSAClient alsaClient = (ALSAClient)client.getTag();
+ XInputStream inputStream = client.getInputStream();
+ XOutputStream outputStream = client.getOutputStream();
+
+ if (inputStream.available() < 5) return false;
+ byte requestCode = inputStream.readByte();
+ int requestLength = inputStream.readInt();
+
+ switch (requestCode) {
+ case RequestCodes.CLOSE:
+ alsaClient.release();
+ break;
+ case RequestCodes.START:
+ alsaClient.start();
+ break;
+ case RequestCodes.STOP:
+ alsaClient.stop();
+ break;
+ case RequestCodes.PAUSE:
+ alsaClient.pause();
+ break;
+ case RequestCodes.PREPARE:
+ if (inputStream.available() < requestLength) return false;
+
+ alsaClient.setChannels(inputStream.readByte());
+ alsaClient.setDataType(ALSAClient.DataType.values()[inputStream.readByte()]);
+ alsaClient.setSampleRate(inputStream.readInt());
+ alsaClient.setBufferSize(inputStream.readInt());
+ alsaClient.prepare();
+
+ createSharedMemory(alsaClient, outputStream);
+ break;
+ case RequestCodes.WRITE:
+ ByteBuffer buffer = alsaClient.getBuffer();
+ if (buffer != null) {
+ buffer.limit(requestLength);
+ alsaClient.writeDataToTrack(buffer);
+ }
+ else {
+ if (inputStream.available() < requestLength) return false;
+ alsaClient.writeDataToTrack(inputStream.readByteBuffer(requestLength));
+ }
+ break;
+ case RequestCodes.DRAIN:
+ alsaClient.drain();
+ break;
+ case RequestCodes.POINTER:
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeInt(alsaClient.pointer());
+ }
+ break;
+ }
+ return true;
+ }
+
+ private void createSharedMemory(ALSAClient alsaClient, XOutputStream outputStream) throws IOException {
+ int size = alsaClient.getBufferSizeInBytes();
+ int fd = SysVSharedMemory.createMemoryFd("alsa-shm"+(++maxSHMemoryId), size);
+
+ if (fd >= 0) {
+ ByteBuffer buffer = SysVSharedMemory.mapSHMSegment(fd, size, 0, true);
+ if (buffer != null) alsaClient.setBuffer(buffer);
+ }
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte((byte)0);
+ outputStream.setAncillaryFd(fd);
+ }
+ finally {
+ if (fd >= 0) XConnectorEpoll.closeFd(fd);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/alsaserver/RequestCodes.java b/app/src/main/java/com/winlator/alsaserver/RequestCodes.java
new file mode 100644
index 0000000..c1b686d
--- /dev/null
+++ b/app/src/main/java/com/winlator/alsaserver/RequestCodes.java
@@ -0,0 +1,12 @@
+package com.winlator.alsaserver;
+
+public abstract class RequestCodes {
+ public static final byte CLOSE = 0;
+ public static final byte START = 1;
+ public static final byte STOP = 2;
+ public static final byte PAUSE = 3;
+ public static final byte PREPARE = 4;
+ public static final byte WRITE = 5;
+ public static final byte DRAIN = 6;
+ public static final byte POINTER = 7;
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/box86_64/Box86_64EditPresetDialog.java b/app/src/main/java/com/winlator/box86_64/Box86_64EditPresetDialog.java
new file mode 100644
index 0000000..b19ba8d
--- /dev/null
+++ b/app/src/main/java/com/winlator/box86_64/Box86_64EditPresetDialog.java
@@ -0,0 +1,126 @@
+package com.winlator.box86_64;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+import androidx.annotation.NonNull;
+
+import com.winlator.R;
+import com.winlator.contentdialog.ContentDialog;
+import com.winlator.core.AppUtils;
+import com.winlator.core.ArrayUtils;
+import com.winlator.core.EnvVars;
+import com.winlator.core.FileUtils;
+import com.winlator.core.StringUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Locale;
+
+public class Box86_64EditPresetDialog extends ContentDialog {
+ private final Context context;
+ private final String prefix;
+ private final Box86_64Preset preset;
+ private final boolean readonly;
+ private Runnable onConfirmCallback;
+
+ public Box86_64EditPresetDialog(@NonNull Context context, String prefix, String presetId) {
+ super(context, R.layout.box86_64_edit_preset_dialog);
+ this.context = context;
+ this.prefix = prefix;
+ preset = presetId != null ? Box86_64PresetManager.getPreset(prefix, context, presetId) : null;
+ readonly = preset != null && !preset.isCustom();
+ setTitle(StringUtils.getString(context, prefix+"_preset"));
+ setIcon(R.drawable.icon_env_var);
+
+ final EditText etName = findViewById(R.id.ETName);
+ etName.getLayoutParams().width = AppUtils.getPreferredDialogWidth(context);
+ etName.setEnabled(!readonly);
+ if (preset != null) {
+ etName.setText(preset.name);
+ }
+ else etName.setText(context.getString(R.string.preset)+"-"+Box86_64PresetManager.getNextPresetId(context, prefix));
+ loadEnvVarsList();
+
+ super.setOnConfirmCallback(() -> {
+ String name = etName.getText().toString().trim();
+ if (name.isEmpty()) return;
+ name = name.replaceAll("[,\\|]+", "");
+ Box86_64PresetManager.editPreset(prefix, context, preset != null ? preset.id : null, name, getEnvVars());
+ if (onConfirmCallback != null) onConfirmCallback.run();
+ });
+ }
+
+ @Override
+ public void setOnConfirmCallback(Runnable onConfirmCallback) {
+ this.onConfirmCallback = onConfirmCallback;
+ }
+
+ private EnvVars getEnvVars() {
+ EnvVars envVars = new EnvVars();
+ LinearLayout parent = findViewById(R.id.LLContent);
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ View child = parent.getChildAt(i);
+ String name = ((TextView)child.findViewById(R.id.TextView)).getText().toString();
+
+ Spinner spinner = child.findViewById(R.id.Spinner);
+ ToggleButton toggleButton = child.findViewById(R.id.ToggleButton);
+ boolean toggleSwitch = toggleButton.getVisibility() == View.VISIBLE;
+ String value = toggleSwitch ? (toggleButton.isChecked() ? "1" : "0") : spinner.getSelectedItem().toString();
+ envVars.put(name, value);
+ }
+ return envVars;
+ }
+
+ private void loadEnvVarsList() {
+ try {
+ LinearLayout parent = findViewById(R.id.LLContent);
+ LayoutInflater inflater = LayoutInflater.from(context);
+ JSONArray data = new JSONArray(FileUtils.readString(context, prefix+"_env_vars.json"));
+ EnvVars envVars = preset != null ? Box86_64PresetManager.getEnvVars(prefix, context, preset.id) : null;
+
+ for (int i = 0; i < data.length(); i++) {
+ JSONObject item = data.getJSONObject(i);
+ final String name = item.getString("name");
+ View child = inflater.inflate(R.layout.box86_64_env_var_list_item, parent, false);
+ ((TextView)child.findViewById(R.id.TextView)).setText(name);
+
+ child.findViewById(R.id.BTHelp).setOnClickListener((v) -> {
+ String suffix = name.replace(prefix.toUpperCase(Locale.ENGLISH)+"_", "").toLowerCase(Locale.ENGLISH);
+ String value = StringUtils.getString(context, "box86_64_env_var_help__"+suffix);
+ AppUtils.showHelpBox(context, v, value);
+ });
+
+ Spinner spinner = child.findViewById(R.id.Spinner);
+ ToggleButton toggleButton = child.findViewById(R.id.ToggleButton);
+ String[] values = ArrayUtils.toStringArray(item.getJSONArray("values"));
+ String value = envVars != null && envVars.has(name) ? envVars.get(name) : item.getString("defaultValue");
+
+ if (item.optBoolean("toggleSwitch", false)) {
+ toggleButton.setVisibility(View.VISIBLE);
+ toggleButton.setEnabled(!readonly);
+ toggleButton.setChecked(value.equals("1"));
+ if (readonly) toggleButton.setAlpha(0.5f);
+ }
+ else {
+ spinner.setVisibility(View.VISIBLE);
+ spinner.setEnabled(!readonly);
+ spinner.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, values));
+ AppUtils.setSpinnerSelectionFromValue(spinner, value);
+ }
+
+ parent.addView(child);
+ }
+ }
+ catch (JSONException e) {}
+ }
+}
diff --git a/app/src/main/java/com/winlator/box86_64/Box86_64Preset.java b/app/src/main/java/com/winlator/box86_64/Box86_64Preset.java
new file mode 100644
index 0000000..4471c03
--- /dev/null
+++ b/app/src/main/java/com/winlator/box86_64/Box86_64Preset.java
@@ -0,0 +1,27 @@
+package com.winlator.box86_64;
+
+import androidx.annotation.NonNull;
+
+public class Box86_64Preset {
+ public static final String COMPATIBILITY = "COMPATIBILITY";
+ public static final String INTERMEDIATE = "INTERMEDIATE";
+ public static final String PERFORMANCE = "PERFORMANCE";
+ public static final String CUSTOM = "CUSTOM";
+ public final String id;
+ public final String name;
+
+ public Box86_64Preset(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public boolean isCustom() {
+ return id.startsWith(CUSTOM);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return name;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/box86_64/Box86_64PresetManager.java b/app/src/main/java/com/winlator/box86_64/Box86_64PresetManager.java
new file mode 100644
index 0000000..4dd56d4
--- /dev/null
+++ b/app/src/main/java/com/winlator/box86_64/Box86_64PresetManager.java
@@ -0,0 +1,201 @@
+package com.winlator.box86_64;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.SpinnerAdapter;
+
+import androidx.preference.PreferenceManager;
+
+import com.winlator.R;
+import com.winlator.SettingsFragment;
+import com.winlator.core.EnvVars;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Locale;
+
+public abstract class Box86_64PresetManager {
+ public static EnvVars getEnvVars(String prefix, Context context, String id) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String ucPrefix = prefix.toUpperCase(Locale.ENGLISH);
+ EnvVars envVars = new EnvVars();
+
+ if (id.equals(Box86_64Preset.COMPATIBILITY)) {
+ envVars.put(ucPrefix+"_DYNAREC_SAFEFLAGS", "2");
+ envVars.put(ucPrefix+"_DYNAREC_FASTNAN", "0");
+ envVars.put(ucPrefix+"_DYNAREC_FASTROUND", "0");
+ envVars.put(ucPrefix+"_DYNAREC_X87DOUBLE", "1");
+ envVars.put(ucPrefix+"_DYNAREC_BIGBLOCK", "0");
+ envVars.put(ucPrefix+"_DYNAREC_STRONGMEM", "1");
+ envVars.put(ucPrefix+"_DYNAREC_WAIT", "0");
+ }
+ else if (id.equals(Box86_64Preset.INTERMEDIATE)) {
+ envVars.put(ucPrefix+"_DYNAREC_SAFEFLAGS", "2");
+ envVars.put(ucPrefix+"_DYNAREC_FASTNAN", "1");
+ envVars.put(ucPrefix+"_DYNAREC_FASTROUND", "1");
+ envVars.put(ucPrefix+"_DYNAREC_X87DOUBLE", "1");
+ envVars.put(ucPrefix+"_DYNAREC_BIGBLOCK", "2");
+ envVars.put(ucPrefix+"_DYNAREC_STRONGMEM", "0");
+ envVars.put(ucPrefix+"_DYNAREC_FORWARD", "512");
+ envVars.put(ucPrefix+"_DYNAREC_WAIT", "1");
+ }
+ else if (id.equals(Box86_64Preset.PERFORMANCE)) {
+ envVars.put(ucPrefix+"_DYNAREC_SAFEFLAGS", "0");
+ envVars.put(ucPrefix+"_DYNAREC_FASTNAN", "1");
+ envVars.put(ucPrefix+"_DYNAREC_FASTROUND", "1");
+ envVars.put(ucPrefix+"_DYNAREC_X87DOUBLE", "0");
+ envVars.put(ucPrefix+"_DYNAREC_BIGBLOCK", "2");
+ envVars.put(ucPrefix+"_DYNAREC_STRONGMEM", "0");
+ envVars.put(ucPrefix+"_DYNAREC_FORWARD", "512");
+ envVars.put(ucPrefix+"_DYNAREC_WAIT", "1");
+ }
+ else if (id.startsWith(Box86_64Preset.CUSTOM)) {
+ for (String[] preset : customPresetsIterator(prefix, context)) {
+ if (preset[0].equals(id)) {
+ envVars.putAll(preset[2]);
+ break;
+ }
+ }
+ }
+
+ String box64Version = preferences.getString("box64_version", SettingsFragment.DEFAULT_BOX64_VERSION);
+ if (box64Version.equals("0.2.2")) {
+ envVars.remove("BOX64_DYNAREC_BIGBLOCK");
+ envVars.remove("BOX64_DYNAREC_STRONGMEM");
+ }
+
+ return envVars;
+ }
+
+ public static ArrayList getPresets(String prefix, Context context) {
+ ArrayList presets = new ArrayList<>();
+ presets.add(new Box86_64Preset(Box86_64Preset.COMPATIBILITY, context.getString(R.string.compatibility)));
+ presets.add(new Box86_64Preset(Box86_64Preset.INTERMEDIATE, context.getString(R.string.intermediate)));
+ presets.add(new Box86_64Preset(Box86_64Preset.PERFORMANCE, context.getString(R.string.performance_unstable)));
+ for (String[] preset : customPresetsIterator(prefix, context)) presets.add(new Box86_64Preset(preset[0], preset[1]));
+ return presets;
+ }
+
+ public static Box86_64Preset getPreset(String prefix, Context context, String id) {
+ for (Box86_64Preset preset : getPresets(prefix, context)) if (preset.id.equals(id)) return preset;
+ return null;
+ }
+
+ private static Iterable customPresetsIterator(String prefix, Context context) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ final String customPresetsStr = preferences.getString(prefix+"_custom_presets", "");
+ final String[] customPresets = customPresetsStr.split(",");
+ final int[] index = {0};
+ return () -> new Iterator() {
+ @Override
+ public boolean hasNext() {
+ return index[0] < customPresets.length && !customPresetsStr.isEmpty();
+ }
+
+ @Override
+ public String[] next() {
+ return customPresets[index[0]++].split("\\|");
+ }
+ };
+ }
+
+ public static int getNextPresetId(Context context, String prefix) {
+ int maxId = 0;
+ for (String[] preset : customPresetsIterator(prefix, context)) {
+ maxId = Math.max(maxId, Integer.parseInt(preset[0].replace(Box86_64Preset.CUSTOM+"-", "")));
+ }
+ return maxId+1;
+ }
+
+ public static void editPreset(String prefix, Context context, String id, String name, EnvVars envVars) {
+ String key = prefix+"_custom_presets";
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String customPresetsStr = preferences.getString(key, "");
+
+ if (id != null) {
+ String[] customPresets = customPresetsStr.split(",");
+ for (int i = 0; i < customPresets.length; i++) {
+ String[] preset = customPresets[i].split("\\|");
+ if (preset[0].equals(id)) {
+ customPresets[i] = id+"|"+name+"|"+envVars.toString();
+ break;
+ }
+ }
+ customPresetsStr = String.join(",", customPresets);
+ }
+ else {
+ String preset = Box86_64Preset.CUSTOM+"-"+getNextPresetId(context, prefix)+"|"+name+"|"+envVars.toString();
+ customPresetsStr += (!customPresetsStr.isEmpty() ? "," : "")+preset;
+ }
+ preferences.edit().putString(key, customPresetsStr).apply();
+ }
+
+ public static void duplicatePreset(String prefix, Context context, String id) {
+ ArrayList presets = getPresets(prefix, context);
+ Box86_64Preset originPreset = null;
+ for (Box86_64Preset preset : presets) {
+ if (preset.id.equals(id)) {
+ originPreset = preset;
+ break;
+ }
+ }
+ if (originPreset == null) return;
+
+ String newName;
+ for (int i = 1;;i++) {
+ newName = originPreset.name+" ("+i+")";
+ boolean found = false;
+ for (Box86_64Preset preset : presets) {
+ if (preset.name.equals(newName)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) break;
+ }
+
+ editPreset(prefix, context, null, newName, getEnvVars(prefix, context, originPreset.id));
+ }
+
+ public static void removePreset(String prefix, Context context, String id) {
+ String key = prefix+"_custom_presets";
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String oldCustomPresetsStr = preferences.getString(key, "");
+ String newCustomPresetsStr = "";
+
+ String[] customPresets = oldCustomPresetsStr.split(",");
+ for (int i = 0; i < customPresets.length; i++) {
+ String[] preset = customPresets[i].split("\\|");
+ if (!preset[0].equals(id)) newCustomPresetsStr += (!newCustomPresetsStr.isEmpty() ? "," : "")+customPresets[i];
+ }
+
+ preferences.edit().putString(key, newCustomPresetsStr).apply();
+ }
+
+ public static void loadSpinner(String prefix, Spinner spinner, String selectedId) {
+ Context context = spinner.getContext();
+ ArrayList presets = getPresets(prefix, context);
+
+ int selectedPosition = 0;
+ for (int i = 0; i < presets.size(); i++) {
+ if (presets.get(i).id.equals(selectedId)) {
+ selectedPosition = i;
+ break;
+ }
+ }
+
+ spinner.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, presets));
+ spinner.setSelection(selectedPosition);
+ }
+
+ public static String getSpinnerSelectedId(Spinner spinner) {
+ SpinnerAdapter adapter = spinner.getAdapter();
+ int selectedPosition = spinner.getSelectedItemPosition();
+ if (adapter != null && adapter.getCount() > 0 && selectedPosition >= 0) {
+ return ((Box86_64Preset)adapter.getItem(selectedPosition)).id;
+ }
+ else return Box86_64Preset.COMPATIBILITY;
+ }
+}
diff --git a/app/src/main/java/com/winlator/container/Container.java b/app/src/main/java/com/winlator/container/Container.java
new file mode 100644
index 0000000..c21c50e
--- /dev/null
+++ b/app/src/main/java/com/winlator/container/Container.java
@@ -0,0 +1,290 @@
+package com.winlator.container;
+
+import android.os.Environment;
+
+import com.winlator.box86_64.Box86_64Preset;
+import com.winlator.core.FileUtils;
+import com.winlator.core.WineInfo;
+import com.winlator.xenvironment.ImageFs;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.Iterator;
+
+public class Container {
+ public static final String DEFAULT_ENV_VARS = "ZINK_DESCRIPTORS=lazy ZINK_CONTEXT_THREADED=false ZINK_DEBUG=compact MESA_SHADER_CACHE_DISABLE=false MESA_SHADER_CACHE_MAX_SIZE=512MB mesa_glthread=true";
+ public static final String DEFAULT_SCREEN_SIZE = "800x600";
+ public static final String DEFAULT_GRAPHICS_DRIVER = "turnip-zink";
+ public static final String DEFAULT_AUDIO_DRIVER = "alsa";
+ public static final String DEFAULT_DXWRAPPER = "original-wined3d";
+ public static final String DEFAULT_DXCOMPONENTS = "direct3d=0,directsound=0,directmusic=0,directshow=0,directplay=0";
+ public static final String DEFAULT_DRIVES = "D:"+Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ public static final byte MAX_DRIVE_LETTERS = 8;
+ public final int id;
+ private String name;
+ private String screenSize = DEFAULT_SCREEN_SIZE;
+ private String envVars = DEFAULT_ENV_VARS;
+ private String graphicsDriver = DEFAULT_GRAPHICS_DRIVER;
+ private String dxwrapper = DEFAULT_DXWRAPPER;
+ private String dxcomponents = DEFAULT_DXCOMPONENTS;
+ private String audioDriver = DEFAULT_AUDIO_DRIVER;
+ private String drives = DEFAULT_DRIVES;
+ private String wineVersion = WineInfo.MAIN_WINE_VERSION.identifier();
+ private boolean showFPS;
+ private boolean stopServicesOnStartup;
+ private String cpuList;
+ private String box86Preset = Box86_64Preset.COMPATIBILITY;
+ private String box64Preset = Box86_64Preset.COMPATIBILITY;
+ private File rootDir;
+ private JSONObject extraData;
+
+ public Container(int id) {
+ this.id = id;
+ this.name = "Container-"+id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getScreenSize() {
+ return screenSize;
+ }
+
+ public void setScreenSize(String screenSize) {
+ this.screenSize = screenSize;
+ }
+
+ public String getEnvVars() {
+ return envVars;
+ }
+
+ public void setEnvVars(String envVars) {
+ this.envVars = envVars != null ? envVars : "";
+ }
+
+ public String getGraphicsDriver() {
+ return graphicsDriver;
+ }
+
+ public void setGraphicsDriver(String graphicsDriver) {
+ this.graphicsDriver = graphicsDriver;
+ }
+
+ public String getDXWrapper() {
+ return dxwrapper;
+ }
+
+ public void setDXWrapper(String dxwrapper) {
+ this.dxwrapper = dxwrapper;
+ }
+
+ public String getAudioDriver() {
+ return audioDriver;
+ }
+
+ public void setAudioDriver(String audioDriver) {
+ this.audioDriver = audioDriver;
+ }
+
+ public String getDXComponents() {
+ return dxcomponents;
+ }
+
+ public void setDXComponents(String dxcomponents) {
+ this.dxcomponents = dxcomponents;
+ }
+
+ public String getDrives() {
+ return drives;
+ }
+
+ public void setDrives(String drives) {
+ this.drives = drives;
+ }
+
+ public boolean isShowFPS() {
+ return showFPS;
+ }
+
+ public void setShowFPS(boolean showFPS) {
+ this.showFPS = showFPS;
+ }
+
+ public boolean isStopServicesOnStartup() {
+ return stopServicesOnStartup;
+ }
+
+ public void setStopServicesOnStartup(boolean stopServicesOnStartup) {
+ this.stopServicesOnStartup = stopServicesOnStartup;
+ }
+
+ public String getCPUList() {
+ return cpuList;
+ }
+
+ public void setCPUList(String cpuList) {
+ this.cpuList = cpuList != null && !cpuList.isEmpty() ? cpuList : null;
+ }
+
+ public String getBox86Preset() {
+ return box86Preset;
+ }
+
+ public void setBox86Preset(String box86Preset) {
+ this.box86Preset = box86Preset;
+ }
+
+ public String getBox64Preset() {
+ return box64Preset;
+ }
+
+ public void setBox64Preset(String box64Preset) {
+ this.box64Preset = box64Preset;
+ }
+
+ public File getRootDir() {
+ return rootDir;
+ }
+
+ public void setRootDir(File rootDir) {
+ this.rootDir = rootDir;
+ }
+
+ public void setExtraData(JSONObject extraData) {
+ this.extraData = extraData;
+ }
+
+ public String getExtra(String name) {
+ return getExtra(name, "");
+ }
+
+ public String getExtra(String name, String fallback) {
+ try {
+ return extraData != null && extraData.has(name) ? extraData.getString(name) : fallback;
+ }
+ catch (JSONException e) {
+ return fallback;
+ }
+ }
+
+ public void putExtra(String name, Object value) {
+ if (extraData == null) extraData = new JSONObject();
+ try {
+ if (value != null) {
+ extraData.put(name, value);
+ }
+ else extraData.remove(name);
+ }
+ catch (JSONException e) {}
+ }
+
+ public String getWineVersion() {
+ return wineVersion;
+ }
+
+ public void setWineVersion(String wineVersion) {
+ this.wineVersion = wineVersion;
+ }
+
+ public File getConfigFile() {
+ return new File(rootDir, ".container");
+ }
+
+ public File getDesktopDir() {
+ return new File(rootDir, ".wine/drive_c/users/"+ImageFs.USER+"/Desktop/");
+ }
+
+ public File getStartMenuDir() {
+ return new File(rootDir, ".wine/drive_c/ProgramData/Microsoft/Windows/Start Menu/");
+ }
+
+ public File getIconsDir(int size) {
+ return new File(rootDir, ".local/share/icons/hicolor/"+size+"x"+size+"/apps/");
+ }
+
+ public Iterable drivesIterator() {
+ return drivesIterator(drives);
+ }
+
+ public static Iterable drivesIterator(final String drives) {
+ final int[] index = {drives.indexOf(":")};
+ final String[] item = new String[2];
+ return () -> new Iterator() {
+ @Override
+ public boolean hasNext() {
+ return index[0] != -1;
+ }
+
+ @Override
+ public String[] next() {
+ item[0] = String.valueOf(drives.charAt(index[0]-1));
+ int nextIndex = drives.indexOf(":", index[0]+1);
+ item[1] = drives.substring(index[0]+1, nextIndex != -1 ? nextIndex-1 : drives.length());
+ index[0] = nextIndex;
+ return item;
+ }
+ };
+ }
+
+ public Iterable dxcomponentsIterator() {
+ return dxcomponentsIterator(dxcomponents);
+ }
+
+ public static Iterable dxcomponentsIterator(final String dxcomponents) {
+ final int[] start = {0};
+ final int[] end = {dxcomponents.indexOf(",")};
+ final String[] item = new String[2];
+ return () -> new Iterator() {
+ @Override
+ public boolean hasNext() {
+ return start[0] < end[0];
+ }
+
+ @Override
+ public String[] next() {
+ int index = dxcomponents.indexOf("=", start[0]);
+ item[0] = dxcomponents.substring(start[0], index);
+ item[1] = dxcomponents.substring(index+1, end[0]);
+ start[0] = end[0]+1;
+ end[0] = dxcomponents.indexOf(",", start[0]);
+ if (end[0] == -1) end[0] = dxcomponents.length();
+ return item;
+ }
+ };
+ }
+
+ public void saveData() {
+ try {
+ JSONObject data = new JSONObject();
+ data.put("id", id);
+ data.put("name", name);
+ data.put("screenSize", screenSize);
+ data.put("envVars", envVars);
+ data.put("cpuList", cpuList);
+ data.put("graphicsDriver", graphicsDriver);
+ data.put("dxwrapper", dxwrapper);
+ data.put("audioDriver", audioDriver);
+ data.put("dxcomponents", dxcomponents);
+ data.put("drives", drives);
+ data.put("showFPS", showFPS);
+ data.put("stopServicesOnStartup", stopServicesOnStartup);
+ data.put("box86Preset", box86Preset);
+ data.put("box64Preset", box64Preset);
+ data.put("extraData", extraData);
+
+ if (!wineVersion.equals(WineInfo.MAIN_WINE_VERSION.identifier())) {
+ data.put("wineVersion", wineVersion);
+ }
+
+ FileUtils.writeString(getConfigFile(), data.toString());
+ }
+ catch (JSONException e) {}
+ }
+}
diff --git a/app/src/main/java/com/winlator/container/ContainerManager.java b/app/src/main/java/com/winlator/container/ContainerManager.java
new file mode 100644
index 0000000..44867c6
--- /dev/null
+++ b/app/src/main/java/com/winlator/container/ContainerManager.java
@@ -0,0 +1,224 @@
+package com.winlator.container;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.winlator.R;
+import com.winlator.core.Callback;
+import com.winlator.core.FileUtils;
+import com.winlator.core.TarZstdUtils;
+import com.winlator.core.WineInfo;
+import com.winlator.xenvironment.ImageFs;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.concurrent.Executors;
+
+public class ContainerManager {
+ private final ArrayList containers = new ArrayList<>();
+ private int maxContainerId = 0;
+ private final File homeDir;
+ private final Context context;
+
+ public ContainerManager(Context context) {
+ this.context = context;
+ File rootDir = ImageFs.find(context).getRootDir();
+ homeDir = new File(rootDir, "home");
+ loadContainers();
+ }
+
+ public ArrayList getContainers() {
+ return containers;
+ }
+
+ private void loadContainers() {
+ containers.clear();
+ maxContainerId = 0;
+
+ try {
+ File[] files = homeDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ if (file.getName().startsWith(ImageFs.USER+"-")) {
+ Container container = new Container(Integer.parseInt(file.getName().replace(ImageFs.USER+"-", "")));
+ container.setRootDir(new File(homeDir, ImageFs.USER+"-"+container.id));
+
+ JSONObject data = new JSONObject(FileUtils.readString(container.getConfigFile()));
+ container.setName(data.getString("name"));
+ container.setScreenSize(data.getString("screenSize"));
+ container.setEnvVars(data.getString("envVars"));
+ container.setCPUList(data.getString("cpuList"));
+ container.setGraphicsDriver(data.getString("graphicsDriver"));
+ container.setDXWrapper(data.getString("dxwrapper"));
+ container.setDXComponents(data.getString("dxcomponents"));
+ container.setDrives(data.getString("drives"));
+ container.setShowFPS(data.getBoolean("showFPS"));
+ container.setStopServicesOnStartup(data.optBoolean("stopServicesOnStartup"));
+
+ if (data.has("extraData")) container.setExtraData(data.getJSONObject("extraData"));
+ if (data.has("wineVersion")) container.setWineVersion(data.getString("wineVersion"));
+ if (data.has("box86Preset")) container.setBox86Preset(data.getString("box86Preset"));
+ if (data.has("box64Preset")) container.setBox64Preset(data.getString("box64Preset"));
+ if (data.has("audioDriver")) container.setAudioDriver(data.getString("audioDriver"));
+
+ containers.add(container);
+ maxContainerId = Math.max(maxContainerId, container.id);
+ }
+ }
+ }
+ }
+ }
+ catch (JSONException e) {}
+ }
+
+ public void activateContainer(Container container) {
+ container.setRootDir(new File(homeDir, ImageFs.USER+"-"+container.id));
+ File file = new File(homeDir, ImageFs.USER);
+ file.delete();
+ FileUtils.symlink("./"+ImageFs.USER+"-"+container.id, file.getPath());
+ }
+
+ public void createContainerAsync(final JSONObject data, Callback callback) {
+ final Handler handler = new Handler();
+ Executors.newSingleThreadExecutor().execute(() -> {
+ final Container container = createContainer(data);
+ handler.post(() -> callback.call(container));
+ });
+ }
+
+ public void duplicateContainerAsync(Container container, Runnable callback) {
+ final Handler handler = new Handler();
+ Executors.newSingleThreadExecutor().execute(() -> {
+ duplicateContainer(container);
+ handler.post(callback);
+ });
+ }
+
+ public void removeContainerAsync(Container container, Runnable callback) {
+ final Handler handler = new Handler();
+ Executors.newSingleThreadExecutor().execute(() -> {
+ removeContainer(container);
+ handler.post(callback);
+ });
+ }
+
+ private Container createContainer(JSONObject data) {
+ try {
+ int id = maxContainerId + 1;
+ data.put("id", id);
+
+ File containerDir = new File(homeDir, ImageFs.USER+"-"+id);
+ if (!containerDir.mkdirs()) return null;
+
+ Container container = new Container(id);
+ container.setRootDir(containerDir);
+ container.setName(data.getString("name"));
+ container.setScreenSize(data.getString("screenSize"));
+ container.setEnvVars(data.getString("envVars"));
+ container.setCPUList(data.getString("cpuList"));
+ container.setGraphicsDriver(data.getString("graphicsDriver"));
+ container.setDXWrapper(data.getString("dxwrapper"));
+ container.setAudioDriver(data.getString("audioDriver"));
+ container.setDXComponents(data.getString("dxcomponents"));
+ container.setDrives(data.getString("drives"));
+ container.setShowFPS(data.getBoolean("showFPS"));
+ container.setStopServicesOnStartup(data.getBoolean("stopServicesOnStartup"));
+ container.setBox86Preset(data.getString("box86Preset"));
+ container.setBox64Preset(data.getString("box64Preset"));
+
+ boolean isMainWineVersion = !data.has("wineVersion") || data.getString("wineVersion").equals(WineInfo.MAIN_WINE_VERSION.identifier());
+ if (!isMainWineVersion) container.setWineVersion(data.getString("wineVersion"));
+
+ if (!TarZstdUtils.extract(getContainerPatternFile(container.getWineVersion()), containerDir)) {
+ FileUtils.delete(containerDir);
+ return null;
+ }
+
+ container.saveData();
+ maxContainerId++;
+ containers.add(container);
+ return container;
+ }
+ catch (JSONException e) {}
+ return null;
+ }
+
+ private void duplicateContainer(Container srcContainer) {
+ int id = maxContainerId + 1;
+
+ File dstDir = new File(homeDir, ImageFs.USER+"-"+id);
+ if (!dstDir.mkdirs()) return;
+
+ if (!FileUtils.copy(srcContainer.getRootDir(), dstDir, (file) -> FileUtils.chmod(file, 0771))) {
+ FileUtils.delete(dstDir);
+ return;
+ }
+
+ Container dstContainer = new Container(id);
+ dstContainer.setRootDir(dstDir);
+ dstContainer.setName(srcContainer.getName()+" ("+context.getString(R.string.copy)+")");
+ dstContainer.setScreenSize(srcContainer.getScreenSize());
+ dstContainer.setEnvVars(srcContainer.getEnvVars());
+ dstContainer.setCPUList(srcContainer.getCPUList());
+ dstContainer.setGraphicsDriver(srcContainer.getGraphicsDriver());
+ dstContainer.setDXWrapper(srcContainer.getDXWrapper());
+ dstContainer.setAudioDriver(srcContainer.getAudioDriver());
+ dstContainer.setDXComponents(srcContainer.getDXComponents());
+ dstContainer.setDrives(srcContainer.getDrives());
+ dstContainer.setShowFPS(srcContainer.isShowFPS());
+ dstContainer.setStopServicesOnStartup(srcContainer.isStopServicesOnStartup());
+ dstContainer.setBox86Preset(srcContainer.getBox86Preset());
+ dstContainer.setBox64Preset(srcContainer.getBox64Preset());
+ dstContainer.saveData();
+
+ maxContainerId++;
+ containers.add(dstContainer);
+ }
+
+ private void removeContainer(Container container) {
+ if (FileUtils.delete(container.getRootDir())) containers.remove(container);
+ }
+
+ public ArrayList loadShortcuts() {
+ ArrayList shortcuts = new ArrayList<>();
+ for (Container container : containers) {
+ File desktopDir = container.getDesktopDir();
+ File[] files = desktopDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.getName().endsWith(".desktop")) shortcuts.add(new Shortcut(container, file));
+ }
+ }
+ }
+
+ shortcuts.sort(Comparator.comparing(a -> a.name));
+ return shortcuts;
+ }
+
+ public int getNextContainerId() {
+ return maxContainerId + 1;
+ }
+
+ public Container getContainerById(int id) {
+ for (Container container : containers) if (container.id == id) return container;
+ return null;
+ }
+
+ public File getContainerPatternFile(String wineVersion) {
+ if (wineVersion == null || wineVersion.equals(WineInfo.MAIN_WINE_VERSION.identifier())) {
+ File rootDir = ImageFs.find(context).getRootDir();
+ return new File(rootDir, "/opt/container-pattern.tzst");
+ }
+ else {
+ File installedWineDir = ImageFs.find(context).getInstalledWineDir();
+ WineInfo wineInfo = WineInfo.fromIdentifier(context, wineVersion);
+ String suffix = wineInfo.fullVersion()+"-"+wineInfo.getArch();
+ return new File(installedWineDir, "container-pattern-"+suffix+".tzst");
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/container/Shortcut.java b/app/src/main/java/com/winlator/container/Shortcut.java
new file mode 100644
index 0000000..115667d
--- /dev/null
+++ b/app/src/main/java/com/winlator/container/Shortcut.java
@@ -0,0 +1,122 @@
+package com.winlator.container;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.winlator.core.FileUtils;
+import com.winlator.core.StringUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.Iterator;
+
+public class Shortcut {
+ public final Container container;
+ public final String name;
+ public final String path;
+ public final Bitmap icon;
+ public final File file;
+ public final File iconFile;
+ public final String wmClass;
+ private final JSONObject extraData = new JSONObject();
+
+ public Shortcut(Container container, File file) {
+ this.container = container;
+ this.file = file;
+
+ String execArgs = "";
+ Bitmap icon = null;
+ File iconFile = null;
+ String wmClass = "";
+
+ File[] iconDirs = {container.getIconsDir(64), container.getIconsDir(48), container.getIconsDir(32)};
+ String section = "";
+
+ int index;
+ for (String line : FileUtils.readLines(file)) {
+ index = line.indexOf("[");
+ if (index != -1) {
+ section = line.substring(index+1, line.indexOf("]", index));
+ }
+ else {
+ index = line.indexOf("=");
+ if (index == -1) continue;
+ String key = line.substring(0, index);
+ String value = line.substring(index+1);
+
+ if (section.equals("Desktop Entry")) {
+ if (key.equals("Exec")) execArgs = value;
+ if (key.equals("Icon")) {
+ for (File iconDir : iconDirs) {
+ iconFile = new File(iconDir, value+".png");
+ if (iconFile.isFile()){
+ icon = BitmapFactory.decodeFile(iconFile.getPath());
+ break;
+ }
+ }
+ }
+ if (key.equals("StartupWMClass")) wmClass = value;
+ }
+ else if (section.equals("Extra Data")) {
+ try {
+ extraData.put(key, value);
+ }
+ catch (JSONException e) {}
+ }
+ }
+ }
+
+ this.name = FileUtils.getBasename(file.getPath());
+ this.icon = icon;
+ this.iconFile = iconFile;
+ this.path = StringUtils.unescape(execArgs.substring(execArgs.lastIndexOf("wine ") + 4));
+ this.wmClass = wmClass;
+ }
+
+ public String getExtra(String name) {
+ return getExtra(name, "");
+ }
+
+ public String getExtra(String name, String fallback) {
+ try {
+ return extraData.has(name) ? extraData.getString(name) : fallback;
+ }
+ catch (JSONException e) {
+ return fallback;
+ }
+ }
+
+ public void putExtra(String name, String value) {
+ try {
+ if (value != null) {
+ extraData.put(name, value);
+ }
+ else extraData.remove(name);
+ }
+ catch (JSONException e) {}
+ }
+
+ public void saveData() {
+ String content = "[Desktop Entry]\n";
+ for (String line : FileUtils.readLines(file)) {
+ if (line.contains("[Extra Data]")) break;
+ if (!line.contains("[Desktop Entry]") && !line.isEmpty()) content += line+"\n";
+ }
+
+ if (extraData.length() > 0) {
+ content += "\n[Extra Data]\n";
+ Iterator keys = extraData.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ try {
+ content += key + "=" + extraData.getString(key) + "\n";
+ }
+ catch (JSONException e) {}
+ }
+ }
+
+ FileUtils.writeString(file, content);
+ }
+}
diff --git a/app/src/main/java/com/winlator/contentdialog/AddEnvVarDialog.java b/app/src/main/java/com/winlator/contentdialog/AddEnvVarDialog.java
new file mode 100644
index 0000000..3b69d53
--- /dev/null
+++ b/app/src/main/java/com/winlator/contentdialog/AddEnvVarDialog.java
@@ -0,0 +1,41 @@
+package com.winlator.contentdialog;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.winlator.R;
+
+public class AddEnvVarDialog extends ContentDialog {
+ public AddEnvVarDialog(final Context context, View container) {
+ super(context, R.layout.add_env_var_dialog);
+
+ EditText etName = findViewById(R.id.ETName);
+ EditText etValue = findViewById(R.id.ETValue);
+ final View emptyTextView = container.findViewById(R.id.TVEnvVarsEmptyText);
+
+ setTitle(context.getString(R.string.new_environment_variable));
+ setIcon(R.drawable.icon_env_var);
+
+ setOnConfirmCallback(() -> {
+ String name = etName.getText().toString().trim();
+ String value = etValue.getText().toString().trim();
+
+ if (!name.isEmpty() && !value.isEmpty()) {
+ LinearLayout parent = container.findViewById(R.id.LLEnvVars);
+ View itemView = LayoutInflater.from(context).inflate(R.layout.env_vars_list_item, parent, false);
+ ((TextView)itemView.findViewById(R.id.TextView)).setText(name);
+ ((EditText)itemView.findViewById(R.id.EditText)).setText(value);
+ itemView.findViewById(R.id.BTRemove).setOnClickListener((v) -> {
+ parent.removeView(itemView);
+ emptyTextView.setVisibility(parent.getChildCount() == 0 ? View.VISIBLE : View.GONE);
+ });
+ parent.addView(itemView);
+ emptyTextView.setVisibility(View.GONE);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/contentdialog/ContentDialog.java b/app/src/main/java/com/winlator/contentdialog/ContentDialog.java
new file mode 100644
index 0000000..a63a2f6
--- /dev/null
+++ b/app/src/main/java/com/winlator/contentdialog/ContentDialog.java
@@ -0,0 +1,171 @@
+package com.winlator.contentdialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.util.SparseBooleanArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.winlator.R;
+import com.winlator.core.AppUtils;
+import com.winlator.core.Callback;
+
+import java.util.ArrayList;
+
+public class ContentDialog extends Dialog {
+ private Runnable onConfirmCallback;
+ private Runnable onCancelCallback;
+ private final View contentView;
+
+ public ContentDialog(@NonNull Context context) {
+ this(context, 0);
+ }
+
+ public ContentDialog(@NonNull Context context, int layoutResId) {
+ super(context, R.style.ContentDialog);
+ contentView = LayoutInflater.from(context).inflate(R.layout.content_dialog, null);
+
+ if (layoutResId > 0) {
+ FrameLayout frameLayout = contentView.findViewById(R.id.FrameLayout);
+ frameLayout.setVisibility(View.VISIBLE);
+ View view = LayoutInflater.from(context).inflate(layoutResId, frameLayout, false);
+ frameLayout.addView(view);
+ }
+
+ View confirmButton = contentView.findViewById(R.id.BTConfirm);
+ confirmButton.setOnClickListener((v) -> {
+ if (onConfirmCallback != null) onConfirmCallback.run();
+ dismiss();
+ });
+
+ View cancelButton = contentView.findViewById(R.id.BTCancel);
+ cancelButton.setOnClickListener((v) -> {
+ if (onCancelCallback != null) onCancelCallback.run();
+ dismiss();
+ });
+
+ setContentView(contentView);
+ }
+
+ public View getContentView() {
+ return contentView;
+ }
+
+ public void setOnConfirmCallback(Runnable onConfirmCallback) {
+ this.onConfirmCallback = onConfirmCallback;
+ }
+
+ public void setOnCancelCallback(Runnable onCancelCallback) {
+ this.onCancelCallback = onCancelCallback;
+ }
+
+ @Override
+ public void setTitle(int titleResId) {
+ setTitle(getContext().getString(titleResId));
+ }
+
+ public void setIcon(int iconResId) {
+ ImageView imageView = findViewById(R.id.IVIcon);
+ imageView.setImageResource(iconResId);
+ imageView.setVisibility(View.VISIBLE);
+ }
+
+ public void setTitle(String title) {
+ LinearLayout titleBar = findViewById(R.id.LLTitleBar);
+ TextView tvTitle = findViewById(R.id.TVTitle);
+
+ if (title != null && !title.isEmpty()) {
+ tvTitle.setText(title);
+ titleBar.setVisibility(View.VISIBLE);
+ }
+ else {
+ tvTitle.setText("");
+ titleBar.setVisibility(View.GONE);
+ }
+ }
+
+ public void setBottomBarText(String bottomBarText) {
+ TextView tvBottomBarText = findViewById(R.id.TVBottomBarText);
+
+ if (bottomBarText != null && !bottomBarText.isEmpty()) {
+ tvBottomBarText.setText(bottomBarText);
+ tvBottomBarText.setVisibility(View.VISIBLE);
+ }
+ else {
+ tvBottomBarText.setText("");
+ tvBottomBarText.setVisibility(View.GONE);
+ }
+ }
+
+ public void setMessage(int msgResId) {
+ setMessage(getContext().getString(msgResId));
+ }
+
+ public void setMessage(String message) {
+ TextView tvMessage = findViewById(R.id.TVMessage);
+
+ if (message != null && !message.isEmpty()) {
+ tvMessage.setText(message);
+ tvMessage.setVisibility(View.VISIBLE);
+ }
+ else {
+ tvMessage.setText("");
+ tvMessage.setVisibility(View.GONE);
+ }
+ }
+
+ public static void confirm(Context context, int msgResId, Runnable callback) {
+ ContentDialog dialog = new ContentDialog(context);
+ dialog.setMessage(msgResId);
+ dialog.setOnConfirmCallback(callback);
+ dialog.show();
+ }
+
+ public static void prompt(Context context, int titleResId, String defaultText, Callback callback) {
+ ContentDialog dialog = new ContentDialog(context);
+
+ final EditText editText = dialog.findViewById(R.id.EditText);
+ editText.setHint(R.string.untitled);
+ if (defaultText != null) editText.setText(defaultText);
+ editText.setVisibility(View.VISIBLE);
+
+ dialog.setTitle(titleResId);
+ dialog.setOnConfirmCallback(() -> {
+ String text = editText.getText().toString().trim();
+ if (!text.isEmpty()) callback.call(text);
+ });
+
+ dialog.show();
+ }
+
+ public static void showMultipleChoiceList(Context context, int titleResId, final String[] items, Callback> callback) {
+ ContentDialog dialog = new ContentDialog(context);
+
+ final ListView listView = dialog.findViewById(R.id.ListView);
+ listView.getLayoutParams().width = AppUtils.getPreferredDialogWidth(context);
+ listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ listView.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_multiple_choice, items));
+ listView.setVisibility(View.VISIBLE);
+
+ dialog.setTitle(titleResId);
+ dialog.setOnConfirmCallback(() -> {
+ ArrayList result = new ArrayList<>();
+ SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions();
+ for (int i = 0; i < checkedItemPositions.size(); i++) {
+ if (checkedItemPositions.valueAt(i)) result.add(checkedItemPositions.keyAt(i));
+ }
+ callback.call(result);
+ });
+
+ dialog.show();
+ }
+}
diff --git a/app/src/main/java/com/winlator/contentdialog/ShortcutSettingsDialog.java b/app/src/main/java/com/winlator/contentdialog/ShortcutSettingsDialog.java
new file mode 100644
index 0000000..73715ca
--- /dev/null
+++ b/app/src/main/java/com/winlator/contentdialog/ShortcutSettingsDialog.java
@@ -0,0 +1,254 @@
+package com.winlator.contentdialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.winlator.R;
+import com.winlator.ShortcutsFragment;
+import com.winlator.box86_64.Box86_64PresetManager;
+import com.winlator.container.Container;
+import com.winlator.container.Shortcut;
+import com.winlator.core.AppUtils;
+import com.winlator.core.ArrayUtils;
+import com.winlator.core.EnvVars;
+import com.winlator.core.StringUtils;
+
+import java.io.File;
+
+public class ShortcutSettingsDialog extends ContentDialog {
+ private final ShortcutsFragment fragment;
+ private final Shortcut shortcut;
+
+ public ShortcutSettingsDialog(ShortcutsFragment fragment, Shortcut shortcut) {
+ super(fragment.getContext(), R.layout.shortcut_settings_dialog);
+ this.fragment = fragment;
+ this.shortcut = shortcut;
+ setTitle(shortcut.name);
+ setIcon(R.drawable.icon_settings);
+
+ createContentView();
+ }
+
+ private void createContentView() {
+ final Context context = fragment.getContext();
+ Resources resources = context.getResources();
+ LinearLayout llContent = findViewById(R.id.LLContent);
+ llContent.getLayoutParams().width = AppUtils.getPreferredDialogWidth(context);
+
+ String[] defaultItem = new String[]{"-- "+context.getString(R.string._default)+" --"};
+
+ final EditText etName = findViewById(R.id.ETName);
+ etName.setText(shortcut.name);
+
+ final EditText etExecArgs = findViewById(R.id.ETExecArgs);
+ etExecArgs.setText(shortcut.getExtra("execArgs"));
+
+ String[] screenSizeEntries = ArrayUtils.concat(defaultItem, resources.getStringArray(R.array.screen_size_entries));
+ loadScreenSizeSpinner(shortcut.getExtra("screenSize"), screenSizeEntries);
+
+ String[] graphicsDriverEntries = ArrayUtils.concat(defaultItem, resources.getStringArray(R.array.graphics_driver_entries));
+ final Spinner sGraphicsDriver = findViewById(R.id.SGraphicsDriver);
+ sGraphicsDriver.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, graphicsDriverEntries));
+ AppUtils.setSpinnerSelectionFromIdentifier(sGraphicsDriver, shortcut.getExtra("graphicsDriver"));
+
+ String[] dxwrapperEntries = ArrayUtils.concat(defaultItem, resources.getStringArray(R.array.dxwrapper_entries));
+ final Spinner sDXWrapper = findViewById(R.id.SDXWrapper);
+ sDXWrapper.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, dxwrapperEntries));
+ AppUtils.setSpinnerSelectionFromIdentifier(sDXWrapper, shortcut.getExtra("dxwrapper"));
+
+ findViewById(R.id.BTHelpDXWrapper).setOnClickListener((v) -> AppUtils.showHelpBox(context, v, R.string.dxwrapper_help_content));
+
+ String[] audioDriverEntries = ArrayUtils.concat(defaultItem, resources.getStringArray(R.array.audio_driver_entries));
+ final Spinner sAudioDriver = findViewById(R.id.SAudioDriver);
+ sAudioDriver.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, audioDriverEntries));
+ AppUtils.setSpinnerSelectionFromIdentifier(sAudioDriver, shortcut.getExtra("audioDriver"));
+
+ final CheckBox cbSingleCPU = findViewById(R.id.CBSingleCPU);
+ cbSingleCPU.setChecked(shortcut.getExtra("singleCPU", "0").equals("1"));
+
+ final CheckBox cbForceFullscreen = findViewById(R.id.CBForceFullscreen);
+ cbForceFullscreen.setChecked(shortcut.getExtra("forceFullscreen", "0").equals("1"));
+
+ final Spinner sBox86Preset = findViewById(R.id.SBox86Preset);
+ Box86_64PresetManager.loadSpinner("box86", sBox86Preset, shortcut.getExtra("box86Preset", shortcut.container.getBox86Preset()));
+
+ final Spinner sBox64Preset = findViewById(R.id.SBox64Preset);
+ Box86_64PresetManager.loadSpinner("box64", sBox64Preset, shortcut.getExtra("box64Preset", shortcut.container.getBox64Preset()));
+
+ createDXComponentsTab();
+ createEnvVarsTab();
+
+ findViewById(R.id.BTAddEnvVar).setOnClickListener((v) -> (new AddEnvVarDialog(context, getContentView())).show());
+ AppUtils.setupTabLayout(getContentView(), R.id.TabLayout, R.id.LLTabDXComponents, R.id.LLTabEnvVars, R.id.LLTabAdvanced);
+
+ findViewById(R.id.BTExtraArgsMenu).setOnClickListener((v) -> {
+ PopupMenu popupMenu = new PopupMenu(context, v);
+ popupMenu.inflate(R.menu.extra_args_popup_menu);
+ popupMenu.setOnMenuItemClickListener((menuItem) -> {
+ String value = String.valueOf(menuItem.getTitle());
+ String execArgs = etExecArgs.getText().toString();
+ if (!execArgs.contains(value)) etExecArgs.setText(!execArgs.isEmpty() ? execArgs+" "+value : value);
+ return true;
+ });
+ popupMenu.show();
+ });
+
+ setOnConfirmCallback(() -> {
+ String name = etName.getText().toString().trim();
+ if (!shortcut.name.equals(name) && !name.isEmpty()) {
+ renameShortcut(name);
+ }
+ else {
+ String execArgs = etExecArgs.getText().toString();
+ shortcut.putExtra("execArgs", !execArgs.isEmpty() ? execArgs : null);
+ shortcut.putExtra("screenSize", getScreenSize());
+ shortcut.putExtra("graphicsDriver", sGraphicsDriver.getSelectedItemPosition() > 0 ? StringUtils.parseIdentifier(sGraphicsDriver.getSelectedItem()) : null);
+ shortcut.putExtra("dxwrapper", sDXWrapper.getSelectedItemPosition() > 0 ? StringUtils.parseIdentifier(sDXWrapper.getSelectedItem()) : null);
+ shortcut.putExtra("audioDriver", sAudioDriver.getSelectedItemPosition() > 0 ? StringUtils.parseIdentifier(sAudioDriver.getSelectedItem()) : null);
+ shortcut.putExtra("forceFullscreen", cbForceFullscreen.isChecked() ? "1" : null);
+ shortcut.putExtra("singleCPU", cbSingleCPU.isChecked() ? "1" : null);
+ shortcut.putExtra("dxcomponents", getDXComponents());
+ shortcut.putExtra("envVars", getEnvVars());
+
+ String box86Preset = Box86_64PresetManager.getSpinnerSelectedId(sBox86Preset);
+ String box64Preset = Box86_64PresetManager.getSpinnerSelectedId(sBox64Preset);
+ shortcut.putExtra("box86Preset", !box86Preset.equals(shortcut.container.getBox86Preset()) ? box86Preset : null);
+ shortcut.putExtra("box64Preset", !box64Preset.equals(shortcut.container.getBox64Preset()) ? box64Preset : null);
+
+ shortcut.saveData();
+ }
+ });
+ }
+
+ private String getScreenSize() {
+ Spinner sScreenSize = findViewById(R.id.SScreenSize);
+ if (sScreenSize.getSelectedItemPosition() == 0) return null;
+ String value = sScreenSize.getSelectedItem().toString();
+ if (value.equalsIgnoreCase("custom")) {
+ value = Container.DEFAULT_SCREEN_SIZE;
+ String strWidth = ((EditText)findViewById(R.id.ETScreenWidth)).getText().toString().trim();
+ String strHeight = ((EditText)findViewById(R.id.ETScreenHeight)).getText().toString().trim();
+ if (strWidth.matches("[0-9]+") && strHeight.matches("[0-9]+")) {
+ int width = Integer.parseInt(strWidth);
+ int height = Integer.parseInt(strHeight);
+ if ((width % 2) == 0 && (height % 2) == 0) return width+"x"+height;
+ }
+ }
+ return StringUtils.parseIdentifier(value);
+ }
+
+ private String getEnvVars() {
+ LinearLayout parent = findViewById(R.id.LLEnvVars);
+ EnvVars envVars = new EnvVars();
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ View child = parent.getChildAt(i);
+ String name = ((TextView)child.findViewById(R.id.TextView)).getText().toString();
+ String value = ((EditText)child.findViewById(R.id.EditText)).getText().toString().trim();
+ if (!value.isEmpty()) envVars.put(name, value);
+ }
+ return !envVars.isEmpty() ? envVars.toString() : null;
+ }
+
+ private void loadScreenSizeSpinner(String selectedValue, String[] entries) {
+ final Spinner sScreenSize = findViewById(R.id.SScreenSize);
+ sScreenSize.setAdapter(new ArrayAdapter<>(fragment.getContext(), android.R.layout.simple_spinner_dropdown_item, entries));
+
+ final LinearLayout llCustomScreenSize = findViewById(R.id.LLCustomScreenSize);
+ sScreenSize.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ String value = sScreenSize.getItemAtPosition(position).toString();
+ llCustomScreenSize.setVisibility(value.equalsIgnoreCase("custom") ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+
+ boolean found = AppUtils.setSpinnerSelectionFromIdentifier(sScreenSize, selectedValue);
+ if (!found && !selectedValue.isEmpty()) {
+ AppUtils.setSpinnerSelectionFromValue(sScreenSize, "custom");
+ String[] screenSize = selectedValue.split("x");
+ ((EditText)findViewById(R.id.ETScreenWidth)).setText(screenSize[0]);
+ ((EditText)findViewById(R.id.ETScreenHeight)).setText(screenSize[1]);
+ }
+ }
+
+ private void renameShortcut(String newName) {
+ File parent = shortcut.file.getParentFile();
+ File newFile = new File(parent, newName+".desktop");
+ if (!newFile.isFile()) shortcut.file.renameTo(newFile);
+
+ File linkFile = new File(parent, shortcut.name+".lnk");
+ if (linkFile.isFile()) {
+ newFile = new File(parent, newName+".lnk");
+ if (!newFile.isFile()) linkFile.renameTo(newFile);
+ }
+ fragment.loadShortcutsList();
+ }
+
+ private String getDXComponents() {
+ ViewGroup parent = findViewById(R.id.LLTabDXComponents);
+ int childCount = parent.getChildCount();
+ String[] dxcomponents = new String[childCount];
+
+ for (int i = 0; i < childCount; i++) {
+ View child = parent.getChildAt(i);
+ Spinner spinner = child.findViewById(R.id.Spinner);
+ dxcomponents[i] = child.getTag().toString()+"="+spinner.getSelectedItemPosition();
+ }
+
+ String result = String.join(",", dxcomponents);
+ return !result.equals(shortcut.container.getDXComponents()) ? result : null;
+ }
+
+ private void createDXComponentsTab() {
+ final String[] dxcomponents = shortcut.getExtra("dxcomponents", shortcut.container.getDXComponents()).split(",");
+
+ Context context = fragment.getContext();
+ LayoutInflater inflater = LayoutInflater.from(context);
+ ViewGroup parent = findViewById(R.id.LLTabDXComponents);
+
+ for (String dxcomponent : dxcomponents) {
+ String[] parts = dxcomponent.split("=");
+ View itemView = inflater.inflate(R.layout.dxcomponent_list_item, parent, false);
+ ((TextView)itemView.findViewById(R.id.TextView)).setText(StringUtils.getString(context, parts[0]));
+ ((Spinner)itemView.findViewById(R.id.Spinner)).setSelection(Integer.parseInt(parts[1]), false);
+ itemView.setTag(parts[0]);
+ parent.addView(itemView);
+ }
+ }
+
+ private void createEnvVarsTab() {
+ final LinearLayout parent = findViewById(R.id.LLEnvVars);
+ final View emptyTextView = findViewById(R.id.TVEnvVarsEmptyText);
+ LayoutInflater inflater = LayoutInflater.from(fragment.getContext());
+ final EnvVars envVars = new EnvVars(shortcut.getExtra("envVars"));
+
+ for (String name : envVars) {
+ final View itemView = inflater.inflate(R.layout.env_vars_list_item, parent, false);
+ ((TextView)itemView.findViewById(R.id.TextView)).setText(name);
+ ((EditText)itemView.findViewById(R.id.EditText)).setText(envVars.get(name));
+
+ itemView.findViewById(R.id.BTRemove).setOnClickListener((v) -> {
+ parent.removeView(itemView);
+ if (parent.getChildCount() == 0) emptyTextView.setVisibility(View.VISIBLE);
+ });
+ parent.addView(itemView);
+ }
+
+ if (envVars.isEmpty()) emptyTextView.setVisibility(View.VISIBLE);
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/AppUtils.java b/app/src/main/java/com/winlator/core/AppUtils.java
new file mode 100644
index 0000000..99d9260
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/AppUtils.java
@@ -0,0 +1,289 @@
+package com.winlator.core;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Looper;
+import android.text.Html;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupWindow;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.android.material.tabs.TabLayout;
+import com.winlator.R;
+
+import java.lang.ref.WeakReference;
+
+public abstract class AppUtils {
+ private static WeakReference globalToastReference = null;
+
+ public static void keepScreenOn(Activity activity) {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ public static String getArchName() {
+ for (String arch : Build.SUPPORTED_ABIS) {
+ switch (arch) {
+ case "arm64-v8a": return "arm64";
+ case "armeabi-v7a": return "armhf";
+ case "x86_64": return "x86_64";
+ case "x86": return "x86";
+ }
+ }
+ return "armhf";
+ }
+
+ public static void restartApplication(Context context) {
+ restartApplication(context, 0);
+ }
+
+ public static void restartApplication(Context context, int selectedMenuItemId) {
+ Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
+ Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
+ if (selectedMenuItemId > 0) mainIntent.putExtra("selected_menu_item_id", selectedMenuItemId);
+ context.startActivity(mainIntent);
+ Runtime.getRuntime().exit(0);
+ }
+
+ public static void showKeyboard(AppCompatActivity activity) {
+ final InputMethodManager imm = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+ activity.getWindow().getDecorView().postDelayed(() -> imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0), 500L);
+ }
+ else imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
+ }
+
+ public static void hideSystemUI(final Activity activity) {
+ Window window = activity.getWindow();
+ final View decorView = window.getDecorView();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ window.setDecorFitsSystemWindows(false);
+ final WindowInsetsController insetsController = decorView.getWindowInsetsController();
+ if (insetsController != null) {
+ insetsController.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
+ insetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ }
+ }
+ else {
+ final int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+
+ decorView.setSystemUiVisibility(flags);
+ decorView.setOnSystemUiVisibilityChangeListener((visibility) -> {
+ if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) decorView.setSystemUiVisibility(flags);
+ });
+ }
+ }
+
+ public static boolean isUiThread() {
+ return Looper.getMainLooper().getThread() == Thread.currentThread();
+ }
+
+ public static int getScreenWidth() {
+ return Resources.getSystem().getDisplayMetrics().widthPixels;
+ }
+
+ public static int getScreenHeight() {
+ return Resources.getSystem().getDisplayMetrics().heightPixels;
+ }
+
+ public static int getPreferredDialogWidth(Context context) {
+ int orientation = context.getResources().getConfiguration().orientation;
+ float scale = orientation == Configuration.ORIENTATION_PORTRAIT ? 0.8f : 0.5f;
+ return (int)UnitUtils.dpToPx(UnitUtils.pxToDp(AppUtils.getScreenWidth()) * scale);
+ }
+
+ public static Toast showToast(Context context, int textResId) {
+ return showToast(context, context.getString(textResId));
+ }
+
+ public static Toast showToast(final Context context, final String text) {
+ if (!isUiThread()) {
+ if (context instanceof Activity) {
+ ((Activity)context).runOnUiThread(() -> showToast(context, text));
+ }
+ return null;
+ }
+
+ if (globalToastReference != null) {
+ Toast toast = globalToastReference.get();
+ if (toast != null) toast.cancel();
+ globalToastReference = null;
+ }
+
+ View view = LayoutInflater.from(context).inflate(R.layout.custom_toast, null);
+ ((TextView)view.findViewById(R.id.TextView)).setText(text);
+
+ Toast toast = new Toast(context);
+ toast.setGravity(Gravity.CENTER | Gravity.BOTTOM, 0, 50);
+ toast.setDuration(text.length() >= 40 ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
+ toast.setView(view);
+ toast.show();
+ globalToastReference = new WeakReference<>(toast);
+ return toast;
+ }
+
+ public static PopupWindow showPopupWindow(View anchor, View contentView) {
+ return showPopupWindow(anchor, contentView, 0, 0);
+ }
+
+ public static PopupWindow showPopupWindow(View anchor, View contentView, int width, int height) {
+ Context context = anchor.getContext();
+ PopupWindow popupWindow = new PopupWindow(context);
+ popupWindow.setElevation(5.0f);
+
+ if (width == 0 && height == 0) {
+ int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ contentView.measure(widthMeasureSpec, heightMeasureSpec);
+ popupWindow.setWidth(contentView.getMeasuredWidth());
+ popupWindow.setHeight(contentView.getMeasuredHeight());
+ }
+ else {
+ if (width > 0) {
+ popupWindow.setWidth((int)UnitUtils.dpToPx(width));
+ }
+ else popupWindow.setWidth(LinearLayout.LayoutParams.WRAP_CONTENT);
+
+ if (height > 0) {
+ popupWindow.setHeight((int)UnitUtils.dpToPx(height));
+ }
+ else popupWindow.setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
+ }
+
+ popupWindow.setContentView(contentView);
+ popupWindow.setFocusable(false);
+ popupWindow.setOutsideTouchable(true);
+
+ popupWindow.update();
+ popupWindow.showAsDropDown(anchor);
+
+ popupWindow.setFocusable(true);
+ popupWindow.update();
+ return popupWindow;
+ }
+
+ public static void showHelpBox(Context context, View anchor, int textResId) {
+ showHelpBox(context, anchor, context.getString(textResId));
+ }
+
+ public static void showHelpBox(Context context, View anchor, String text) {
+ int padding = (int)UnitUtils.dpToPx(8);
+ TextView textView = new TextView(context);
+ textView.setLayoutParams(new ViewGroup.LayoutParams((int)UnitUtils.dpToPx(284), ViewGroup.LayoutParams.WRAP_CONTENT));
+ textView.setPadding(padding, padding, padding, padding);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
+ textView.setText(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY));
+ int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ textView.measure(widthMeasureSpec, heightMeasureSpec);
+ showPopupWindow(anchor, textView, 300, textView.getMeasuredHeight());
+ }
+
+ public static int getVersionCode(Context context) {
+ try {
+ PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ return pInfo.versionCode;
+ }
+ catch (PackageManager.NameNotFoundException e) {
+ return 0;
+ }
+ }
+
+ public static void observeSoftKeyboardVisibility(View rootView, Callback callback) {
+ final boolean[] visible = {false};
+ rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
+ Rect rect = new Rect();
+ rootView.getWindowVisibleDisplayFrame(rect);
+ int screenHeight = rootView.getRootView().getHeight();
+ int keypadHeight = screenHeight - rect.bottom;
+
+ if (keypadHeight > screenHeight * 0.15f) {
+ if (!visible[0]) {
+ visible[0] = true;
+ callback.call(true);
+ }
+ }
+ else {
+ if (visible[0]) {
+ visible[0] = false;
+ callback.call(false);
+ }
+ }
+ });
+ }
+
+ public static boolean setSpinnerSelectionFromValue(Spinner spinner, String value) {
+ spinner.setSelection(0, false);
+ for (int i = 0; i < spinner.getCount(); i++) {
+ if (spinner.getItemAtPosition(i).toString().equalsIgnoreCase(value)) {
+ spinner.setSelection(i, false);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean setSpinnerSelectionFromIdentifier(Spinner spinner, String identifier) {
+ spinner.setSelection(0, false);
+ for (int i = 0; i < spinner.getCount(); i++) {
+ if (StringUtils.parseIdentifier(spinner.getItemAtPosition(i)).equals(identifier)) {
+ spinner.setSelection(i, false);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static void setupTabLayout(final View view, int tabLayoutResId, final int... tabResIds) {
+ final Callback tabSelectedCallback = (position) -> {
+ for (int i = 0; i < tabResIds.length; i++) {
+ View tabView = view.findViewById(tabResIds[i]);
+ tabView.setVisibility(position == i ? View.VISIBLE : View.GONE);
+ }
+ };
+
+ TabLayout tabLayout = view.findViewById(tabLayoutResId);
+ tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(TabLayout.Tab tab) {
+ tabSelectedCallback.call(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(TabLayout.Tab tab) {}
+
+ @Override
+ public void onTabReselected(TabLayout.Tab tab) {
+ tabSelectedCallback.call(tab.getPosition());
+ }
+ });
+ tabLayout.getTabAt(0).select();
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/ArrayUtils.java b/app/src/main/java/com/winlator/core/ArrayUtils.java
new file mode 100644
index 0000000..54998b8
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/ArrayUtils.java
@@ -0,0 +1,40 @@
+package com.winlator.core;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.util.Arrays;
+
+public abstract class ArrayUtils {
+ public static byte[] concat(byte[]... elements) {
+ byte[] result = Arrays.copyOf(elements[0], elements[0].length);
+ for (int i = 1; i < elements.length; i++) {
+ byte[] newArray = Arrays.copyOf(result, result.length + elements[i].length);
+ System.arraycopy(elements[i], 0, newArray, result.length, elements[i].length);
+ result = newArray;
+ }
+ return result;
+ }
+
+ @SafeVarargs
+ public static T[] concat(T[]... elements) {
+ T[] result = Arrays.copyOf(elements[0], elements[0].length);
+ for (int i = 1; i < elements.length; i++) {
+ T[] newArray = Arrays.copyOf(result, result.length + elements[i].length);
+ System.arraycopy(elements[i], 0, newArray, result.length, elements[i].length);
+ result = newArray;
+ }
+ return result;
+ }
+
+ public static String[] toStringArray(JSONArray data) {
+ String[] stringArray = new String[data.length()];
+ for (int i = 0; i < data.length(); i++) {
+ try {
+ stringArray[i] = data.getString(i);
+ }
+ catch (JSONException e) {}
+ }
+ return stringArray;
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/Callback.java b/app/src/main/java/com/winlator/core/Callback.java
new file mode 100644
index 0000000..73c8f0d
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/Callback.java
@@ -0,0 +1,5 @@
+package com.winlator.core;
+
+public interface Callback {
+ void call(T object);
+}
diff --git a/app/src/main/java/com/winlator/core/CursorLocker.java b/app/src/main/java/com/winlator/core/CursorLocker.java
new file mode 100644
index 0000000..444398a
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/CursorLocker.java
@@ -0,0 +1,76 @@
+package com.winlator.core;
+
+import com.winlator.math.Mathf;
+import com.winlator.xserver.Window;
+import com.winlator.xserver.XServer;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class CursorLocker extends TimerTask {
+ public enum State {NONE, LOCKED, CONFINED}
+ private final XServer xServer;
+ private State state = State.CONFINED;
+ private float damping = 0.25f;
+ private short maxDistance;
+
+ public CursorLocker(XServer xServer) {
+ this.xServer = xServer;
+ maxDistance = (short)(xServer.screenInfo.width * 0.05f);
+ Timer timer = new Timer();
+ timer.scheduleAtFixedRate(this, 0, 1000 / 60);
+ }
+
+ public short getMaxDistance() {
+ return maxDistance;
+ }
+
+ public void setMaxDistance(short maxDistance) {
+ this.maxDistance = maxDistance;
+ }
+
+ public float getDamping() {
+ return damping;
+ }
+
+ public void setDamping(float damping) {
+ this.damping = damping;
+ }
+
+ public State getState() {
+ return state;
+ }
+
+ public void setState(State state) {
+ this.state = state;
+ }
+
+ @Override
+ public void run() {
+ if (state == State.LOCKED) {
+ Window window = xServer.inputDeviceManager.getPointWindow();
+ short centerX = (short)(window.getRootX() + window.getWidth() / 2);
+ short centerY = (short)(window.getRootY() + window.getHeight() / 2);
+ xServer.pointer.setX(centerX);
+ xServer.pointer.setY(centerY);
+
+ }
+ else if (state == State.CONFINED) {
+ short x = (short)Mathf.clamp(xServer.pointer.getX(), -maxDistance, xServer.screenInfo.width + maxDistance);
+ short y = (short)Mathf.clamp(xServer.pointer.getY(), -maxDistance, xServer.screenInfo.height + maxDistance);
+
+ if (x < 0) {
+ xServer.pointer.setX((short)Math.ceil(x * damping));
+ }
+ else if (x >= xServer.screenInfo.width) {
+ xServer.pointer.setX((short)Math.floor(xServer.screenInfo.width + (x - xServer.screenInfo.width) * damping));
+ }
+ if (y < 0) {
+ xServer.pointer.setY((short)Math.ceil(y * damping));
+ }
+ else if (y >= xServer.screenInfo.height) {
+ xServer.pointer.setY((short)Math.floor(xServer.screenInfo.height + (y - xServer.screenInfo.height) * damping));
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/DownloadProgressDialog.java b/app/src/main/java/com/winlator/core/DownloadProgressDialog.java
new file mode 100644
index 0000000..523d5e2
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/DownloadProgressDialog.java
@@ -0,0 +1,62 @@
+package com.winlator.core;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import com.google.android.material.progressindicator.CircularProgressIndicator;
+import com.winlator.R;
+
+public class DownloadProgressDialog {
+ private final Activity activity;
+ private Dialog dialog;
+
+ public DownloadProgressDialog(Activity activity) {
+ this.activity = activity;
+ }
+
+ private void create() {
+ if (dialog != null) return;
+ dialog = new Dialog(activity, android.R.style.Theme_Translucent_NoTitleBar_Fullscreen);
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ dialog.setCancelable(false);
+ dialog.setCanceledOnTouchOutside(false);
+ dialog.setContentView(R.layout.download_progress_dialog);
+
+ Window window = dialog.getWindow();
+ if (window != null) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
+ window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ }
+ }
+
+ public void show(final Runnable onCancelCallback) {
+ if (isShowing()) return;
+ close();
+ if (dialog == null) create();
+ setProgress(0);
+ dialog.findViewById(R.id.BTCancel).setOnClickListener((v) -> onCancelCallback.run());
+ dialog.show();
+ }
+
+ public void setProgress(int progress) {
+ if (dialog == null) return;
+ ((CircularProgressIndicator)dialog.findViewById(R.id.CircularProgressIndicator)).setProgress(progress);
+ ((TextView)dialog.findViewById(R.id.TVProgress)).setText(progress+"%");
+ }
+
+ public void close() {
+ try {
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+ catch (Exception e) {}
+ }
+
+ public boolean isShowing() {
+ return dialog != null && dialog.isShowing();
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/EnvVars.java b/app/src/main/java/com/winlator/core/EnvVars.java
new file mode 100644
index 0000000..1873f2c
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/EnvVars.java
@@ -0,0 +1,84 @@
+package com.winlator.core;
+
+import androidx.annotation.NonNull;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+
+public class EnvVars implements Iterable {
+ private final LinkedHashMap data = new LinkedHashMap<>();
+
+ public EnvVars() {}
+
+ public EnvVars(String values) {
+ putAll(values);
+ }
+
+ public void put(String name, Object value) {
+ data.put(name, String.valueOf(value));
+ }
+
+ public void putAll(String values) {
+ if (values == null || values.isEmpty()) return;
+ String[] parts = values.split(" ");
+ for (String part : parts) {
+ int index = part.indexOf("=");
+ String name = part.substring(0, index);
+ String value = part.substring(index+1);
+ data.put(name, value);
+ }
+ }
+
+ public void putAll(EnvVars envVars) {
+ data.putAll(envVars.data);
+ }
+
+ public String get(String name) {
+ return data.get(name);
+ }
+
+ public void remove(String name) {
+ data.remove(name);
+ }
+
+ public boolean has(String name) {
+ return data.containsKey(name);
+ }
+
+ public void clear() {
+ data.clear();
+ }
+
+ public boolean isEmpty() {
+ return data.isEmpty();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.join(" ", toStringArray());
+ }
+
+ public String toEscapedString() {
+ String result = "";
+ for (String key : data.keySet()) {
+ if (!result.isEmpty()) result += " ";
+ String value = data.get(key);
+ result += key+"="+value.replace(" ", "\\ ");
+ }
+ return result;
+ }
+
+ public String[] toStringArray() {
+ String[] stringArray = new String[data.size()];
+ int index = 0;
+ for (String key : data.keySet()) stringArray[index++] = key+"="+data.get(key);
+ return stringArray;
+ }
+
+ @NonNull
+ @Override
+ public Iterator iterator() {
+ return data.keySet().iterator();
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/FileUtils.java b/app/src/main/java/com/winlator/core/FileUtils.java
new file mode 100644
index 0000000..5be3b07
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/FileUtils.java
@@ -0,0 +1,320 @@
+package com.winlator.core;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.StatFs;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Stack;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+
+public abstract class FileUtils {
+ public static byte[] read(Context context, String assetFile) {
+ try (InputStream inStream = context.getAssets().open(assetFile)) {
+ return StreamUtils.copyToByteArray(inStream);
+ }
+ catch (IOException e) {
+ return null;
+ }
+ }
+
+ public static byte[] read(File file) {
+ try (InputStream inStream = new BufferedInputStream(new FileInputStream(file))) {
+ return StreamUtils.copyToByteArray(inStream);
+ }
+ catch (IOException e) {
+ return null;
+ }
+ }
+
+ public static String readString(Context context, String assetFile) {
+ return new String(read(context, assetFile), StandardCharsets.UTF_8);
+ }
+
+ public static String readString(File file) {
+ return new String(read(file), StandardCharsets.UTF_8);
+ }
+
+ public static String readString(Context context, Uri uri) {
+ StringBuilder sb = new StringBuilder();
+ try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = reader.readLine()) != null) sb.append(line);
+ return sb.toString();
+ }
+ catch (IOException e) {
+ return null;
+ }
+ }
+
+ public static boolean write(File file, byte[] data) {
+ try (OutputStream os = new FileOutputStream(file)) {
+ os.write(data, 0, data.length);
+ return true;
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ public static boolean writeString(File file, String data) {
+ try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
+ bw.write(data);
+ bw.flush();
+ return true;
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ public static void symlink(File oldFile, File newFile) {
+ symlink(oldFile.getAbsolutePath(), newFile.getAbsolutePath());
+ }
+
+ public static void symlink(String oldPath, String newPath) {
+ try {
+ (new File(newPath)).delete();
+ Os.symlink(oldPath, newPath);
+ }
+ catch (ErrnoException e) {}
+ }
+
+ public static boolean isSymlink(File file) {
+ return Files.isSymbolicLink(file.toPath());
+ }
+
+ public static boolean delete(File targetFile) {
+ if (targetFile == null) return false;
+ if (targetFile.isDirectory()) {
+ if (!isSymlink(targetFile)) if (!clear(targetFile)) return false;
+ }
+ return targetFile.delete();
+ }
+
+ public static boolean clear(File targetFile) {
+ if (targetFile == null) return false;
+ if (targetFile.isDirectory()) {
+ File[] files = targetFile.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (!delete(file)) return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public static boolean isEmpty(File targetFile) {
+ if (targetFile == null) return true;
+ if (targetFile.isDirectory()) {
+ String[] files = targetFile.list();
+ return files == null || files.length == 0;
+ }
+ else return targetFile.length() == 0;
+ }
+
+ public static boolean copy(File srcFile, File dstFile) {
+ return copy(srcFile, dstFile, null);
+ }
+
+ public static boolean copy(File srcFile, File dstFile, Callback callback) {
+ if (isSymlink(srcFile)) return true;
+ if (srcFile.isDirectory()) {
+ if (!dstFile.exists() && !dstFile.mkdirs()) return false;
+ if (callback != null) callback.call(dstFile);
+
+ String[] filenames = srcFile.list();
+ if (filenames != null) {
+ for (String filename : filenames) {
+ if (!copy(new File(srcFile, filename), new File(dstFile, filename), callback)) {
+ return false;
+ }
+ }
+ }
+ }
+ else {
+ File parent = dstFile.getParentFile();
+ if (!srcFile.exists() || (parent != null && !parent.exists() && !parent.mkdirs())) return false;
+
+ try {
+ FileChannel inChannel = (new FileInputStream(srcFile)).getChannel();
+ FileChannel outChannel = (new FileOutputStream(dstFile)).getChannel();
+ inChannel.transferTo(0, inChannel.size(), outChannel);
+ inChannel.close();
+ outChannel.close();
+
+ if (callback != null) callback.call(dstFile);
+ return dstFile.exists();
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static void copy(Context context, String assetFile, File dstFile) {
+ if (isDirectory(context, assetFile)) {
+ if (!dstFile.isDirectory()) dstFile.mkdirs();
+ try {
+ String[] filenames = context.getAssets().list(assetFile);
+ for (String filename : filenames) {
+ String relativePath = StringUtils.addEndSlash(assetFile)+filename;
+ if (isDirectory(context, relativePath)) {
+ copy(context, relativePath, new File(dstFile, filename));
+ }
+ else copy(context, relativePath, dstFile);
+ }
+ }
+ catch (IOException e) {}
+ }
+ else {
+ if (dstFile.isDirectory()) dstFile = new File(dstFile, FileUtils.getName(assetFile));
+ File parent = dstFile.getParentFile();
+ if (!parent.isDirectory()) parent.mkdirs();
+ try (InputStream inStream = context.getAssets().open(assetFile);
+ BufferedOutputStream outStream = new BufferedOutputStream(new FileOutputStream(dstFile), StreamUtils.BUFFER_SIZE)) {
+ StreamUtils.copy(inStream, outStream);
+ }
+ catch (IOException e) {}
+ }
+ }
+
+ public static List readLines(File file) {
+ List lines = new ArrayList<>();
+ try (FileInputStream fis = new FileInputStream(file)) {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
+ String line;
+ while ((line = reader.readLine()) != null) lines.add(line);
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ return lines;
+ }
+
+ public static String getName(String path) {
+ if (path == null) return "";
+ path = StringUtils.removeEndSlash(path);
+ int index = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
+ return path.substring(index + 1);
+ }
+
+ public static String getBasename(String path) {
+ return getName(path).replaceFirst("\\.[^\\.]+$", "");
+ }
+
+ public static String getDirname(String path) {
+ if (path == null) return "";
+ path = StringUtils.removeEndSlash(path);
+ int index = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
+ return path.substring(0, index);
+ }
+
+ public static void chmod(File file, int mode) {
+ try {
+ Os.chmod(file.getAbsolutePath(), mode);
+ }
+ catch (ErrnoException e) {}
+ }
+
+ public static File createTempFile(File parent, String prefix) {
+ File tempFile = null;
+ boolean exists = true;
+ while (exists) {
+ tempFile = new File(parent, prefix+"-"+ UUID.randomUUID().toString().replace("-", "")+".tmp");
+ exists = tempFile.exists();
+ }
+ return tempFile;
+ }
+
+ public static String getFilePathFromUri(Uri uri) {
+ String path = null;
+ if (uri.getAuthority().equals("com.android.externalstorage.documents")) {
+ String[] parts = uri.getLastPathSegment().split(":");
+ if (parts[0].equalsIgnoreCase("primary")) path = Environment.getExternalStorageDirectory() + "/" + parts[1];
+ }
+ return path;
+ }
+
+ public static boolean contentEquals(File origin, File target) {
+ return Arrays.equals(read(origin), read(target));
+ }
+
+ public static void getSizeAsync(File file, Callback callback) {
+ Executors.newSingleThreadExecutor().execute(() -> getSize(file, callback));
+ }
+
+ private static void getSize(File file, Callback callback) {
+ if (file == null) return;
+ if (file.isFile()) {
+ callback.call(file.length());
+ return;
+ }
+
+ Stack stack = new Stack<>();
+ stack.push(file);
+
+ while (!stack.isEmpty()) {
+ File current = stack.pop();
+ File[] files = current.listFiles();
+ if (files == null) continue;
+ for (File f : files) {
+ if (f.isDirectory()) {
+ stack.push(f);
+ }
+ else {
+ long length = f.length();
+ if (length > 0) callback.call(length);
+ }
+ }
+ }
+ }
+
+ public static long getInternalStorageSize() {
+ File dataDir = Environment.getDataDirectory();
+ StatFs stat = new StatFs(dataDir.getPath());
+ long blockSize = stat.getBlockSizeLong();
+ long totalBlocks = stat.getBlockCountLong();
+ return totalBlocks * blockSize;
+ }
+
+ public static boolean isDirectory(Context context, String assetFile) {
+ try {
+ String[] files = context.getAssets().list(assetFile);
+ return files != null && files.length > 0;
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+
+ public static String toRelativePath(String basePath, String fullPath) {
+ return StringUtils.removeEndSlash((fullPath.startsWith("/") ? "/" : "")+(new File(basePath).toURI().relativize(new File(fullPath).toURI()).getPath()));
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/GPUInformation.java b/app/src/main/java/com/winlator/core/GPUInformation.java
new file mode 100644
index 0000000..3830a70
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/GPUInformation.java
@@ -0,0 +1,112 @@
+package com.winlator.core;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.opengl.EGL14;
+import android.preference.PreferenceManager;
+
+import androidx.collection.ArrayMap;
+
+import java.util.Locale;
+import java.util.Objects;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.opengles.GL10;
+
+public abstract class GPUInformation {
+ private static ArrayMap loadGPUInformation(Context context) {
+ final Thread thread = Thread.currentThread();
+ final ArrayMap gpuInfo = new ArrayMap<>();
+ gpuInfo.put("renderer", "");
+ gpuInfo.put("vendor", "");
+ gpuInfo.put("version", "");
+
+ (new Thread(() -> {
+ int[] attribList = new int[] {
+ EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT,
+ EGL10.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
+ EGL10.EGL_RED_SIZE, 8,
+ EGL10.EGL_GREEN_SIZE, 8,
+ EGL10.EGL_BLUE_SIZE, 8,
+ EGL10.EGL_ALPHA_SIZE, 0,
+ EGL10.EGL_NONE
+ };
+ EGLConfig[] configs = new EGLConfig[1];
+ int[] configCounts = new int[1];
+
+ EGL10 egl = (EGL10)EGLContext.getEGL();
+ EGLDisplay eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+ int[] version = new int[2];
+ egl.eglInitialize(eglDisplay, version);
+ egl.eglChooseConfig(eglDisplay, attribList, configs, 1, configCounts);
+
+ attribList = new int[]{EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE};
+ EGLContext eglContext = egl.eglCreateContext(eglDisplay, configs[0], EGL10.EGL_NO_CONTEXT, attribList);
+
+ egl.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, eglContext);
+
+ GL10 gl = (GL10)eglContext.getGL();
+ String gpuRenderer = Objects.toString(gl.glGetString(GL10.GL_RENDERER), "");
+ String gpuVendor = Objects.toString(gl.glGetString(GL10.GL_VENDOR), "");
+ String gpuVersion = Objects.toString(gl.glGetString(GL10.GL_VERSION), "");
+
+ gpuInfo.put("renderer", gpuRenderer);
+ gpuInfo.put("vendor", gpuVendor);
+ gpuInfo.put("version", gpuVersion);
+
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ preferences.edit()
+ .putString("gpu_renderer", gpuRenderer)
+ .putString("gpu_vendor", gpuVendor)
+ .putString("gpu_version", gpuVersion)
+ .apply();
+
+ synchronized (thread) {
+ thread.notify();
+ }
+ })).start();
+
+ synchronized (thread) {
+ try {
+ thread.wait();
+ }
+ catch (InterruptedException e) {}
+ }
+ return gpuInfo;
+ }
+
+ public static String getRenderer(Context context) {
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String value = preferences.getString("gpu_renderer", "");
+ if (!value.isEmpty()) return value;
+
+ ArrayMap gpuInfo = loadGPUInformation(context);
+ return gpuInfo.get("renderer");
+ }
+
+ public static String getVendor(Context context) {
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String value = preferences.getString("gpu_vendor", "");
+ if (!value.isEmpty()) return value;
+
+ ArrayMap gpuInfo = loadGPUInformation(context);
+ return gpuInfo.get("vendor");
+ }
+
+ public static String getVersion(Context context) {
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String value = preferences.getString("gpu_version", "");
+ if (!value.isEmpty()) return value;
+
+ ArrayMap gpuInfo = loadGPUInformation(context);
+ return gpuInfo.get("version");
+ }
+
+ public static boolean isAdreno6xx(Context context) {
+ return getRenderer(context).toLowerCase(Locale.ENGLISH).matches(".*adreno[^6]+6[0-9]{2}.*");
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/HttpUtils.java b/app/src/main/java/com/winlator/core/HttpUtils.java
new file mode 100644
index 0000000..c41fbd3
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/HttpUtils.java
@@ -0,0 +1,92 @@
+package com.winlator.core;
+
+import android.app.Activity;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public abstract class HttpUtils {
+ private static void downloadAsync(String url, Callback onDownloadComplete) {
+ try {
+ HttpURLConnection connection = (HttpURLConnection)(new URL(url)).openConnection();
+ if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ onDownloadComplete.call(null);
+ return;
+ }
+
+ byte[] bytes;
+ try (InputStream inStream = connection.getInputStream()) {
+ bytes = StreamUtils.copyToByteArray(inStream);
+ }
+ onDownloadComplete.call(new String(bytes, StandardCharsets.UTF_8));
+ }
+ catch (Exception e) {
+ onDownloadComplete.call(null);
+ }
+ }
+
+ public static void download(final String url, final Callback onDownloadComplete) {
+ Executors.newSingleThreadExecutor().execute(() -> downloadAsync(url, onDownloadComplete));
+ }
+
+ private static void downloadAsync(String url, File destination, AtomicBoolean interruptRef, Callback onPublishProgress, Callback onDownloadComplete) {
+ try {
+ interruptRef.set(false);
+ HttpURLConnection connection = (HttpURLConnection)(new URL(url)).openConnection();
+ if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ onDownloadComplete.call(false);
+ return;
+ }
+
+ int contentLength = connection.getContentLength();
+ try (InputStream inStream = new BufferedInputStream(connection.getInputStream(), StreamUtils.BUFFER_SIZE);
+ OutputStream outStream = new FileOutputStream(destination)) {
+
+ byte[] buffer = new byte[1024];
+ int totalSize = 0;
+ int bytesRead;
+ while ((bytesRead = inStream.read(buffer)) != -1 && !interruptRef.get()) {
+ totalSize += bytesRead;
+ if (onPublishProgress != null) {
+ int progress = (int)(((float)totalSize / contentLength) * 100);
+ onPublishProgress.call(progress);
+ }
+ outStream.write(buffer, 0, bytesRead);
+ }
+
+ }
+
+ onDownloadComplete.call(!interruptRef.get());
+ }
+ catch (Exception e) {
+ onDownloadComplete.call(false);
+ }
+ }
+
+ public static void download(final Activity activity, final String url, final File destination, final Callback onDownloadComplete) {
+ final DownloadProgressDialog dialog = new DownloadProgressDialog(activity);
+ final AtomicBoolean interruptRef = new AtomicBoolean();
+ dialog.show(() -> interruptRef.set(true));
+ Executors.newSingleThreadExecutor().execute(() -> {
+ downloadAsync(url, destination, interruptRef, (progress) -> {
+ activity.runOnUiThread(() -> {
+ dialog.setProgress(progress);
+ });
+ }, (success) -> {
+ if (!success && destination.isFile()) destination.delete();
+ activity.runOnUiThread(() -> {
+ dialog.close();
+ onDownloadComplete.call(success);
+ });
+ });
+ });
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/MSLink.java b/app/src/main/java/com/winlator/core/MSLink.java
new file mode 100644
index 0000000..f4df2a3
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/MSLink.java
@@ -0,0 +1,125 @@
+package com.winlator.core;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public abstract class MSLink {
+ private static int charToHexDigit(char chr) {
+ return chr >= 'A' ? chr - 'A' + 10 : chr - '0';
+ }
+
+ private static byte twoCharsToByte(char chr1, char chr2) {
+ return (byte)(charToHexDigit(Character.toUpperCase(chr1)) * 16 + charToHexDigit(Character.toUpperCase(chr2)));
+ }
+
+ private static byte[] convertCLSIDtoDATA(String str) {
+ return new byte[]{
+ twoCharsToByte(str.charAt(6), str.charAt(7)),
+ twoCharsToByte(str.charAt(4), str.charAt(5)),
+ twoCharsToByte(str.charAt(2), str.charAt(3)),
+ twoCharsToByte(str.charAt(0), str.charAt(1)),
+ twoCharsToByte(str.charAt(11), str.charAt(12)),
+ twoCharsToByte(str.charAt(9), str.charAt(10)),
+ twoCharsToByte(str.charAt(16), str.charAt(17)),
+ twoCharsToByte(str.charAt(14), str.charAt(15)),
+ twoCharsToByte(str.charAt(19), str.charAt(20)),
+ twoCharsToByte(str.charAt(21), str.charAt(22)),
+ twoCharsToByte(str.charAt(24), str.charAt(25)),
+ twoCharsToByte(str.charAt(26), str.charAt(27)),
+ twoCharsToByte(str.charAt(28), str.charAt(29)),
+ twoCharsToByte(str.charAt(30), str.charAt(31)),
+ twoCharsToByte(str.charAt(32), str.charAt(33)),
+ twoCharsToByte(str.charAt(34), str.charAt(35))
+ };
+ }
+
+ private static byte[] stringToByteArray(String str) {
+ byte[] bytes = new byte[str.length()];
+ for (int i = 0; i < bytes.length; i++) bytes[i] = (byte)str.charAt(i);
+ return bytes;
+ }
+
+ private static byte[] generateIDLIST(byte[] bytes) {
+ String itemSize = Integer.toHexString(0x10000 + bytes.length + 2).substring(1);
+ return ArrayUtils.concat(new byte[]{Byte.parseByte(itemSize.substring(2, 4), 16), Byte.parseByte(itemSize.substring(0, 2), 16)}, bytes);
+ }
+
+ public static void createFile(String targetPath, File outputFile) {
+ byte[] HeaderSize = new byte[]{0x4c, 0x00, 0x00, 0x00};
+ byte[] LinkCLSID = convertCLSIDtoDATA("00021401-0000-0000-c000-000000000046");
+ byte[] LinkFlags = new byte[]{0x01, 0x01, 0x00, 0x00};
+
+ byte[] FileAttributes, prefixOfTarget;
+ targetPath = targetPath.replaceAll("/+", "\\\\");
+ if (targetPath.endsWith("\\")) {
+ FileAttributes = new byte[]{0x10, 0x00, 0x00, 0x00};
+ prefixOfTarget = new byte[]{0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+ targetPath = targetPath.replaceAll("\\\\+$", "");
+ }
+ else {
+ FileAttributes = new byte[]{0x20, 0x00, 0x00, 0x00};
+ prefixOfTarget = new byte[]{0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+ }
+
+ byte[] CreationTime, AccessTime, WriteTime;
+ CreationTime = AccessTime = WriteTime = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+ byte[] FileSize, IconIndex;
+ FileSize = IconIndex = new byte[]{0x00, 0x00, 0x00, 0x00};
+ byte[] ShowCommand = new byte[]{0x01, 0x00, 0x00, 0x00};
+ byte[] Hotkey = new byte[]{0x00, 0x00};
+ byte[] Reserved = new byte[]{0x00, 0x00};
+ byte[] Reserved2 = new byte[]{0x00, 0x00, 0x00, 0x00};
+ byte[] Reserved3 = new byte[]{0x00, 0x00, 0x00, 0x00};
+
+ byte[] CLSIDComputer = convertCLSIDtoDATA("20d04fe0-3aea-1069-a2d8-08002b30309d");
+ byte[] CLSIDNetwork = convertCLSIDtoDATA("208d2c60-3aea-1069-a2d7-08002b30309d");
+
+ byte[] itemData, prefixRoot, targetRoot, targetLeaf;
+ if (targetPath.startsWith("\\")) {
+ prefixRoot = new byte[]{(byte)0xc3, 0x01, (byte)0x81};
+ targetRoot = stringToByteArray(targetPath);
+ targetLeaf = !targetPath.endsWith("\\") ? stringToByteArray(targetPath.substring(targetPath.lastIndexOf("\\") + 1)) : null;
+ itemData = ArrayUtils.concat(new byte[]{0x1f, 0x58}, CLSIDNetwork);
+ }
+ else {
+ prefixRoot = new byte[]{0x2f};
+ int index = targetPath.indexOf("\\");
+ targetRoot = stringToByteArray(targetPath.substring(0, index+1));
+ targetLeaf = stringToByteArray(targetPath.substring(index+1));
+ itemData = ArrayUtils.concat(new byte[]{0x1f, 0x50}, CLSIDComputer);
+ }
+
+ targetRoot = ArrayUtils.concat(targetRoot, new byte[21]);
+
+ byte[] endOfString = new byte[]{0x00};
+ byte[] IDListItems = ArrayUtils.concat(generateIDLIST(itemData), generateIDLIST(ArrayUtils.concat(prefixRoot, targetRoot, endOfString)));
+ if (targetLeaf != null) IDListItems = ArrayUtils.concat(IDListItems, generateIDLIST(ArrayUtils.concat(prefixOfTarget, targetLeaf, endOfString)));
+ byte[] IDList = generateIDLIST(IDListItems);
+
+ byte[] TerminalID = new byte[]{0x00, 0x00};
+
+ try (FileOutputStream os = new FileOutputStream(outputFile)) {
+ os.write(HeaderSize);
+ os.write(LinkCLSID);
+ os.write(LinkFlags);
+ os.write(FileAttributes);
+ os.write(CreationTime);
+ os.write(AccessTime);
+ os.write(WriteTime);
+ os.write(FileSize);
+ os.write(IconIndex);
+ os.write(ShowCommand);
+ os.write(Hotkey);
+ os.write(Reserved);
+ os.write(Reserved2);
+ os.write(Reserved3);
+ os.write(IDList);
+ os.write(TerminalID);
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/OBBImageInstaller.java b/app/src/main/java/com/winlator/core/OBBImageInstaller.java
new file mode 100644
index 0000000..5c8fba7
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/OBBImageInstaller.java
@@ -0,0 +1,160 @@
+package com.winlator.core;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+
+import androidx.preference.PreferenceManager;
+
+import com.winlator.MainActivity;
+import com.winlator.R;
+import com.winlator.contentdialog.ContentDialog;
+import com.winlator.xenvironment.ImageFs;
+
+import java.io.File;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class OBBImageInstaller {
+ public static final byte LATEST_VERSION = 3;
+ private static final String DOWNLOAD_URL = "https://github.com/brunodev85/winlator/releases/download/v3.1.0/main."+LATEST_VERSION+".com.winlator.obb";
+
+ private static void installFromSource(final MainActivity activity, final Object source, int foundVersion, final Runnable onSuccessCallback) {
+ if (source instanceof Uri) {
+ Uri uri = (Uri)source;
+ Matcher matcher = Pattern.compile("main\\.([0-9]+)\\.com\\.winlator").matcher(uri.getPath());
+ if (matcher.find()) {
+ foundVersion = Integer.parseInt(matcher.group(1));
+ }
+ else {
+ AppUtils.showToast(activity, R.string.unable_to_install_obb_image);
+ return;
+ }
+ }
+
+ ImageFs imageFs = ImageFs.find(activity);
+ final File rootDir = imageFs.getRootDir();
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.remove("current_box86_version");
+ editor.remove("current_box64_version");
+ editor.apply();
+
+ if (foundVersion <= 0) foundVersion = imageFs.isValid() ? imageFs.getVersion() + 1 : 1;
+ final int finalVersion = foundVersion;
+
+ activity.preloaderDialog.show(R.string.installing_obb_image);
+ Executors.newSingleThreadExecutor().execute(() -> {
+ clearRootDir(rootDir);
+
+ boolean success = false;
+ if (source instanceof File) {
+ success = TarZstdUtils.extract((File)source, rootDir);
+ }
+ else if (source instanceof Uri) {
+ success = TarZstdUtils.extract(activity, (Uri)source, rootDir);
+ }
+
+ if (success) {
+ imageFs.createImgVersionFile(finalVersion);
+ if (source instanceof File) ((File)source).delete();
+ if (onSuccessCallback != null) activity.runOnUiThread(onSuccessCallback);
+ }
+ else AppUtils.showToast(activity, R.string.unable_to_install_obb_image);
+
+ activity.preloaderDialog.closeOnUiThread();
+ });
+ }
+
+ public static void openFileForInstall(final MainActivity activity, final Runnable onSuccessCallback) {
+ activity.setOpenFileCallback((uri) -> installFromSource(activity, uri, 0, onSuccessCallback));
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("*/*");
+ activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST_CODE);
+ }
+
+ public static void downloadFileForInstall(final MainActivity activity, final Runnable onSuccessCallback) {
+ AppUtils.keepScreenOn(activity);
+ String filename = DOWNLOAD_URL.substring(DOWNLOAD_URL.lastIndexOf("/") + 1);
+ final File destination = new File(activity.getCacheDir(), filename);
+ if (destination.isFile()) destination.delete();
+ HttpUtils.download(activity, DOWNLOAD_URL, destination, (success) -> {
+ if (success) {
+ installFromSource(activity, destination, LATEST_VERSION, onSuccessCallback);
+ }
+ else AppUtils.showToast(activity, R.string.unable_to_download_file);
+ });
+ }
+
+ public static void installIfNeeded(final MainActivity activity) {
+ ImageFs imageFs = ImageFs.find(activity);
+ final AtomicReference obbFileRef = new AtomicReference<>();
+ int foundVersion = findOBBFile(activity, obbFileRef);
+
+ if (!imageFs.isValid() || imageFs.getVersion() < foundVersion) {
+ if (foundVersion == -1) {
+ ContentDialog.confirm(activity, R.string.unable_to_find_the_obb_image_do_you_want_to_download_file_and_install, () -> downloadFileForInstall(activity, null));
+ }
+ else OBBImageInstaller.installFromSource(activity, obbFileRef.get(), foundVersion, null);
+ }
+ }
+
+ private static void clearOptDir(File optDir) {
+ File[] files = optDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.getName().equals("installed-wine")) continue;
+ FileUtils.delete(file);
+ }
+ }
+ }
+
+ private static void clearRootDir(File rootDir) {
+ if (rootDir.isDirectory()) {
+ File[] files = rootDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ String name = file.getName();
+ if (name.equals("home") || name.equals("opt")) {
+ if (name.equals("opt")) clearOptDir(file);
+ continue;
+ }
+ }
+ FileUtils.delete(file);
+ }
+ }
+ }
+ else rootDir.mkdirs();
+ }
+
+ private static int findOBBFile(Context context, AtomicReference result) {
+ String packageName = context.getPackageName();
+ int foundObbVersion;
+ try {
+ foundObbVersion = context.getPackageManager().getPackageInfo(packageName, 0).versionCode;
+ }
+ catch (PackageManager.NameNotFoundException unused) {
+ foundObbVersion = 0;
+ }
+
+ File obbDir = context.getObbDir();
+ while (foundObbVersion >= 0) {
+ String filename = "main."+foundObbVersion+"."+packageName+".obb";
+ File file = new File(obbDir, filename);
+ if (file.exists()) {
+ result.set(file);
+ return foundObbVersion;
+ }
+ foundObbVersion--;
+ }
+ foundObbVersion = -1;
+ return foundObbVersion;
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/OnExtractFileListener.java b/app/src/main/java/com/winlator/core/OnExtractFileListener.java
new file mode 100644
index 0000000..7b93ca5
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/OnExtractFileListener.java
@@ -0,0 +1,7 @@
+package com.winlator.core;
+
+import java.io.File;
+
+public interface OnExtractFileListener {
+ File onExtractFile(File destination, String entryName);
+}
diff --git a/app/src/main/java/com/winlator/core/PreloaderDialog.java b/app/src/main/java/com/winlator/core/PreloaderDialog.java
new file mode 100644
index 0000000..66cb2e3
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/PreloaderDialog.java
@@ -0,0 +1,62 @@
+package com.winlator.core;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import com.winlator.R;
+
+public class PreloaderDialog {
+ private final Activity activity;
+ private Dialog dialog;
+
+ public PreloaderDialog(Activity activity) {
+ this.activity = activity;
+ }
+
+ private void create() {
+ if (dialog != null) return;
+ dialog = new Dialog(activity, android.R.style.Theme_Translucent_NoTitleBar_Fullscreen);
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ dialog.setCancelable(false);
+ dialog.setCanceledOnTouchOutside(false);
+ dialog.setContentView(R.layout.preloader_dialog);
+
+ Window window = dialog.getWindow();
+ if (window != null) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
+ window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ }
+ }
+
+ public synchronized void show(int textResId) {
+ if (isShowing()) return;
+ close();
+ if (dialog == null) create();
+ ((TextView)dialog.findViewById(R.id.TextView)).setText(textResId);
+ dialog.show();
+ }
+
+ public void showOnUiThread(final int textResId) {
+ activity.runOnUiThread(() -> show(textResId));
+ }
+
+ public synchronized void close() {
+ try {
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+ catch (Exception e) {}
+ }
+
+ public void closeOnUiThread() {
+ activity.runOnUiThread(this::close);
+ }
+
+ public boolean isShowing() {
+ return dialog != null && dialog.isShowing();
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/ProcessHelper.java b/app/src/main/java/com/winlator/core/ProcessHelper.java
new file mode 100644
index 0000000..6978521
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/ProcessHelper.java
@@ -0,0 +1,157 @@
+package com.winlator.core;
+
+import android.os.Process;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.concurrent.Executors;
+
+public abstract class ProcessHelper {
+ public static boolean debugMode = false;
+ public static Callback debugCallback;
+ private static final byte SIGCONT = 18;
+ private static final byte SIGSTOP = 19;
+
+ public static void suspendProcess(int pid) {
+ Process.sendSignal(pid, SIGSTOP);
+ }
+
+ public static void resumeProcess(int pid) {
+ Process.sendSignal(pid, SIGCONT);
+ }
+
+ public static int exec(String command) {
+ return exec(command, null);
+ }
+
+ public static int exec(String command, String[] envp) {
+ return exec(command, envp, null);
+ }
+
+ public static int exec(String command, String[] envp, File workingDir) {
+ return exec(command, envp, workingDir, null);
+ }
+
+ public static int exec(String command, String[] envp, File workingDir, Callback terminationCallback) {
+ int pid = -1;
+ try {
+ java.lang.Process process = Runtime.getRuntime().exec(splitCommand(command), envp, workingDir);
+ Field pidField = process.getClass().getDeclaredField("pid");
+ pidField.setAccessible(true);
+ pid = pidField.getInt(process);
+ pidField.setAccessible(false);
+
+ Callback debugCallback = ProcessHelper.debugCallback;
+ if (debugMode || debugCallback != null) {
+ createDebugThread(process.getInputStream(), debugCallback);
+ createDebugThread(process.getErrorStream(), debugCallback);
+ }
+
+ if (terminationCallback != null) createWaitForThread(process, terminationCallback);
+ }
+ catch (Exception e) {}
+ return pid;
+ }
+
+ private static void createDebugThread(final InputStream inputStream, final Callback debugCallback) {
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (debugCallback != null) {
+ debugCallback.call(line);
+ }
+ else System.out.println(line);
+ }
+ }
+ catch (IOException e) {}
+ });
+ }
+
+ private static void createWaitForThread(java.lang.Process process, final Callback terminationCallback) {
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ int status = process.waitFor();
+ terminationCallback.call(status);
+ }
+ catch (InterruptedException e) {}
+ });
+ }
+
+ public static String[] splitCommand(String command) {
+ ArrayList result = new ArrayList<>();
+ boolean startedQuotes = false;
+ String value = "";
+ char currChar, nextChar;
+ for (int i = 0, count = command.length(); i < count; i++) {
+ currChar = command.charAt(i);
+
+ if (startedQuotes) {
+ if (currChar == '"') {
+ startedQuotes = false;
+ if (!value.isEmpty()) {
+ value += '"';
+ result.add(value);
+ value = "";
+ }
+ }
+ else value += currChar;
+ }
+ else if (currChar == '"') {
+ startedQuotes = true;
+ value += '"';
+ }
+ else {
+ nextChar = i < count-1 ? command.charAt(i+1) : '\0';
+ if (currChar == ' ' || (currChar == '\\' && nextChar == ' ')) {
+ if (currChar == '\\') {
+ value += ' ';
+ i++;
+ }
+ else if (!value.isEmpty()) {
+ result.add(value);
+ value = "";
+ }
+ }
+ else {
+ value += currChar;
+ if (i == count-1) {
+ result.add(value);
+ value = "";
+ }
+ }
+ }
+ }
+
+ return result.toArray(new String[0]);
+ }
+
+ public static String getSingleCPUAffinityMask() {
+ int numProcessors = (byte)Runtime.getRuntime().availableProcessors();
+ int affinityMask = (int)Math.pow(2, numProcessors-1);
+ return Integer.toHexString(affinityMask);
+ }
+
+ public static String getAffinityMask(String cpuList) {
+ String[] values = cpuList.split(",");
+ int affinityMask = 0;
+ for (String value : values) {
+ byte index = Byte.parseByte(value);
+ affinityMask |= (int)Math.pow(2, index);
+ }
+ return Integer.toHexString(affinityMask);
+ }
+
+ public static int getAffinityMask(boolean[] cpuList) {
+ int affinityMask = 0;
+ for (int i = 0; i < cpuList.length; i++) {
+ if (cpuList[i]) affinityMask |= (int)Math.pow(2, i);
+ }
+ return affinityMask;
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/StreamUtils.java b/app/src/main/java/com/winlator/core/StreamUtils.java
new file mode 100644
index 0000000..314f7e7
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/StreamUtils.java
@@ -0,0 +1,33 @@
+package com.winlator.core;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class StreamUtils {
+ public static final int BUFFER_SIZE = 64 * 1024;
+
+ public static byte[] copyToByteArray(InputStream inStream) {
+ if (inStream == null) return new byte[0];
+
+ ByteArrayOutputStream outStream = new ByteArrayOutputStream(BUFFER_SIZE);
+ copy(inStream, outStream);
+ return outStream.toByteArray();
+ }
+
+ public static boolean copy(InputStream inStream, OutputStream outStream) {
+ try {
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int amountRead;
+ while ((amountRead = inStream.read(buffer)) != -1) {
+ outStream.write(buffer, 0, amountRead);
+ }
+ outStream.flush();
+ return true;
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/StringUtils.java b/app/src/main/java/com/winlator/core/StringUtils.java
new file mode 100644
index 0000000..ef2c7c6
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/StringUtils.java
@@ -0,0 +1,63 @@
+package com.winlator.core;
+
+import android.content.Context;
+
+import java.nio.charset.Charset;
+import java.util.Locale;
+
+public class StringUtils {
+ public static String removeEndSlash(String value) {
+ while (value.endsWith("/") || value.endsWith("\\")) value = value.substring(0, value.length()-1);
+ return value;
+ }
+
+ public static String addEndSlash(String value) {
+ return value.endsWith("/") ? value : value+"/";
+ }
+
+ public static String insert(String text, int index, String value) {
+ return text.substring(0, index) + value + text.substring(index);
+ }
+
+ public static String replace(String text, int start, int end, String value) {
+ return text.substring(0, start) + value + text.substring(end);
+ }
+
+ public static String unescape(String path) {
+ return path.replaceAll("\\\\([^\\\\]+)", "$1").replaceAll("\\\\([^\\\\]+)", "$1").replaceAll("\\\\\\\\", "\\\\").trim();
+ }
+
+ public static String parseIdentifier(Object text) {
+ return text.toString().toLowerCase(Locale.ENGLISH).replaceAll(" *\\(([^\\)]+)\\)$", "").replaceAll("( \\+ )+| +", "-");
+ }
+
+ public static String getString(Context context, String resName) {
+ String string = null;
+ try {
+ resName = resName.toLowerCase(Locale.ENGLISH);
+ int resID = context.getResources().getIdentifier(resName, "string", context.getPackageName());
+ string = context.getString(resID);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ return string != null && !string.isEmpty() ? string : resName;
+ }
+
+ public static String formatBytes(long bytes) {
+ if (bytes <= 0) return "0 bytes";
+ final String[] units = new String[]{"bytes", "KB", "MB", "GB", "TB"};
+ int digitGroups = (int)(Math.log10(bytes) / Math.log10(1024));
+ return String.format(Locale.ENGLISH, "%.2f", bytes / Math.pow(1024, digitGroups))+" "+units[digitGroups];
+ }
+
+ public static String fromANSIString(byte[] bytes) {
+ return fromANSIString(bytes, null);
+ }
+
+ public static String fromANSIString(byte[] bytes, Charset charset) {
+ String value = charset != null ? new String(bytes, charset) : new String(bytes);
+ int indexOfNull = value.indexOf('\0');
+ return indexOfNull != -1 ? value.substring(0, indexOfNull) : value;
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/TarXZUtils.java b/app/src/main/java/com/winlator/core/TarXZUtils.java
new file mode 100644
index 0000000..038a135
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/TarXZUtils.java
@@ -0,0 +1,50 @@
+package com.winlator.core;
+
+import android.content.Context;
+import android.net.Uri;
+
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public abstract class TarXZUtils {
+ public static boolean extract(Context context, Uri source, File destination) {
+ if (source == null) return false;
+ try (InputStream inStream = new XZCompressorInputStream(new BufferedInputStream(context.getContentResolver().openInputStream(source), StreamUtils.BUFFER_SIZE));
+ ArchiveInputStream tar = new TarArchiveInputStream(inStream)) {
+ TarArchiveEntry entry;
+ while ((entry = (TarArchiveEntry)tar.getNextEntry()) != null) {
+ if (!tar.canReadEntryData(entry)) continue;
+ File file = new File(destination, entry.getName());
+
+ if (entry.isDirectory()) {
+ if (!file.isDirectory()) file.mkdirs();
+ }
+ else {
+ if (entry.isSymbolicLink()) {
+ FileUtils.symlink(entry.getLinkName(), file.getAbsolutePath());
+ }
+ else {
+ try (BufferedOutputStream outStream = new BufferedOutputStream(new FileOutputStream(file), StreamUtils.BUFFER_SIZE)) {
+ if (!StreamUtils.copy(tar, outStream)) return false;
+ }
+ }
+ }
+
+ FileUtils.chmod(file, 0771);
+ }
+ return true;
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/TarZstdUtils.java b/app/src/main/java/com/winlator/core/TarZstdUtils.java
new file mode 100644
index 0000000..7fc2b6d
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/TarZstdUtils.java
@@ -0,0 +1,152 @@
+package com.winlator.core;
+
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.IntRange;
+
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.ArchiveOutputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream;
+import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public abstract class TarZstdUtils {
+ private static void addFile(ArchiveOutputStream tar, File file, String entryName) {
+ try {
+ tar.putArchiveEntry(tar.createArchiveEntry(file, entryName));
+ try (BufferedInputStream inStream = new BufferedInputStream(new FileInputStream(file), StreamUtils.BUFFER_SIZE)) {
+ StreamUtils.copy(inStream, tar);
+ }
+ tar.closeArchiveEntry();
+ }
+ catch (Exception e) {}
+ }
+
+ private static void addDirectory(ArchiveOutputStream tar, File folder, String basePath) throws IOException {
+ File[] files = folder.listFiles();
+ if (files == null) return;
+ for (File file : files) {
+ if (FileUtils.isSymlink(file)) continue;
+ if (file.isDirectory()) {
+ String entryName = basePath+file.getName() + "/";
+ tar.putArchiveEntry(tar.createArchiveEntry(folder, entryName));
+ tar.closeArchiveEntry();
+ addDirectory(tar, file, entryName);
+ }
+ else addFile(tar, file, basePath+file.getName());
+ }
+ }
+
+ public static void compress(File file, File zipFile) {
+ compress(file, zipFile, 3);
+ }
+
+ public static void compress(File file, File zipFile, @IntRange(from = 1, to = 19) int level) {
+ compress(new File[]{file}, zipFile, level);
+ }
+
+ public static void compress(File[] files, File destination, @IntRange(from = 1, to = 19) int level) {
+ try (OutputStream outStream = new ZstdCompressorOutputStream(new BufferedOutputStream(new FileOutputStream(destination), StreamUtils.BUFFER_SIZE), level);
+ TarArchiveOutputStream tar = new TarArchiveOutputStream(outStream)) {
+ tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
+ for (File file : files) {
+ if (FileUtils.isSymlink(file)) continue;
+ if (file.isDirectory()) {
+ String basePath = file.getName() + "/";
+ tar.putArchiveEntry(tar.createArchiveEntry(file, basePath));
+ tar.closeArchiveEntry();
+ addDirectory(tar, file, basePath);
+ }
+ else addFile(tar, file, file.getName());
+ }
+ tar.finish();
+ }
+ catch (IOException e) {}
+ }
+
+ public static boolean extract(Context context, String assetFile, File destination) {
+ return extract(context, assetFile, destination, null);
+ }
+
+ public static boolean extract(Context context, String assetFile, File destination, OnExtractFileListener onExtractFileListener) {
+ try {
+ return extract(context.getAssets().open(assetFile), destination, onExtractFileListener);
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+
+ public static boolean extract(File source, File destination) {
+ return extract(source, destination, null);
+ }
+
+ public static boolean extract(File source, File destination, OnExtractFileListener onExtractFileListener) {
+ if (source == null || !source.isFile()) return false;
+ try {
+ return extract(new BufferedInputStream(new FileInputStream(source), StreamUtils.BUFFER_SIZE), destination, onExtractFileListener);
+ }
+ catch (FileNotFoundException e) {
+ return false;
+ }
+ }
+
+ public static boolean extract(Context context, Uri source, File destination) {
+ if (source == null) return false;
+ try {
+ return extract(context.getContentResolver().openInputStream(source), destination, null);
+ }
+ catch (FileNotFoundException e) {
+ return false;
+ }
+ }
+
+ private static boolean extract(InputStream source, File destination, OnExtractFileListener onExtractFileListener) {
+ try (InputStream inStream = new ZstdCompressorInputStream(source);
+ ArchiveInputStream tar = new TarArchiveInputStream(inStream)) {
+ TarArchiveEntry entry;
+ while ((entry = (TarArchiveEntry)tar.getNextEntry()) != null) {
+ if (!tar.canReadEntryData(entry)) continue;
+ File file = new File(destination, entry.getName());
+
+ if (onExtractFileListener != null) {
+ file = onExtractFileListener.onExtractFile(destination, entry.getName());
+ if (file == null) continue;
+ }
+
+ if (entry.isDirectory()) {
+ if (!file.isDirectory()) file.mkdirs();
+ }
+ else {
+ if (entry.isSymbolicLink()) {
+ FileUtils.symlink(entry.getLinkName(), file.getAbsolutePath());
+ }
+ else {
+ try (BufferedOutputStream outStream = new BufferedOutputStream(new FileOutputStream(file), StreamUtils.BUFFER_SIZE)) {
+ if (!StreamUtils.copy(tar, outStream)) return false;
+ }
+ }
+ }
+
+ FileUtils.chmod(file, 0771);
+ }
+ return true;
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/UnitUtils.java b/app/src/main/java/com/winlator/core/UnitUtils.java
new file mode 100644
index 0000000..d365455
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/UnitUtils.java
@@ -0,0 +1,13 @@
+package com.winlator.core;
+
+import android.content.res.Resources;
+
+public class UnitUtils {
+ public static float dpToPx(float dp) {
+ return dp * Resources.getSystem().getDisplayMetrics().density;
+ }
+
+ public static float pxToDp(float px) {
+ return px / Resources.getSystem().getDisplayMetrics().density;
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/WineInfo.java b/app/src/main/java/com/winlator/core/WineInfo.java
new file mode 100644
index 0000000..d8517bd
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/WineInfo.java
@@ -0,0 +1,104 @@
+package com.winlator.core;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.winlator.xenvironment.ImageFs;
+
+import java.io.File;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class WineInfo implements Parcelable {
+ public static final WineInfo MAIN_WINE_VERSION = new WineInfo("8.0.1", "x86_64");
+ private static final Pattern pattern = Pattern.compile("^wine\\-([0-9\\.]+)\\-?([0-9\\.]+)?\\-(x86|x86_64)$");
+ public final String version;
+ public final String subversion;
+ public final String path;
+ private String arch;
+
+ public WineInfo(String version, String arch) {
+ this.version = version;
+ this.subversion = null;
+ this.arch = arch;
+ this.path = null;
+ }
+
+ public WineInfo(String version, String subversion, String arch, String path) {
+ this.version = version;
+ this.subversion = subversion != null && !subversion.isEmpty() ? subversion : null;
+ this.arch = arch;
+ this.path = path;
+ }
+
+ private WineInfo(Parcel in) {
+ version = in.readString();
+ subversion = in.readString();
+ arch = in.readString();
+ path = in.readString();
+ }
+
+ public String getArch() {
+ return arch;
+ }
+
+ public void setArch(String arch) {
+ this.arch = arch;
+ }
+
+ public boolean isWin64() {
+ return arch.equals("x86_64");
+ }
+
+ public String identifier() {
+ return "wine-"+fullVersion()+"-"+arch;
+ }
+
+ public String fullVersion() {
+ return version+(subversion != null ? "-"+subversion : "");
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "Wine "+fullVersion()+" ("+arch+")";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public WineInfo createFromParcel(Parcel in) {
+ return new WineInfo(in);
+ }
+
+ public WineInfo[] newArray(int size) {
+ return new WineInfo[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(version);
+ dest.writeString(subversion);
+ dest.writeString(arch);
+ dest.writeString(path);
+ }
+
+ @NonNull
+ public static WineInfo fromIdentifier(Context context, String identifier) {
+ if (identifier.equals(MAIN_WINE_VERSION.identifier())) return MAIN_WINE_VERSION;
+ Matcher matcher = pattern.matcher(identifier);
+ if (matcher.find()) {
+ File installedWineDir = ImageFs.find(context).getInstalledWineDir();
+ String path = (new File(installedWineDir, identifier)).getPath();
+ return new WineInfo(matcher.group(1), matcher.group(2), matcher.group(3), path);
+ }
+ else return MAIN_WINE_VERSION;
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/WineRegistryEditor.java b/app/src/main/java/com/winlator/core/WineRegistryEditor.java
new file mode 100644
index 0000000..02e7ab4
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/WineRegistryEditor.java
@@ -0,0 +1,363 @@
+package com.winlator.core;
+
+import com.winlator.math.Mathf;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+public class WineRegistryEditor implements Closeable {
+ private final File file;
+ private final File cloneFile;
+ private boolean modified = false;
+
+ private static class Location {
+ private final int offset;
+ private final int start;
+ private final int end;
+
+ private Location(int offset, int start, int end) {
+ this.offset = offset;
+ this.start = start;
+ this.end = end;
+ }
+
+ private int length() {
+ return end - start;
+ }
+ }
+
+ public WineRegistryEditor(File file) {
+ this.file = file;
+ cloneFile = FileUtils.createTempFile(file.getParentFile(), FileUtils.getBasename(file.getPath()));
+ if (!file.isFile()) {
+ try {
+ cloneFile.createNewFile();
+ }
+ catch (IOException e) {}
+ }
+ else FileUtils.copy(file, cloneFile);
+ }
+
+ private static String escape(String str) {
+ return str.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+
+ private static String unescape(String str) {
+ return str.replace("\\\"", "\"").replace("\\\\", "\\");
+ }
+
+ private static boolean lineHasName(String line) {
+ int index;
+ return (index = line.indexOf('"')) != -1 &&
+ (index = line.indexOf('"', index)) != -1 &&
+ (index = line.indexOf('=', index)) != -1;
+ }
+
+ @Override
+ public void close() {
+ if (modified && cloneFile.exists()) {
+ cloneFile.renameTo(file);
+ }
+ else cloneFile.delete();
+ }
+
+ private Location createKey(String key) {
+ Location location = getParentKeyLocation(key);
+ boolean success = false;
+ int offset = 0;
+ int totalLength = 0;
+
+ char[] buffer = new char[StreamUtils.BUFFER_SIZE];
+ File tempFile = FileUtils.createTempFile(file.getParentFile(), FileUtils.getBasename(file.getPath()));
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(cloneFile), StreamUtils.BUFFER_SIZE);
+ BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile), StreamUtils.BUFFER_SIZE)) {
+
+ int length;
+ for (int i = 0, end = location != null ? location.end+1 : (int)cloneFile.length(); i < end; i += length) {
+ length = Math.min(buffer.length, end - i);
+ reader.read(buffer, 0, length);
+ writer.write(buffer, 0, length);
+ totalLength += length;
+ }
+
+ offset = totalLength;
+ long ticks1601To1970 = 86400L * (369 * 365 + 89) * 10000000;
+ long currentTime = System.currentTimeMillis() + ticks1601To1970;
+ String content = "\n["+escape(key)+"] "+((currentTime - ticks1601To1970) / 1000) +
+ String.format(Locale.ENGLISH, "\n#time=%x%08x", currentTime >> 32, (int)currentTime)+"\n";
+ writer.write(content);
+ totalLength += content.length() - 1;
+
+ while ((length = reader.read(buffer)) != -1) writer.write(buffer, 0, length);
+ success = true;
+ }
+ catch (IOException e) {}
+
+ if (success) {
+ modified = true;
+ tempFile.renameTo(cloneFile);
+ return new Location(offset, totalLength, totalLength);
+ }
+ else {
+ tempFile.delete();
+ return null;
+ }
+ }
+
+ public String getStringValue(String key, String name) {
+ return getStringValue(key, name, null);
+ }
+
+ public String getStringValue(String key, String name, String fallback) {
+ String value = getRawValue(key, name);
+ return value != null ? value.substring(1, value.length() - 1) : fallback;
+ }
+
+ public void setStringValue(String key, String name, String value) {
+ setRawValue(key, name, value != null ? "\""+escape(value)+"\"" : "\"\"");
+ }
+
+ public Integer getDwordValue(String key, String name) {
+ return getDwordValue(key, name, null);
+ }
+
+ public Integer getDwordValue(String key, String name, Integer fallback) {
+ String value = getRawValue(key, name);
+ return value != null ? Integer.decode("0x" + value.substring(6)) : fallback;
+ }
+
+ public void setDwordValue(String key, String name, int value) {
+ setRawValue(key, name, "dword:"+String.format("%08x", value));
+ }
+
+ public void setHexValue(String key, String name, String value) {
+ int start = (int)Mathf.roundTo(name.length(), 2) + 7;
+ StringBuilder lines = new StringBuilder();
+ for (int i = 0, j = start; i < value.length(); i++) {
+ if (i > 0 && (i % 2) == 0) lines.append(",");
+ if (j++ > 56) {
+ lines.append("\\\n ");
+ j = 8;
+ }
+ lines.append(value.charAt(i));
+ }
+ setRawValue(key, name, "hex:"+lines);
+ }
+
+ private String getRawValue(String key, String name) {
+ Location keyLocation = getKeyLocation(key);
+ if (keyLocation == null) return null;
+
+ Location valueLocation = getValueLocation(keyLocation, name);
+ if (valueLocation == null) return null;
+ boolean success = false;
+ char[] buffer = new char[valueLocation.length()];
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(cloneFile), StreamUtils.BUFFER_SIZE)) {
+ reader.skip(valueLocation.start);
+ success = reader.read(buffer) == buffer.length;
+ }
+ catch (IOException e) {}
+ return success ? unescape(new String(buffer)) : null;
+ }
+
+ private void setRawValue(String key, String name, String value) {
+ Location keyLocation = getKeyLocation(key);
+ if (keyLocation == null) keyLocation = createKey(key);
+ Location valueLocation = getValueLocation(keyLocation, name);
+ char[] buffer = new char[StreamUtils.BUFFER_SIZE];
+ boolean success = false;
+
+ File tempFile = FileUtils.createTempFile(file.getParentFile(), FileUtils.getBasename(file.getPath()));
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(cloneFile), StreamUtils.BUFFER_SIZE);
+ BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile), StreamUtils.BUFFER_SIZE)) {
+
+ int length;
+ for (int i = 0, end = valueLocation != null ? valueLocation.start : keyLocation.end; i < end; i += length) {
+ length = Math.min(buffer.length, end - i);
+ reader.read(buffer, 0, length);
+ writer.write(buffer, 0, length);
+ }
+
+ if (valueLocation == null) {
+ writer.write("\n"+(name != null ? "\""+escape(name)+"\"" : "@")+"="+value);
+ }
+ else {
+ writer.write(value);
+ reader.skip(valueLocation.length());
+ }
+
+ while ((length = reader.read(buffer)) != -1) writer.write(buffer, 0, length);
+ success = true;
+ }
+ catch (IOException e) {}
+
+ if (success) {
+ modified = true;
+ tempFile.renameTo(cloneFile);
+ }
+ else tempFile.delete();
+ }
+
+ public void removeValue(String key, String name) {
+ Location keyLocation = getKeyLocation(key);
+ if (keyLocation == null) return;
+
+ Location valueLocation = getValueLocation(keyLocation, name);
+ if (valueLocation == null) return;
+ removeRegion(valueLocation);
+ }
+
+ public boolean removeKey(String key) {
+ return removeKey(key, false);
+ }
+
+ public boolean removeKey(String key, boolean removeTree) {
+ boolean removed = false;
+ if (removeTree) {
+ Location location;
+ while ((location = getKeyLocation(key, true)) != null) {
+ if (removeRegion(location)) removed = true;
+ }
+ }
+ else {
+ Location location = getKeyLocation(key, false);
+ if (location != null && removeRegion(location)) removed = true;
+ }
+ return removed;
+ }
+
+ private boolean removeRegion(Location location) {
+ char[] buffer = new char[StreamUtils.BUFFER_SIZE];
+ boolean success = false;
+
+ File tempFile = FileUtils.createTempFile(file.getParentFile(), FileUtils.getBasename(file.getPath()));
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(cloneFile), StreamUtils.BUFFER_SIZE);
+ BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile), StreamUtils.BUFFER_SIZE)) {
+
+ int length = 0;
+ for (int i = 0; i < location.offset; i += length) {
+ length = Math.min(buffer.length, location.offset - i);
+ reader.read(buffer, 0, length);
+ writer.write(buffer, 0, length);
+ }
+
+ boolean skipLine = length > 1 && buffer[length-1] == '\n';
+ reader.skip(location.end - location.offset + (skipLine ? 1 : 0));
+ while ((length = reader.read(buffer)) != -1) writer.write(buffer, 0, length);
+ success = true;
+ }
+ catch (IOException e) {}
+
+ if (success) {
+ modified = true;
+ tempFile.renameTo(cloneFile);
+ }
+ else tempFile.delete();
+ return success;
+ }
+
+ private Location getKeyLocation(String key) {
+ return getKeyLocation(key, false);
+ }
+
+ private Location getKeyLocation(String key, boolean keyAsPrefix) {
+ try (BufferedReader reader = new BufferedReader(new FileReader(cloneFile), StreamUtils.BUFFER_SIZE)) {
+ key = "["+escape(key)+(!keyAsPrefix ? "]" : "");
+ int totalLength = 0;
+ int index;
+ int start = -1;
+ int end = -1;
+ int emptyLines = 0;
+ int offset = 0;
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (start == -1) {
+ index = line.indexOf(key);
+ if (index != -1) {
+ offset = totalLength + index - 1;
+ start = totalLength + line.length() + 1;
+ }
+ }
+ else {
+ index = line.indexOf('[');
+ if (index != -1) {
+ end = Math.max(-1, totalLength - emptyLines - 1);
+ break;
+ }
+ else emptyLines = line.isEmpty() ? emptyLines + 1 : 0;
+ }
+ totalLength += line.length() + 1;
+ }
+
+ if (end == -1) end = totalLength - 1;
+ return start != -1 ? new Location(offset, start, end) : null;
+ }
+ catch (IOException e) {
+ return null;
+ }
+ }
+
+ private Location getParentKeyLocation(String key) {
+ String[] parts = key.split("\\\\");
+ ArrayList stack = new ArrayList<>(Arrays.asList(parts).subList(0, parts.length - 1));
+
+ while (!stack.isEmpty()) {
+ String currentKey = String.join("\\", stack);
+ Location location = getKeyLocation(currentKey, true);
+ if (location != null) return location;
+ stack.remove(stack.size()-1);
+ }
+
+ return null;
+ }
+
+ private Location getValueLocation(Location keyLocation, String name) {
+ if (keyLocation.start == keyLocation.end) return null;
+ try (BufferedReader reader = new BufferedReader(new FileReader(cloneFile), StreamUtils.BUFFER_SIZE)) {
+ reader.skip(keyLocation.start);
+ name = name != null ? "\""+escape(name)+"\"=" : "@=";
+ int totalLength = 0;
+ int index;
+ int start = -1;
+ int end = -1;
+ int offset = 0;
+
+ String line;
+ while ((line = reader.readLine()) != null && totalLength < keyLocation.length()) {
+ if (start == -1) {
+ index = line.indexOf(name);
+ if (index != -1) {
+ offset = totalLength + index - 1;
+ start = totalLength + name.length() + index;
+ }
+ }
+ else {
+ if (line.isEmpty() || lineHasName(line)) {
+ end = totalLength - 1;
+ break;
+ }
+ }
+ totalLength += line.length() + 1;
+ }
+
+ if (end == -1) end = totalLength - 1;
+ return start != -1 ? new Location(keyLocation.start + offset, keyLocation.start + start, keyLocation.start + end) : null;
+ }
+ catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/WineStartMenuCreator.java b/app/src/main/java/com/winlator/core/WineStartMenuCreator.java
new file mode 100644
index 0000000..9fd6ac4
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/WineStartMenuCreator.java
@@ -0,0 +1,58 @@
+package com.winlator.core;
+
+import android.content.Context;
+
+import com.winlator.container.Container;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+
+public abstract class WineStartMenuCreator {
+ private static void createMenuEntry(JSONObject item, File currentDir) throws JSONException {
+ if (item.has("children")) {
+ currentDir = new File(currentDir, item.getString("name"));
+ currentDir.mkdirs();
+
+ JSONArray children = item.getJSONArray("children");
+ for (int i = 0; i < children.length(); i++) createMenuEntry(children.getJSONObject(i), currentDir);
+ }
+ else {
+ File outputFile = new File(currentDir, item.getString("name")+".lnk");
+ MSLink.createFile(item.getString("path"), outputFile);
+ }
+ }
+
+ private static void removeMenuEntry(JSONObject item, File currentDir) throws JSONException {
+ if (item.has("children")) {
+ currentDir = new File(currentDir, item.getString("name"));
+
+ JSONArray children = item.getJSONArray("children");
+ for (int i = 0; i < children.length(); i++) removeMenuEntry(children.getJSONObject(i), currentDir);
+
+ if (FileUtils.isEmpty(currentDir)) currentDir.delete();
+ }
+ else (new File(currentDir, item.getString("name")+".lnk")).delete();
+ }
+
+ private static void removeOldMenu(File containerStartMenuFile, File startMenuDir) throws JSONException {
+ if (!containerStartMenuFile.isFile()) return;
+ JSONArray data = new JSONArray(FileUtils.readString(containerStartMenuFile));
+ for (int i = 0; i < data.length(); i++) removeMenuEntry(data.getJSONObject(i), startMenuDir);
+ }
+
+ public static void create(Context context, Container container) {
+ try {
+ File startMenuDir = container.getStartMenuDir();
+ File containerStartMenuFile = new File(container.getRootDir(), ".startmenu");
+ removeOldMenu(containerStartMenuFile, startMenuDir);
+
+ JSONArray data = new JSONArray(FileUtils.readString(context, "wine_startmenu.json"));
+ FileUtils.writeString(containerStartMenuFile, data.toString());
+ for (int i = 0; i < data.length(); i++) createMenuEntry(data.getJSONObject(i), startMenuDir);
+ }
+ catch (JSONException e) {}
+ }
+}
diff --git a/app/src/main/java/com/winlator/core/WineUtils.java b/app/src/main/java/com/winlator/core/WineUtils.java
new file mode 100644
index 0000000..188c3fa
--- /dev/null
+++ b/app/src/main/java/com/winlator/core/WineUtils.java
@@ -0,0 +1,178 @@
+package com.winlator.core;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.winlator.container.Container;
+import com.winlator.xenvironment.ImageFs;
+import com.winlator.xenvironment.XEnvironment;
+import com.winlator.xenvironment.components.GuestProgramLauncherComponent;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class WineUtils {
+ public static void createDosdevicesSymlinks(Container container) {
+ String dosdevicesPath = (new File(container.getRootDir(), ".wine/dosdevices")).getPath();
+ File[] files = (new File(dosdevicesPath)).listFiles();
+ if (files != null) for (File file : files) if (file.getName().matches("[a-z]:")) file.delete();
+
+ FileUtils.symlink("../drive_c", dosdevicesPath+"/c:");
+ FileUtils.symlink("/", dosdevicesPath+"/z:");
+
+ for (String[] drive : container.drivesIterator()) {
+ FileUtils.symlink((new File(drive[1])).getAbsolutePath(), dosdevicesPath+"/"+drive[0].toLowerCase(Locale.ENGLISH)+":");
+ }
+ }
+
+ public static void extractWineFileForInstallAsync(Context context, Uri uri, Callback callback) {
+ Executors.newSingleThreadExecutor().execute(() -> {
+ File destination = new File(ImageFs.find(context).getInstalledWineDir(), "/preinstall/wine");
+ FileUtils.delete(destination);
+ destination.mkdirs();
+ boolean success = TarXZUtils.extract(context, uri, destination);
+ if (!success) FileUtils.delete(destination);
+ if (callback != null) callback.call(success ? destination : null);
+ });
+ }
+
+ public static void findWineVersionAsync(Context context, File wineDir, Callback callback) {
+ if (wineDir == null || !wineDir.isDirectory()) {
+ callback.call(null);
+ return;
+ }
+ File[] files = wineDir.listFiles();
+ if (files == null || files.length == 0) {
+ callback.call(null);
+ return;
+ }
+
+ if (files.length == 1) {
+ if (!files[0].isDirectory()) {
+ callback.call(null);
+ return;
+ }
+ wineDir = files[0];
+ files = wineDir.listFiles();
+ if (files == null || files.length == 0) {
+ callback.call(null);
+ return;
+ }
+ }
+
+ File binDir = null;
+ for (File file : files) {
+ if (file.isDirectory() && file.getName().equals("bin")) {
+ binDir = file;
+ break;
+ }
+ }
+
+ if (binDir == null) {
+ callback.call(null);
+ return;
+ }
+
+ File wineBin = new File(binDir, "wine");
+ if (!wineBin.isFile()) {
+ callback.call(null);
+ return;
+ }
+
+ final String arch = (new File(binDir, "wine64")).isFile() ? "x86_64" : "x86";
+
+ ImageFs imageFs = ImageFs.find(context);
+ File rootDir = ImageFs.find(context).getRootDir();
+ String wineBinRelPath = FileUtils.toRelativePath(rootDir.getPath(), wineBin.getPath());
+ final String winePath = wineDir.getPath();
+
+ try {
+ final AtomicReference wineInfoRef = new AtomicReference<>();
+ ProcessHelper.debugCallback = (line) -> {
+ Pattern pattern = Pattern.compile("^wine\\-([0-9\\.]+)\\-?([0-9\\.]+)?", Pattern.CASE_INSENSITIVE);
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.find()) {
+ String version = matcher.group(1);
+ String subversion = matcher.groupCount() >= 2 ? matcher.group(2) : null;
+ wineInfoRef.set(new WineInfo(version, subversion, arch, winePath));
+ }
+ };
+
+ File linkFile = new File(rootDir, ImageFs.HOME_PATH);
+ linkFile.delete();
+ FileUtils.symlink(wineDir, linkFile);
+
+ XEnvironment environment = new XEnvironment(context, imageFs);
+ GuestProgramLauncherComponent guestProgramLauncherComponent = new GuestProgramLauncherComponent();
+ guestProgramLauncherComponent.setGuestExecutable(wineBinRelPath+" --version");
+ guestProgramLauncherComponent.setTerminationCallback((status) -> callback.call(wineInfoRef.get()));
+ environment.addComponent(guestProgramLauncherComponent);
+ environment.startEnvironmentComponents();
+ }
+ finally {
+ ProcessHelper.debugCallback = null;
+ }
+ }
+
+ public static ArrayList getInstalledWineInfos(Context context) {
+ ArrayList wineInfos = new ArrayList<>();
+ wineInfos.add(WineInfo.MAIN_WINE_VERSION);
+ File installedWineDir = ImageFs.find(context).getInstalledWineDir();
+
+ File[] files = installedWineDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ String name = file.getName();
+ if (name.startsWith("wine")) wineInfos.add(WineInfo.fromIdentifier(context, name));
+ }
+ }
+
+ return wineInfos;
+ }
+
+ public static void applyRegistryKeyTweaks(Context context) {
+ File rootDir = ImageFs.find(context).getRootDir();
+ File systemRegFile = new File(rootDir, ImageFs.WINEPREFIX+"/system.reg");
+
+ try (WineRegistryEditor registryEditor = new WineRegistryEditor(systemRegFile)) {
+ registryEditor.setStringValue("Software\\Classes\\.reg", null, "REGfile");
+ registryEditor.setStringValue("Software\\Classes\\.reg", "Content Type", "application/reg");
+ registryEditor.setStringValue("Software\\Classes\\REGfile\\Shell\\Open\\command", null, "C:\\windows\\regedit.exe /C \"%1\"");
+ }
+ }
+
+ public static void overrideDXComponentDlls(Context context, Container container, String dxcomponents) {
+ final String dllOverridesKey = "Software\\Wine\\DllOverrides";
+ File userRegFile = new File(container.getRootDir(), ".wine/user.reg");
+ Iterator oldDXComponentsIter = Container.dxcomponentsIterator(container.getExtra("dxcomponents", dxcomponents)).iterator();
+
+ try (WineRegistryEditor registryEditor = new WineRegistryEditor(userRegFile)) {
+ JSONObject dxcomponentsJSONObject = new JSONObject(FileUtils.readString(context, "dxcomponents.json"));
+
+ for (String[] dxcomponent : Container.dxcomponentsIterator(dxcomponents)) {
+ if (dxcomponent[1].equals(oldDXComponentsIter.next()[1])) continue;
+ boolean useNative = dxcomponent[1].equals("1");
+
+ JSONArray libNames = dxcomponentsJSONObject.getJSONArray(dxcomponent[0]);
+ for (int i = 0; i < libNames.length(); i++) {
+ String libName = libNames.getString(i);
+ if (useNative) {
+ registryEditor.setStringValue(dllOverridesKey, libName, "native,builtin");
+ }
+ else registryEditor.removeValue(dllOverridesKey, libName);
+ }
+ }
+ }
+ catch (JSONException e) {}
+ }
+}
diff --git a/app/src/main/java/com/winlator/inputcontrols/Binding.java b/app/src/main/java/com/winlator/inputcontrols/Binding.java
new file mode 100644
index 0000000..73f0960
--- /dev/null
+++ b/app/src/main/java/com/winlator/inputcontrols/Binding.java
@@ -0,0 +1,127 @@
+package com.winlator.inputcontrols;
+
+import androidx.annotation.NonNull;
+
+import com.winlator.xserver.Pointer;
+import com.winlator.xserver.XKeycode;
+
+import java.util.ArrayList;
+
+public enum Binding {
+ NONE, MOUSE_LEFT_BUTTON, MOUSE_MIDDLE_BUTTON, MOUSE_RIGHT_BUTTON, MOUSE_MOVE_LEFT, MOUSE_MOVE_RIGHT, MOUSE_MOVE_UP, MOUSE_MOVE_DOWN, MOUSE_SCROLL_UP, MOUSE_SCROLL_DOWN, KEY_UP, KEY_RIGHT, KEY_DOWN, KEY_LEFT, KEY_ENTER, KEY_ESC, KEY_BKSP, KEY_DEL, KEY_TAB, KEY_SPACE, KEY_CTRL_L, KEY_CTRL_R, KEY_SHIFT_L, KEY_SHIFT_R, KEY_ALT_L, KEY_ALT_R, KEY_HOME, KEY_PRTSCN, KEY_CAPS_LOCK, KEY_NUM_LOCK, KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z, KEY_BRACKET_LEFT, KEY_BRACKET_RIGHT, KEY_BACKSLASH, KEY_SLASH, KEY_SEMICOLON, KEY_COMMA, KEY_PERIOD, KEY_APOSTROPHE, KEY_KP_ADD, KEY_MINUS, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, KEY_KP_0, KEY_KP_1, KEY_KP_2, KEY_KP_3, KEY_KP_4, KEY_KP_5, KEY_KP_6, KEY_KP_7, KEY_KP_8, KEY_KP_9;
+ public final XKeycode keycode;
+
+ Binding() {
+ XKeycode keycode;
+ try {
+ keycode = XKeycode.valueOf(name());
+ }
+ catch (IllegalArgumentException e) {
+ keycode = XKeycode.KEY_NONE;
+ }
+ this.keycode = keycode;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ switch (this) {
+ case KEY_SHIFT_L:
+ return "L SHIFT";
+ case KEY_SHIFT_R:
+ return "R SHIFT";
+ case KEY_CTRL_L:
+ return "L CTRL";
+ case KEY_CTRL_R:
+ return "R CTRL";
+ case KEY_ALT_L:
+ return "L ALT";
+ case KEY_ALT_R:
+ return "R ALT";
+ case KEY_BRACKET_LEFT:
+ return "[";
+ case KEY_BRACKET_RIGHT:
+ return "]";
+ case KEY_BACKSLASH:
+ return "\\";
+ case KEY_SLASH:
+ return "/";
+ case KEY_SEMICOLON:
+ return ";";
+ case KEY_COMMA:
+ return ",";
+ case KEY_PERIOD:
+ return ".";
+ case KEY_APOSTROPHE:
+ return "'";
+ case KEY_MINUS:
+ return "-";
+ case KEY_KP_ADD:
+ return "+";
+ default:
+ return super.toString().replaceAll("^(MOUSE_)|(KEY_)", "").replace("KP_", "NUMPAD_").replace("_", " ");
+ }
+ }
+
+ public static Binding fromString(String name) {
+ switch (name) {
+ case "KEY_CTRL":
+ return Binding.KEY_CTRL_L;
+ case "KEY_SHIFT":
+ return Binding.KEY_SHIFT_L;
+ case "KEY_ALT":
+ return Binding.KEY_ALT_L;
+ default:
+ return valueOf(name);
+ }
+ }
+
+ public Pointer.Button getPointerButton() {
+ switch (this) {
+ case MOUSE_LEFT_BUTTON:
+ return Pointer.Button.BUTTON_LEFT;
+ case MOUSE_MIDDLE_BUTTON:
+ return Pointer.Button.BUTTON_MIDDLE;
+ case MOUSE_RIGHT_BUTTON:
+ return Pointer.Button.BUTTON_RIGHT;
+ case MOUSE_SCROLL_UP:
+ return Pointer.Button.BUTTON_SCROLL_UP;
+ case MOUSE_SCROLL_DOWN:
+ return Pointer.Button.BUTTON_SCROLL_DOWN;
+ default:
+ return null;
+ }
+ }
+
+ public boolean isMouse() {
+ return name().startsWith("MOUSE_");
+ }
+
+ public boolean isKeyboard() {
+ return name().startsWith("KEY_") || this == NONE;
+ }
+
+ public static String[] mouseBindingLabels() {
+ ArrayList names = new ArrayList<>();
+ for (Binding binding : values()) if (binding.isMouse()) names.add(binding.toString());
+ return names.toArray(new String[0]);
+ }
+
+ public static String[] keyboardBindingLabels() {
+ ArrayList labels = new ArrayList<>();
+ for (Binding binding : values()) if (binding.isKeyboard()) labels.add(binding.toString());
+ return labels.toArray(new String[0]);
+ }
+
+ public static Binding[] mouseBindingValues() {
+ ArrayList labels = new ArrayList<>();
+ for (Binding binding : values()) if (binding.isMouse()) labels.add(binding);
+ return labels.toArray(new Binding[0]);
+ }
+
+ public static Binding[] keyboardBindingValues() {
+ ArrayList values = new ArrayList<>();
+ for (Binding binding : values()) if (binding.isKeyboard()) values.add(binding);
+ return values.toArray(new Binding[0]);
+ }
+}
diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java
new file mode 100644
index 0000000..9cf3ad0
--- /dev/null
+++ b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java
@@ -0,0 +1,666 @@
+package com.winlator.inputcontrols;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.Rect;
+
+import androidx.annotation.IntRange;
+import androidx.core.graphics.ColorUtils;
+
+import com.winlator.math.Mathf;
+import com.winlator.widget.InputControlsView;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class ControlElement {
+ public enum Type {
+ BUTTON, D_PAD, RANGE_BUTTON, STICK;
+
+ public static String[] names() {
+ Type[] types = values();
+ String[] names = new String[types.length];
+ for (int i = 0; i < types.length; i++) names[i] = types[i].name().replace("_", "-");
+ return names;
+ }
+ }
+ public enum Shape {
+ CIRCLE, RECT, ROUND_RECT, SQUARE;
+
+ public static String[] names() {
+ Shape[] shapes = values();
+ String[] names = new String[shapes.length];
+ for (int i = 0; i < shapes.length; i++) names[i] = shapes[i].name().replace("_", " ");
+ return names;
+ }
+ }
+ public enum Range {
+ FROM_A_TO_Z(26), FROM_0_TO_9(10), FROM_F1_TO_F12(12);
+ public final byte max;
+
+ Range(int max) {
+ this.max = (byte)max;
+ }
+
+ public static String[] names() {
+ Range[] ranges = values();
+ String[] names = new String[ranges.length];
+ for (int i = 0; i < ranges.length; i++) names[i] = ranges[i].name().replace("_", " ");
+ return names;
+ }
+ }
+ private final InputControlsView inputControlsView;
+ private Type type = Type.BUTTON;
+ private Shape shape = Shape.CIRCLE;
+ private final Binding[] bindings = {Binding.NONE, Binding.NONE, Binding.NONE, Binding.NONE};
+ private float scale = 1.0f;
+ private short x;
+ private short y;
+ private boolean selected = false;
+ private boolean toggleSwitch = false;
+ private int currentPointerId = -1;
+ private final Rect boundingBox = new Rect();
+ private final boolean[] states = new boolean[4];
+ private boolean boundingBoxNeedsUpdate = true;
+ private String text = "";
+ private byte iconId;
+ private Range range;
+ private byte currentPage;
+ private byte orientation;
+ private int currentElementIndex;
+ private PointF thumbstickPosition;
+
+ public ControlElement(InputControlsView inputControlsView) {
+ this.inputControlsView = inputControlsView;
+ }
+
+ private void reset() {
+ bindings[0] = bindings[1] = bindings[2] = bindings[3] = Binding.NONE;
+ if (type == Type.D_PAD || type == Type.STICK) {
+ bindings[0] = Binding.KEY_W;
+ bindings[1] = Binding.KEY_D;
+ bindings[2] = Binding.KEY_S;
+ bindings[3] = Binding.KEY_A;
+ }
+
+ text = "";
+ iconId = 0;
+ range = null;
+ boundingBoxNeedsUpdate = true;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public void setType(Type type) {
+ this.type = type;
+ reset();
+ }
+
+ public Shape getShape() {
+ return shape;
+ }
+
+ public void setShape(Shape shape) {
+ this.shape = shape;
+ boundingBoxNeedsUpdate = true;
+ }
+
+ public Range getRange() {
+ return range != null ? range : Range.FROM_A_TO_Z;
+ }
+
+ public void setRange(Range range) {
+ this.range = range;
+ }
+
+ public byte getOrientation() {
+ return orientation;
+ }
+
+ public void setOrientation(byte orientation) {
+ this.orientation = orientation;
+ boundingBoxNeedsUpdate = true;
+ }
+
+ public boolean isToggleSwitch() {
+ return toggleSwitch;
+ }
+
+ public void setToggleSwitch(boolean toggleSwitch) {
+ this.toggleSwitch = toggleSwitch;
+ }
+
+ public Binding getBindingAt(@IntRange(from = 0, to = 4) int index) {
+ return bindings[index];
+ }
+
+ public void setBindingAt(@IntRange(from = 0, to = 4) int index, Binding binding) {
+ this.bindings[index] = binding;
+ }
+
+ public float getScale() {
+ return scale;
+ }
+
+ public void setScale(float scale) {
+ this.scale = scale;
+ boundingBoxNeedsUpdate = true;
+ }
+
+ public short getX() {
+ return x;
+ }
+
+ public void setX(int x) {
+ this.x = (short)x;
+ boundingBoxNeedsUpdate = true;
+ }
+
+ public short getY() {
+ return y;
+ }
+
+ public void setY(int y) {
+ this.y = (short)y;
+ boundingBoxNeedsUpdate = true;
+ }
+
+ public boolean isSelected() {
+ return selected;
+ }
+
+ public void setSelected(boolean selected) {
+ this.selected = selected;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text != null ? text : "";
+ }
+
+ public byte getIconId() {
+ return iconId;
+ }
+
+ public void setIconId(int iconId) {
+ this.iconId = (byte)iconId;
+ }
+
+ public Rect getBoundingBox() {
+ if (boundingBoxNeedsUpdate) computeBoundingBox();
+ return boundingBox;
+ }
+
+ private Rect computeBoundingBox() {
+ int snappingSize = inputControlsView.getSnappingSize();
+ int halfWidth = 0;
+ int halfHeight = 0;
+
+ switch (type) {
+ case BUTTON:
+ switch (shape) {
+ case RECT:
+ case ROUND_RECT:
+ halfWidth = snappingSize * 4;
+ halfHeight = snappingSize * 2;
+ break;
+ case SQUARE:
+ halfWidth = (int)(snappingSize * 2.5f);
+ halfHeight = (int)(snappingSize * 2.5f);
+ break;
+ case CIRCLE:
+ halfWidth = snappingSize * 3;
+ halfHeight = snappingSize * 3;
+ break;
+ }
+ break;
+ case D_PAD:
+ halfWidth = snappingSize * 7;
+ halfHeight = snappingSize * 7;
+ break;
+ case STICK:
+ halfWidth = snappingSize * 6;
+ halfHeight = snappingSize * 6;
+ break;
+ case RANGE_BUTTON:
+ if (orientation == 0) {
+ halfWidth = snappingSize * 12;
+ halfHeight = (int)(snappingSize * 1.5f);
+ }
+ else {
+ halfWidth = (int)(snappingSize * 1.5f);
+ halfHeight = snappingSize * 12;
+ }
+ break;
+ }
+
+ halfWidth *= scale;
+ halfHeight *= scale;
+ boundingBox.set(x - halfWidth, y - halfHeight, x + halfWidth, y + halfHeight);
+ boundingBoxNeedsUpdate = false;
+ return boundingBox;
+ }
+
+ private String getDisplayText() {
+ if (text != null && !text.isEmpty()) {
+ return text;
+ }
+ else {
+ Binding binding = getBindingAt(0);
+ String text = binding.toString().replace("NUMPAD ", "NP");
+ if (text.length() > 7) {
+ String[] parts = text.split(" ");
+ StringBuilder sb = new StringBuilder();
+ for (String part : parts) sb.append(part.charAt(0));
+ return (binding.isMouse() ? "M" : "")+ sb;
+ }
+ else return text;
+ }
+ }
+
+ private static float getTextSizeForWidth(Paint paint, String text, float desiredWidth) {
+ final byte testTextSize = 48;
+ paint.setTextSize(testTextSize);
+ return testTextSize * desiredWidth / paint.measureText(text);
+ }
+
+ private static String getRangeTextForIndex(Range range, int index) {
+ String text = "";
+ switch (range) {
+ case FROM_A_TO_Z:
+ text = String.valueOf((char)(65 + index));
+ break;
+ case FROM_0_TO_9:
+ text = String.valueOf((index + 1) % 10);
+ break;
+ case FROM_F1_TO_F12:
+ text = "F"+(index + 1);
+ break;
+ }
+ return text;
+ }
+
+ public void draw(Canvas canvas) {
+ int snappingSize = inputControlsView.getSnappingSize();
+ Paint paint = inputControlsView.getPaint();
+ int primaryColor = inputControlsView.getPrimaryColor();
+
+ paint.setColor(selected ? inputControlsView.getSecondaryColor() : primaryColor);
+ paint.setStyle(Paint.Style.STROKE);
+ float strokeWidth = snappingSize * 0.25f;
+ paint.setStrokeWidth(strokeWidth);
+ Rect boundingBox = getBoundingBox();
+
+ switch (type) {
+ case BUTTON: {
+ float cx = boundingBox.centerX();
+ float cy = boundingBox.centerY();
+
+ switch (shape) {
+ case CIRCLE:
+ canvas.drawCircle(cx, cy, boundingBox.width() * 0.5f, paint);
+ break;
+ case RECT:
+ canvas.drawRect(boundingBox, paint);
+ break;
+ case ROUND_RECT: {
+ float radius = boundingBox.height() * 0.5f;
+ canvas.drawRoundRect(boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, radius, radius, paint);
+ break;
+ }
+ case SQUARE: {
+ float radius = snappingSize * 0.75f * scale;
+ canvas.drawRoundRect(boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, radius, radius, paint);
+ break;
+ }
+ }
+
+ if (iconId > 0) {
+ drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId);
+ } else {
+ String text = getDisplayText();
+ paint.setTextSize(Math.min(getTextSizeForWidth(paint, text, boundingBox.width() - strokeWidth * 2), snappingSize * 2 * scale));
+ paint.setTextAlign(Paint.Align.CENTER);
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(primaryColor);
+ canvas.drawText(text, x, (y - ((paint.descent() + paint.ascent()) * 0.5f)), paint);
+ }
+ break;
+ }
+ case D_PAD: {
+ float cx = boundingBox.centerX();
+ float cy = boundingBox.centerY();
+ float offsetX = snappingSize * 2 * scale;
+ float offsetY = snappingSize * 3 * scale;
+ float start = snappingSize * scale;
+ Path path = inputControlsView.getPath();
+ path.reset();
+
+ path.moveTo(cx, cy - start);
+ path.lineTo(cx - offsetX, cy - offsetY);
+ path.lineTo(cx - offsetX, boundingBox.top);
+ path.lineTo(cx + offsetX, boundingBox.top);
+ path.lineTo(cx + offsetX, cy - offsetY);
+ path.close();
+
+ path.moveTo(cx - start, cy);
+ path.lineTo(cx - offsetY, cy - offsetX);
+ path.lineTo(boundingBox.left, cy - offsetX);
+ path.lineTo(boundingBox.left, cy + offsetX);
+ path.lineTo(cx - offsetY, cy + offsetX);
+ path.close();
+
+ path.moveTo(cx, cy + start);
+ path.lineTo(cx - offsetX, cy + offsetY);
+ path.lineTo(cx - offsetX, boundingBox.bottom);
+ path.lineTo(cx + offsetX, boundingBox.bottom);
+ path.lineTo(cx + offsetX, cy + offsetY);
+ path.close();
+
+ path.moveTo(cx + start, cy);
+ path.lineTo(cx + offsetY, cy - offsetX);
+ path.lineTo(boundingBox.right, cy - offsetX);
+ path.lineTo(boundingBox.right, cy + offsetX);
+ path.lineTo(cx + offsetY, cy + offsetX);
+ path.close();
+
+ canvas.drawPath(path, paint);
+ break;
+ }
+ case RANGE_BUTTON: {
+ Range range = getRange();
+ int oldColor = paint.getColor();
+ float radius = snappingSize * 0.5f * scale;
+ float elementSize = (float)Math.max(boundingBox.width(), boundingBox.height()) / (bindings.length + 2);
+ float iconSize = Math.min(boundingBox.width(), boundingBox.height()) * 1.35f;
+ float minTextSize = snappingSize * 2 * scale;
+
+ if (orientation == 0) {
+ float lineTop = boundingBox.top + strokeWidth * 0.5f;
+ float lineBottom = boundingBox.bottom - strokeWidth * 0.5f;
+ float cy = boundingBox.top + boundingBox.height() * 0.5f;
+
+ float startX = boundingBox.left;
+ canvas.drawRoundRect(startX, boundingBox.top, boundingBox.right, boundingBox.bottom, radius, radius, paint);
+ drawIcon(canvas, startX + elementSize * 0.5f, cy, iconSize, iconSize, 8);
+ startX += elementSize;
+
+ for (byte i = 0; i < bindings.length; i++) {
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(oldColor);
+ canvas.drawLine(startX, lineTop, startX, lineBottom, paint);
+ int index = (i + currentPage * bindings.length) % range.max;
+ String text = getRangeTextForIndex(range, index);
+
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(primaryColor);
+ paint.setTextSize(Math.min(getTextSizeForWidth(paint, text, elementSize - strokeWidth * 2), minTextSize));
+ paint.setTextAlign(Paint.Align.CENTER);
+ canvas.drawText(text, startX + elementSize * 0.5f, (y - ((paint.descent() + paint.ascent()) * 0.5f)), paint);
+ startX += elementSize;
+ }
+
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(oldColor);
+ canvas.drawLine(startX, lineTop, startX, lineBottom, paint);
+ drawIcon(canvas, startX + elementSize * 0.5f, cy, iconSize, iconSize, 10);
+ }
+ else {
+ float lineLeft = boundingBox.left + strokeWidth * 0.5f;
+ float lineRight = boundingBox.right - strokeWidth * 0.5f;
+ float cx = boundingBox.left + boundingBox.width() * 0.5f;
+
+ float startY = boundingBox.top;
+ canvas.drawRoundRect(boundingBox.left, startY, boundingBox.right, boundingBox.bottom, radius, radius, paint);
+ drawIcon(canvas, cx, startY + elementSize * 0.5f, iconSize, iconSize, 9);
+ startY += elementSize;
+
+ for (byte i = 0; i < bindings.length; i++) {
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(oldColor);
+ canvas.drawLine(lineLeft, startY, lineRight, startY, paint);
+ int index = (i + currentPage * bindings.length) % range.max;
+ String text = getRangeTextForIndex(range, index);
+
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(primaryColor);
+ paint.setTextSize(Math.min(getTextSizeForWidth(paint, text, boundingBox.width() - strokeWidth * 2), minTextSize));
+ paint.setTextAlign(Paint.Align.CENTER);
+ canvas.drawText(text, x, startY + elementSize * 0.5f - ((paint.descent() + paint.ascent()) * 0.5f), paint);
+ startY += elementSize;
+ }
+
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(oldColor);
+ canvas.drawLine(lineLeft, startY, lineRight, startY, paint);
+ drawIcon(canvas, cx, startY + elementSize * 0.5f, iconSize, iconSize, 11);
+ }
+ break;
+ }
+ case STICK:
+ int cx = boundingBox.centerX();
+ int cy = boundingBox.centerY();
+ int oldColor = paint.getColor();
+ canvas.drawCircle(cx, cy, boundingBox.height() * 0.5f, paint);
+
+ float thumbstickX = thumbstickPosition != null ? thumbstickPosition.x : cx;
+ float thumbstickY = thumbstickPosition != null ? thumbstickPosition.y : cy;
+
+ short thumbRadius = (short)(snappingSize * 3.5f * scale);
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(ColorUtils.setAlphaComponent(primaryColor, 50));
+ canvas.drawCircle(thumbstickX, thumbstickY, thumbRadius, paint);
+
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(oldColor);
+ canvas.drawCircle(thumbstickX, thumbstickY, thumbRadius + strokeWidth * 0.5f, paint);
+ break;
+ }
+ }
+
+ private void drawIcon(Canvas canvas, float cx, float cy, float width, float height, int iconId) {
+ Paint paint = inputControlsView.getPaint();
+ Bitmap icon = inputControlsView.getIcon((byte)iconId);
+ paint.setColorFilter(inputControlsView.getColorFilter());
+ int halfSize = (int)(Math.min(width, height) * 0.5f - inputControlsView.getSnappingSize());
+
+ Rect srcRect = new Rect(0, 0, icon.getWidth(), icon.getHeight());
+ Rect dstRect = new Rect((int)(cx - halfSize), (int)(cy - halfSize), (int)(cx + halfSize), (int)(cy + halfSize));
+ canvas.drawBitmap(icon, srcRect, dstRect, paint);
+ paint.setColorFilter(null);
+ }
+
+ public JSONObject toJSONObject() {
+ try {
+ JSONObject elementJSONObject = new JSONObject();
+ elementJSONObject.put("type", type.name());
+ elementJSONObject.put("shape", shape.name());
+
+ JSONArray bindingsJSONArray = new JSONArray();
+ for (Binding binding : bindings) bindingsJSONArray.put(binding.name());
+
+ elementJSONObject.put("bindings", bindingsJSONArray);
+ elementJSONObject.put("scale", Float.valueOf(scale));
+ elementJSONObject.put("x", (float)x / inputControlsView.getMaxWidth());
+ elementJSONObject.put("y", (float)y / inputControlsView.getMaxHeight());
+ elementJSONObject.put("toggleSwitch", toggleSwitch);
+ elementJSONObject.put("text", text);
+ elementJSONObject.put("iconId", iconId);
+
+ if (type == Type.RANGE_BUTTON && range != null) {
+ elementJSONObject.put("range", range.name());
+ if (orientation != 0) elementJSONObject.put("orientation", orientation);
+ }
+ return elementJSONObject;
+ }
+ catch (JSONException e) {
+ return null;
+ }
+ }
+
+ public boolean containsPoint(float x, float y) {
+ return getBoundingBox().contains((int)(x + 0.5f), (int)(y + 0.5f));
+ }
+
+ public boolean handleTouchDown(int pointerId, float x, float y) {
+ if (currentPointerId == -1 && containsPoint(x, y)) {
+ currentPointerId = pointerId;
+ if (type == ControlElement.Type.BUTTON) {
+ if (!toggleSwitch || !selected) inputControlsView.handleInputEvent(getBindingAt(0), true);
+ return true;
+ }
+ else if (type == Type.RANGE_BUTTON) {
+ Range range = getRange();
+ currentElementIndex = getElementIndexAt(x, y);
+ if (currentElementIndex > 0 && currentElementIndex < bindings.length + 1) {
+ int index = ((currentElementIndex - 1) + currentPage * bindings.length) % range.max;
+ Binding binding = Binding.NONE;
+ switch (range) {
+ case FROM_A_TO_Z:
+ binding = Binding.valueOf("KEY_"+((char)(65 + index)));
+ break;
+ case FROM_0_TO_9:
+ binding = Binding.valueOf("KEY_"+((index + 1) % 10));
+ break;
+ case FROM_F1_TO_F12:
+ binding = Binding.valueOf("KEY_F"+(index + 1));
+ break;
+ }
+
+ bindings[0] = bindings[1] = bindings[2] = bindings[3] = Binding.NONE;
+ bindings[currentElementIndex-1] = binding;
+ states[currentElementIndex-1] = true;
+ inputControlsView.handleInputEvent(binding, true);
+ }
+ return true;
+ }
+ else return handleTouchMove(pointerId, x, y);
+ }
+ else return false;
+ }
+
+ public boolean handleTouchMove(int pointerId, float x, float y) {
+ if (pointerId == currentPointerId && (type == ControlElement.Type.D_PAD || type == Type.STICK)) {
+ Rect boundingBox = getBoundingBox();
+ x -= boundingBox.left;
+ y -= boundingBox.top;
+
+ float radius = boundingBox.width() * 0.5f;
+ float distance = Mathf.lengthSq(radius - x, radius - y);
+ if (distance > radius * radius) {
+ float angle = (float)Math.atan2(y - radius, x - radius);
+ x = (float)((Math.cos(angle) * radius) + radius);
+ y = (float)((Math.sin(angle) * radius) + radius);
+ }
+
+ float invWidth = 1.0f / boundingBox.width();
+ float deltaX = Mathf.clamp(-1 + 2 * ((radius - x) + radius) * invWidth, -1, 1) * -1;
+ float deltaY = Mathf.clamp(-1 + 2 * ((radius - y) + radius) * invWidth, -1, 1) * -1;
+
+ if (type == Type.STICK) {
+ if (thumbstickPosition == null) thumbstickPosition = new PointF();
+ thumbstickPosition.x = boundingBox.left + deltaX * radius + radius;
+ thumbstickPosition.y = boundingBox.top + deltaY * radius + radius;
+
+ boolean[] states = {
+ deltaY <= -InputControlsView.JOYSTICK_DEAD_ZONE,
+ deltaX >= InputControlsView.JOYSTICK_DEAD_ZONE,
+ deltaY >= InputControlsView.JOYSTICK_DEAD_ZONE,
+ deltaX <= -InputControlsView.JOYSTICK_DEAD_ZONE
+ };
+
+ for (byte i = 0; i < 4; i++) {
+ float value = i == 1 || i == 3 ? deltaX : deltaY;
+ if (states[i]) {
+ inputControlsView.handleInputEvent(getBindingAt(i), true, value);
+ this.states[i] = true;
+ }
+ else {
+ inputControlsView.handleInputEvent(getBindingAt(i), false, value);
+ this.states[i] = false;
+ }
+ }
+
+ inputControlsView.invalidate();
+ }
+ else {
+ boolean[] states = {
+ deltaY <= -InputControlsView.DPAD_DEAD_ZONE,
+ deltaX >= InputControlsView.DPAD_DEAD_ZONE,
+ deltaY >= InputControlsView.DPAD_DEAD_ZONE,
+ deltaX <= -InputControlsView.DPAD_DEAD_ZONE
+ };
+
+ for (byte i = 0; i < 4; i++) {
+ if (states[i] && !this.states[i]) {
+ inputControlsView.handleInputEvent(getBindingAt(i), true);
+ this.states[i] = true;
+ }
+ else if (!states[i] && this.states[i]) {
+ inputControlsView.handleInputEvent(getBindingAt(i), false);
+ this.states[i] = false;
+ }
+ }
+ }
+
+ return true;
+ }
+ else return false;
+ }
+
+ public boolean handleTouchUp(int pointerId) {
+ if (pointerId == currentPointerId) {
+ if (type == Type.BUTTON) {
+ if (!toggleSwitch || selected) inputControlsView.handleInputEvent(getBindingAt(0), false);
+ if (toggleSwitch) {
+ selected = !selected;
+ inputControlsView.invalidate();
+ }
+ }
+ else if (type == Type.RANGE_BUTTON || type == Type.D_PAD || type == Type.STICK) {
+ for (byte i = 0; i < 4; i++) {
+ if (states[i]) inputControlsView.handleInputEvent(getBindingAt(i), false);
+ states[i] = false;
+ }
+
+ if (type == Type.RANGE_BUTTON) {
+ Range range = getRange();
+ float invLength = 1.0f / bindings.length;
+ byte totalPage = (byte)((Math.ceil(range.max * invLength) * bindings.length) * invLength);
+ if (currentElementIndex == 0) {
+ currentPage--;
+ if (currentPage < 0) currentPage = (byte)(totalPage - 1);
+ inputControlsView.invalidate();
+ }
+ else if (currentElementIndex == (bindings.length+1)) {
+ currentPage = (byte)((currentPage + 1) % totalPage);
+ inputControlsView.invalidate();
+ }
+ }
+ else if (type == Type.STICK) {
+ if (thumbstickPosition != null) thumbstickPosition = null;
+ inputControlsView.invalidate();
+ }
+ }
+ currentPointerId = -1;
+ return true;
+ }
+ return false;
+ }
+
+ private byte getElementIndexAt(float x, float y) {
+ Rect boundingBox = getBoundingBox();
+ float elementSize = (float)Math.max(boundingBox.width(), boundingBox.height()) / (bindings.length + 2);
+ float offset = orientation == 0 ? x - boundingBox.left : y - boundingBox.top;
+ return (byte)Mathf.clamp((int)Math.floor(offset / elementSize), 0, bindings.length + 1);
+ }
+}
diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java b/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java
new file mode 100644
index 0000000..b450fd6
--- /dev/null
+++ b/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java
@@ -0,0 +1,219 @@
+package com.winlator.inputcontrols;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.winlator.core.FileUtils;
+import com.winlator.widget.InputControlsView;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+public class ControlsProfile implements Comparable {
+ public final int id;
+ private String name;
+ private float cursorSpeed = 1.0f;
+ private final ArrayList elements = new ArrayList<>();
+ private final ArrayList controllers = new ArrayList<>();
+ private final List immutableElements = Collections.unmodifiableList(elements);
+ private boolean elementsLoaded = false;
+ private boolean controllersLoaded = false;
+ private final Context context;
+
+ public ControlsProfile(Context context, int id) {
+ this.context = context;
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public float getCursorSpeed() {
+ return cursorSpeed;
+ }
+
+ public void setCursorSpeed(float cursorSpeed) {
+ this.cursorSpeed = cursorSpeed;
+ }
+
+ public ExternalController addController(String id) {
+ ExternalController controller = getController(id);
+ if (controller == null) controllers.add(controller = ExternalController.getController(id));
+ return controller;
+ }
+
+ public void removeController(ExternalController controller) {
+ if (!controllersLoaded) loadControllers();
+ controllers.remove(controller);
+ }
+
+ public ExternalController getController(String id) {
+ if (!controllersLoaded) loadControllers();
+ for (ExternalController controller : controllers) if (controller.getId().equals(id)) return controller;
+ return null;
+ }
+
+ public ExternalController getController(int deviceId) {
+ if (!controllersLoaded) loadControllers();
+ for (ExternalController controller : controllers) if (controller.getDeviceId() == deviceId) return controller;
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public int compareTo(ControlsProfile o) {
+ return Integer.compare(id, o.id);
+ }
+
+ public boolean isElementsLoaded() {
+ return elementsLoaded;
+ }
+
+ public void save() {
+ File file = getProfileFile(context, id);
+
+ try {
+ JSONObject data = new JSONObject();
+ data.put("id", id);
+ data.put("name", name);
+ data.put("cursorSpeed", Float.valueOf(cursorSpeed));
+
+ JSONArray elementsJSONArray = new JSONArray();
+ if (!elementsLoaded && file.isFile()) {
+ JSONObject profileJSONObject = new JSONObject(FileUtils.readString(file));
+ elementsJSONArray = profileJSONObject.getJSONArray("elements");
+ }
+ else for (ControlElement element : elements) elementsJSONArray.put(element.toJSONObject());
+ data.put("elements", elementsJSONArray);
+
+ JSONArray controllersJSONArray = new JSONArray();
+ if (!controllersLoaded && file.isFile()) {
+ JSONObject profileJSONObject = new JSONObject(FileUtils.readString(file));
+ if (profileJSONObject.has("controllers")) controllersJSONArray = profileJSONObject.getJSONArray("controllers");
+ }
+ else {
+ for (ExternalController controller : controllers) {
+ JSONObject controllerJSONObject = controller.toJSONObject();
+ if (controllerJSONObject != null) controllersJSONArray.put(controllerJSONObject);
+ }
+ }
+ if (controllersJSONArray.length() > 0) data.put("controllers", controllersJSONArray);
+
+ FileUtils.writeString(file, data.toString());
+ }
+ catch (JSONException e) {}
+ }
+
+ public static File getProfileFile(Context context, int id) {
+ return new File(InputControlsManager.getProfilesDir(context), "controls-"+id+".icp");
+ }
+
+ public void addElement(ControlElement element) {
+ elements.add(element);
+ elementsLoaded = true;
+ }
+
+ public void removeElement(ControlElement element) {
+ elements.remove(element);
+ elementsLoaded = true;
+ }
+
+ public List getElements() {
+ return immutableElements;
+ }
+
+ public boolean isTemplate() {
+ return name.toLowerCase(Locale.ENGLISH).contains("template");
+ }
+
+ public ArrayList loadControllers() {
+ controllers.clear();
+ controllersLoaded = false;
+
+ File file = getProfileFile(context, id);
+ if (!file.isFile()) return controllers;
+
+ try {
+ JSONObject profileJSONObject = new JSONObject(FileUtils.readString(file));
+ if (!profileJSONObject.has("controllers")) return controllers;
+ JSONArray controllersJSONArray = profileJSONObject.getJSONArray("controllers");
+ for (int i = 0; i < controllersJSONArray.length(); i++) {
+ JSONObject controllerJSONObject = controllersJSONArray.getJSONObject(i);
+ String id = controllerJSONObject.getString("id");
+ ExternalController controller = new ExternalController();
+ controller.setId(id);
+ controller.setName(controllerJSONObject.getString("name"));
+
+ JSONArray controllerBindingsJSONArray = controllerJSONObject.getJSONArray("controllerBindings");
+ for (int j = 0; j < controllerBindingsJSONArray.length(); j++) {
+ JSONObject controllerBindingJSONObject = controllerBindingsJSONArray.getJSONObject(j);
+ ExternalControllerBinding controllerBinding = new ExternalControllerBinding();
+ controllerBinding.setKeyCode(controllerBindingJSONObject.getInt("keyCode"));
+ controllerBinding.setBinding(Binding.fromString(controllerBindingJSONObject.getString("binding")));
+ controller.addControllerBinding(controllerBinding);
+ }
+ controllers.add(controller);
+ }
+ controllersLoaded = true;
+ }
+ catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return controllers;
+ }
+
+ public void loadElements(InputControlsView inputControlsView) {
+ elements.clear();
+ elementsLoaded = false;
+
+ File file = getProfileFile(inputControlsView.getContext(), id);
+ if (!file.isFile()) return;
+
+ try {
+ JSONObject profileJSONObject = new JSONObject(FileUtils.readString(file));
+ JSONArray elementsJSONArray = profileJSONObject.getJSONArray("elements");
+ for (int i = 0; i < elementsJSONArray.length(); i++) {
+ JSONObject elementJSONObject = elementsJSONArray.getJSONObject(i);
+ ControlElement element = new ControlElement(inputControlsView);
+ element.setType(ControlElement.Type.valueOf(elementJSONObject.getString("type")));
+ element.setShape(ControlElement.Shape.valueOf(elementJSONObject.getString("shape")));
+ element.setToggleSwitch(elementJSONObject.getBoolean("toggleSwitch"));
+ element.setX((int)(elementJSONObject.getDouble("x") * inputControlsView.getMaxWidth()));
+ element.setY((int)(elementJSONObject.getDouble("y") * inputControlsView.getMaxHeight()));
+ element.setScale((float)elementJSONObject.getDouble("scale"));
+ element.setText(elementJSONObject.getString("text"));
+ element.setIconId(elementJSONObject.getInt("iconId"));
+ if (elementJSONObject.has("range")) element.setRange(ControlElement.Range.valueOf(elementJSONObject.getString("range")));
+ if (elementJSONObject.has("orientation")) element.setOrientation((byte)elementJSONObject.getInt("orientation"));
+
+ JSONArray bindingsJSONArray = elementJSONObject.getJSONArray("bindings");
+ for (int j = 0; j < bindingsJSONArray.length(); j++) {
+ element.setBindingAt(j, Binding.fromString(bindingsJSONArray.getString(j)));
+ }
+ elements.add(element);
+ }
+ elementsLoaded = true;
+ }
+ catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/inputcontrols/ExternalController.java b/app/src/main/java/com/winlator/inputcontrols/ExternalController.java
new file mode 100644
index 0000000..51c465d
--- /dev/null
+++ b/app/src/main/java/com/winlator/inputcontrols/ExternalController.java
@@ -0,0 +1,150 @@
+package com.winlator.inputcontrols;
+
+import android.view.InputDevice;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class ExternalController {
+ private String name;
+ private String id;
+ private int deviceId = -1;
+ private final ArrayList controllerBindings = new ArrayList<>();
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public int getDeviceId() {
+ if (this.deviceId == -1) {
+ for (int deviceId : InputDevice.getDeviceIds()) {
+ InputDevice device = InputDevice.getDevice(deviceId);
+ if (device != null && device.getDescriptor().equals(id)) {
+ this.deviceId = deviceId;
+ break;
+ }
+ }
+ }
+ return this.deviceId;
+ }
+
+ public boolean isConnected() {
+ for (int deviceId : InputDevice.getDeviceIds()) {
+ InputDevice device = InputDevice.getDevice(deviceId);
+ if (device != null && device.getDescriptor().equals(id)) return true;
+ }
+ return false;
+ }
+
+ public ExternalControllerBinding getControllerBinding(int keyCode) {
+ for (ExternalControllerBinding controllerBinding : controllerBindings) {
+ if (controllerBinding.getKeyCodeForAxis() == keyCode) return controllerBinding;
+ }
+ return null;
+ }
+
+ public ExternalControllerBinding getControllerBindingAt(int index) {
+ return controllerBindings.get(index);
+ }
+
+ public void addControllerBinding(ExternalControllerBinding controllerBinding) {
+ if (getControllerBinding(controllerBinding.getKeyCodeForAxis()) == null) controllerBindings.add(controllerBinding);
+ }
+
+ public int getPosition(ExternalControllerBinding controllerBinding) {
+ return controllerBindings.indexOf(controllerBinding);
+ }
+
+ public void removeControllerBinding(ExternalControllerBinding controllerBinding) {
+ controllerBindings.remove(controllerBinding);
+ }
+
+ public int getControllerBindingCount() {
+ return controllerBindings.size();
+ }
+
+ public JSONObject toJSONObject() {
+ try {
+ if (controllerBindings.isEmpty()) return null;
+ JSONObject controllerJSONObject = new JSONObject();
+ controllerJSONObject.put("id", id);
+ controllerJSONObject.put("name", name);
+
+ JSONArray controllerBindingsJSONArray = new JSONArray();
+ for (ExternalControllerBinding controllerBinding : controllerBindings) controllerBindingsJSONArray.put(controllerBinding.toJSONObject());
+ controllerJSONObject.put("controllerBindings", controllerBindingsJSONArray);
+
+ return controllerJSONObject;
+ }
+ catch (JSONException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ return obj instanceof ExternalController ? ((ExternalController)obj).id.equals(this.id) : super.equals(obj);
+ }
+
+ public static ArrayList getControllers() {
+ ArrayList controllers = new ArrayList<>();
+ for (int deviceId : InputDevice.getDeviceIds()) {
+ InputDevice device = InputDevice.getDevice(deviceId);
+ if (device != null && !device.isVirtual() && isGameController(device)) {
+ ExternalController controller = new ExternalController();
+ controller.setId(device.getDescriptor());
+ controller.setName(device.getName());
+ controllers.add(controller);
+ }
+ }
+ return controllers;
+ }
+
+ public static ExternalController getController(String id) {
+ for (ExternalController controller : getControllers()) if (controller.getId().equals(id)) return controller;
+ return null;
+ }
+
+ public static boolean isGameController(InputDevice device) {
+ int sources = device.getSources();
+ return (sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
+ (sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK;
+ }
+
+ public static float getCenteredAxis(MotionEvent event, int axis, int historyPos) {
+ InputDevice device = event.getDevice();
+ InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource());
+ if (range != null) {
+ float flat = range.getFlat();
+ float value = historyPos < 0 ? event.getAxisValue(axis) : event.getHistoricalAxisValue(axis, historyPos);
+ if (Math.abs(value) > flat) return value;
+ }
+ return 0;
+ }
+
+ public static boolean isJoystickDevice(MotionEvent event) {
+ return (event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.getAction() == MotionEvent.ACTION_MOVE;
+ }
+
+ public static boolean isDPadDevice(MotionEvent event) {
+ return (event.getSource() & InputDevice.SOURCE_DPAD) != InputDevice.SOURCE_DPAD && event.getAction() == MotionEvent.ACTION_MOVE;
+ }
+}
diff --git a/app/src/main/java/com/winlator/inputcontrols/ExternalControllerBinding.java b/app/src/main/java/com/winlator/inputcontrols/ExternalControllerBinding.java
new file mode 100644
index 0000000..d111ef3
--- /dev/null
+++ b/app/src/main/java/com/winlator/inputcontrols/ExternalControllerBinding.java
@@ -0,0 +1,100 @@
+package com.winlator.inputcontrols;
+
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class ExternalControllerBinding {
+ public static final byte AXIS_X_NEGATIVE = -1;
+ public static final byte AXIS_X_POSITIVE = -2;
+ public static final byte AXIS_Y_NEGATIVE = -3;
+ public static final byte AXIS_Y_POSITIVE = -4;
+ public static final byte AXIS_Z_NEGATIVE = -5;
+ public static final byte AXIS_Z_POSITIVE = -6;
+ public static final byte AXIS_RZ_NEGATIVE = -7;
+ public static final byte AXIS_RZ_POSITIVE = -8;
+ public static final byte AXIS_LTRIGGER = -9;
+ public static final byte AXIS_RTRIGGER = -10;
+ private short keyCode;
+ private Binding binding = Binding.NONE;
+
+ public int getKeyCodeForAxis() {
+ return keyCode;
+ }
+
+ public void setKeyCode(int keyCode) {
+ this.keyCode = (short)keyCode;
+ }
+
+ public Binding getBinding() {
+ return binding;
+ }
+
+ public void setBinding(Binding binding) {
+ this.binding = binding;
+ }
+
+ public JSONObject toJSONObject() {
+ try {
+ JSONObject controllerBindingJSONObject = new JSONObject();
+ controllerBindingJSONObject.put("keyCode", keyCode);
+ controllerBindingJSONObject.put("binding", binding.name());
+ return controllerBindingJSONObject;
+ }
+ catch (JSONException e) {
+ return null;
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ switch (keyCode) {
+ case AXIS_X_NEGATIVE:
+ return "AXIS X-";
+ case AXIS_X_POSITIVE:
+ return "AXIS X+";
+ case AXIS_Y_NEGATIVE:
+ return "AXIS Y-";
+ case AXIS_Y_POSITIVE:
+ return "AXIS Y+";
+ case AXIS_Z_NEGATIVE:
+ return "AXIS Z-";
+ case AXIS_Z_POSITIVE:
+ return "AXIS Z+";
+ case AXIS_RZ_NEGATIVE:
+ return "AXIS RZ-";
+ case AXIS_RZ_POSITIVE:
+ return "AXIS RZ+";
+ case AXIS_LTRIGGER:
+ return "L TRIGGER";
+ case AXIS_RTRIGGER:
+ return "R TRIGGER";
+ default:
+ return KeyEvent.keyCodeToString(keyCode).replace("KEYCODE_", "").replace("_", " ");
+ }
+ }
+
+ public static int getKeyCodeForAxis(int axis, byte sign) {
+ switch (axis) {
+ case MotionEvent.AXIS_X:
+ return sign > 0 ? AXIS_X_POSITIVE : AXIS_X_NEGATIVE;
+ case MotionEvent.AXIS_Y:
+ return sign > 0 ? AXIS_Y_NEGATIVE : AXIS_Y_POSITIVE;
+ case MotionEvent.AXIS_Z:
+ return sign > 0 ? AXIS_Z_POSITIVE : AXIS_Z_NEGATIVE;
+ case MotionEvent.AXIS_RZ:
+ return sign > 0 ? AXIS_RZ_NEGATIVE : AXIS_RZ_POSITIVE;
+ case MotionEvent.AXIS_HAT_X:
+ return sign > 0 ? KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT;
+ case MotionEvent.AXIS_HAT_Y:
+ return sign > 0 ? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP;
+ default:
+ return KeyEvent.KEYCODE_UNKNOWN;
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/inputcontrols/InputControlsManager.java b/app/src/main/java/com/winlator/inputcontrols/InputControlsManager.java
new file mode 100644
index 0000000..6ac21af
--- /dev/null
+++ b/app/src/main/java/com/winlator/inputcontrols/InputControlsManager.java
@@ -0,0 +1,201 @@
+package com.winlator.inputcontrols;
+
+import android.content.Context;
+import android.media.MediaScannerConnection;
+import android.os.Environment;
+import android.util.JsonReader;
+
+import com.winlator.XServerDisplayActivity;
+import com.winlator.core.FileUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+
+public class InputControlsManager {
+ private final Context context;
+ private ArrayList profiles;
+ private int maxProfileId;
+ private boolean profilesLoaded = false;
+
+ public InputControlsManager(Context context) {
+ this.context = context;
+ }
+
+ public static File getProfilesDir(Context context) {
+ File profilesDir = new File(context.getFilesDir(), "profiles");
+ if (!profilesDir.isDirectory()) profilesDir.mkdir();
+ return profilesDir;
+ }
+
+ public ArrayList getProfiles() {
+ return getProfiles(false);
+ }
+
+ public ArrayList getProfiles(boolean ignoreTemplates) {
+ if (!profilesLoaded) loadProfiles();
+ return profiles;
+ }
+
+ public void loadProfiles() {
+ File profilesDir = InputControlsManager.getProfilesDir(context);
+ if (FileUtils.isEmpty(profilesDir)) FileUtils.copy(context, "inputcontrols/profiles", profilesDir);
+
+ ArrayList profiles = new ArrayList<>();
+ File[] files = profilesDir.listFiles();
+ if (files != null) {
+ boolean ignoreTemplates = context instanceof XServerDisplayActivity;
+ for (File file : files) {
+ ControlsProfile profile = loadProfile(context, file);
+ if (!(ignoreTemplates && profile.isTemplate())) profiles.add(profile);
+ maxProfileId = Math.max(maxProfileId, profile.id);
+ }
+ }
+
+ Collections.sort(profiles);
+ this.profiles = profiles;
+ profilesLoaded = true;
+ }
+
+ public ControlsProfile createProfile(String name) {
+ ControlsProfile profile = new ControlsProfile(context, ++maxProfileId);
+ profile.setName(name);
+ profile.save();
+ profiles.add(profile);
+ return profile;
+ }
+
+ public ControlsProfile duplicateProfile(ControlsProfile source) {
+ String newName;
+ for (int i = 1;;i++) {
+ newName = source.getName() + " ("+i+")";
+ boolean found = false;
+ for (ControlsProfile profile : profiles) {
+ if (profile.getName().equals(newName)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) break;
+ }
+
+ int newId = ++maxProfileId;
+ File newFile = ControlsProfile.getProfileFile(context, newId);
+
+ try {
+ JSONObject data = new JSONObject(FileUtils.readString(ControlsProfile.getProfileFile(context, source.id)));
+ data.put("id", newId);
+ data.put("name", newName);
+ if (data.has("template")) data.remove("template");
+ FileUtils.writeString(newFile, data.toString());
+ }
+ catch (JSONException e) {}
+
+ ControlsProfile profile = loadProfile(context, newFile);
+ profiles.add(profile);
+ return profile;
+ }
+
+ public void removeProfile(ControlsProfile profile) {
+ File file = ControlsProfile.getProfileFile(context, profile.id);
+ if (file.isFile() && file.delete()) profiles.remove(profile);
+ }
+
+ public ControlsProfile importProfile(JSONObject data) {
+ try {
+ if (!data.has("id") || !data.has("name")) return null;
+ int newId = ++maxProfileId;
+ File newFile = ControlsProfile.getProfileFile(context, newId);
+ data.put("id", newId);
+ FileUtils.writeString(newFile, data.toString());
+ ControlsProfile newProfile = loadProfile(context, newFile);
+
+ int foundIndex = -1;
+ for (int i = 0; i < profiles.size(); i++) {
+ ControlsProfile profile = profiles.get(i);
+ if (profile.getName().equals(newProfile.getName())) {
+ foundIndex = i;
+ break;
+ }
+ }
+
+ if (foundIndex != -1) {
+ profiles.set(foundIndex, newProfile);
+ }
+ else profiles.add(newProfile);
+ return newProfile;
+ }
+ catch (JSONException e) {
+ return null;
+ }
+ }
+
+ public File exportProfile(ControlsProfile profile) {
+ File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ File destination = new File(downloadsDir, "Winlator/profiles/"+profile.getName()+".icp");
+ FileUtils.copy(ControlsProfile.getProfileFile(context, profile.id), destination);
+ MediaScannerConnection.scanFile(context, new String[]{destination.getAbsolutePath()}, null, null);
+ return destination.isFile() ? destination : null;
+ }
+
+ public static ControlsProfile loadProfile(Context context, File file) {
+ JsonReader reader = null;
+ try {
+ FileInputStream fis = new FileInputStream(file);
+ reader = new JsonReader(new InputStreamReader(fis, StandardCharsets.UTF_8));
+
+ int profileId = 0;
+ String profileName = null;
+ float cursorSpeed = Float.NaN;
+ int fieldsRead = 0;
+
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+
+ if (name.equals("id")) {
+ profileId = reader.nextInt();
+ fieldsRead++;
+ }
+ else if (name.equals("name")) {
+ profileName = reader.nextString();
+ fieldsRead++;
+ }
+ else if (name.equals("cursorSpeed")) {
+ cursorSpeed = (float)reader.nextDouble();
+ fieldsRead++;
+ }
+ else {
+ if (fieldsRead == 3) break;
+ reader.skipValue();
+ }
+ }
+
+ ControlsProfile profile = new ControlsProfile(context, profileId);
+ profile.setName(profileName);
+ profile.setCursorSpeed(cursorSpeed);
+ return profile;
+ }
+ catch (IOException e) {
+ return null;
+ }
+ finally {
+ try {
+ if (reader != null) reader.close();
+ }
+ catch (IOException e) {}
+ }
+ }
+
+ public ControlsProfile getProfile(int id) {
+ for (ControlsProfile profile : getProfiles()) if (profile.id == id) return profile;
+ return null;
+ }
+}
diff --git a/app/src/main/java/com/winlator/math/Mathf.java b/app/src/main/java/com/winlator/math/Mathf.java
new file mode 100644
index 0000000..9668f94
--- /dev/null
+++ b/app/src/main/java/com/winlator/math/Mathf.java
@@ -0,0 +1,23 @@
+package com.winlator.math;
+
+public abstract class Mathf {
+ public static float clamp(float x, float min, float max) {
+ return (x < min) ? min : ((x > max) ? max : x);
+ }
+
+ public static int clamp(int x, int min, int max) {
+ return (x < min) ? min : (x > max ? max : x);
+ }
+
+ public static float roundTo(float x, float step) {
+ return (float)(Math.floor(x / step) * step);
+ }
+
+ public static byte sign(float x) {
+ return (byte)(x < 0 ? -1 : (x > 0 ? 1 : 0));
+ }
+
+ public static float lengthSq(float x, float y) {
+ return x * x + y * y;
+ }
+}
diff --git a/app/src/main/java/com/winlator/math/XForm.java b/app/src/main/java/com/winlator/math/XForm.java
new file mode 100644
index 0000000..e6f7f6e
--- /dev/null
+++ b/app/src/main/java/com/winlator/math/XForm.java
@@ -0,0 +1,96 @@
+package com.winlator.math;
+
+public class XForm {
+ private static final float[] tmpXForm = XForm.getInstance();
+
+ public static float[] getInstance() {
+ return identity(new float[6]);
+ }
+
+ public static float[] identity(float[] xform) {
+ return set(xform,
+ 1, 0,
+ 0, 1,
+ 0, 0
+ );
+ }
+
+ public static float[] set(float[] xform, float n11, float n12, float n21, float n22, float dx, float dy) {
+ xform[0] = n11; xform[1] = n12;
+ xform[2] = n21; xform[3] = n22;
+ xform[4] = dx; xform[5] = dy;
+ return xform;
+ }
+
+ public static float[] set(float[] xform, float tx, float ty, float sx, float sy) {
+ xform[0] = sx; xform[1] = 0;
+ xform[2] = 0; xform[3] = sy;
+ xform[4] = tx; xform[5] = ty;
+ return xform;
+ }
+
+ public static float[] makeTransform(float[] xform, float tx, float ty, float sx, float sy, float angle) {
+ float c = (float)Math.cos(angle);
+ float s = (float)Math.sin(angle);
+ return set(xform,
+ sx * c, sy * s,
+ sx * -s, sy * c,
+ tx, ty
+ );
+ }
+
+ public static float[] makeTranslation(float[] xform, float x, float y) {
+ return set(xform,
+ 1, 0,
+ 0, 1,
+ x, y
+ );
+ }
+
+ public static float[] makeScale(float[] xform, float x, float y) {
+ return set(xform,
+ x, 0,
+ 0, y,
+ 0, 0
+ );
+ }
+
+ public static float[] makeRotation(float[] xform, float angle) {
+ float c = (float)Math.cos(angle);
+ float s = (float)Math.sin(angle);
+
+ return set(xform,
+ c, s,
+ -s, c,
+ 0, 0
+ );
+ }
+
+ public static synchronized float[] translate(float[] xform, float x, float y) {
+ return multiply(xform, xform, makeTranslation(tmpXForm, x, y));
+ }
+
+ public static synchronized float[] scale(float[] xform, float x, float y) {
+ return multiply(xform, xform, makeScale(tmpXForm, x, y));
+ }
+
+ public static synchronized float[] rotate(float[] xform, float angle) {
+ return multiply(xform, xform, makeRotation(tmpXForm, angle));
+ }
+
+ public static float[] multiply(float[] result, float[] ta, float[] tb) {
+ float a0 = ta[0], a3 = ta[3],
+ a1 = ta[1], a4 = ta[4],
+ a2 = ta[2], a5 = ta[5],
+ b0 = tb[0], b3 = tb[3],
+ b1 = tb[1], b4 = tb[4],
+ b2 = tb[2], b5 = tb[5];
+ result[0] = a0 * b0 + a1 * b2;
+ result[1] = a0 * b1 + a1 * b3;
+ result[2] = a2 * b0 + a3 * b2;
+ result[3] = a2 * b1 + a3 * b3;
+ result[4] = a4 * b0 + a5 * b2 + b4;
+ result[5] = a4 * b1 + a5 * b3 + b5;
+ return result;
+ }
+}
diff --git a/app/src/main/java/com/winlator/renderer/GLRenderer.java b/app/src/main/java/com/winlator/renderer/GLRenderer.java
new file mode 100644
index 0000000..a4be3aa
--- /dev/null
+++ b/app/src/main/java/com/winlator/renderer/GLRenderer.java
@@ -0,0 +1,345 @@
+package com.winlator.renderer;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+
+import com.winlator.R;
+import com.winlator.math.Mathf;
+import com.winlator.math.XForm;
+import com.winlator.renderer.material.CursorMaterial;
+import com.winlator.renderer.material.ShaderMaterial;
+import com.winlator.renderer.material.WindowMaterial;
+import com.winlator.widget.XServerView;
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Cursor;
+import com.winlator.xserver.Drawable;
+import com.winlator.xserver.Pointer;
+import com.winlator.xserver.Window;
+import com.winlator.xserver.WindowAttributes;
+import com.winlator.xserver.WindowManager;
+import com.winlator.xserver.XLock;
+import com.winlator.xserver.XServer;
+
+import java.util.ArrayList;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+public class GLRenderer implements GLSurfaceView.Renderer, WindowManager.OnWindowModificationListener, Pointer.OnPointerMotionListener {
+ public final XServerView xServerView;
+ private final XServer xServer;
+ private final VertexAttribute quadVertices = new VertexAttribute("position", 2);
+ private final float[] tmpXForm1 = XForm.getInstance();
+ private final float[] tmpXForm2 = XForm.getInstance();
+ private final CursorMaterial cursorMaterial = new CursorMaterial();
+ private final WindowMaterial windowMaterial = new WindowMaterial();
+ private final TransformationInfo transformationInfo = new TransformationInfo();
+ private final Drawable rootCursorDrawable;
+ private final ArrayList renderableWindows = new ArrayList<>();
+ private String forceFullscreenWMClass = null;
+ private boolean fullscreen = false;
+ private boolean toggleFullscreen = false;
+ private boolean cursorVisible = true;
+ private boolean screenOffsetYRelativeToCursor = false;
+ private String[] unviewableWMClasses = null;
+
+ public GLRenderer(XServerView xServerView, XServer xServer) {
+ this.xServerView = xServerView;
+ this.xServer = xServer;
+ rootCursorDrawable = createRootCursorDrawable();
+
+ quadVertices.put(new float[]{
+ 0.0f, 0.0f,
+ 0.0f, 1.0f,
+ 1.0f, 0.0f,
+ 1.0f, 1.0f
+ });
+
+ xServer.windowManager.addOnWindowModificationListener(this);
+ xServer.pointer.addOnPointerMotionListener(this);
+ }
+
+ @Override
+ public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+ GPUImage.checkIsSupported();
+
+ GLES20.glFrontFace(GLES20.GL_CCW);
+ GLES20.glDisable(GLES20.GL_CULL_FACE);
+
+ GLES20.glDisable(GLES20.GL_DEPTH_TEST);
+ GLES20.glDepthMask(false);
+
+ GLES20.glEnable(GLES20.GL_BLEND);
+ GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+ GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl, int width, int height) {
+ transformationInfo.update(width, height, xServer.screenInfo.width, xServer.screenInfo.height);
+ GLES20.glViewport(0, 0, width, height);
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ if (toggleFullscreen) {
+ fullscreen = !fullscreen;
+ toggleFullscreen = false;
+ }
+
+ drawFrame();
+ }
+
+ private void drawFrame() {
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+
+ if (!fullscreen) {
+ int pointerY = 0;
+ if (screenOffsetYRelativeToCursor) {
+ short halfScreenHeight = (short)(xServer.screenInfo.height / 2);
+ pointerY = Mathf.clamp(xServer.pointer.getY() - halfScreenHeight / 2, 0, halfScreenHeight);
+ }
+ XForm.makeTransform(tmpXForm2, transformationInfo.sceneOffsetX, transformationInfo.sceneOffsetY - pointerY, transformationInfo.sceneScaleX, transformationInfo.sceneScaleY, 0);
+ }
+ else XForm.identity(tmpXForm2);
+
+ if (!fullscreen) {
+ GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
+ GLES20.glScissor(transformationInfo.viewOffsetX, transformationInfo.viewOffsetY, transformationInfo.viewWidth, transformationInfo.viewHeight);
+ }
+
+ renderWindows();
+ if (cursorVisible) renderCursor();
+
+ if (!fullscreen) GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+ }
+
+ @Override
+ public void onMapWindow(Window window) {
+ xServerView.queueEvent(this::updateScene);
+ xServerView.requestRender();
+ }
+
+ @Override
+ public void onUnmapWindow(Window window) {
+ xServerView.queueEvent(this::updateScene);
+ xServerView.requestRender();
+ }
+
+ @Override
+ public void onChangeWindowZOrder(Window window) {
+ xServerView.queueEvent(this::updateScene);
+ xServerView.requestRender();
+ }
+
+ @Override
+ public void onUpdateWindowContent(Window window) {
+ xServerView.requestRender();
+ }
+
+ @Override
+ public void onUpdateWindowGeometry(final Window window, boolean resized) {
+ if (resized) {
+ xServerView.queueEvent(this::updateScene);
+ }
+ else xServerView.queueEvent(() -> updateWindowPosition(window));
+ xServerView.requestRender();
+ }
+
+ @Override
+ public void onUpdateWindowAttributes(Window window, Bitmask mask) {
+ if (mask.isSet(WindowAttributes.FLAG_CURSOR)) xServerView.requestRender();
+ }
+
+ @Override
+ public void onPointerMove(short x, short y) {
+ xServerView.requestRender();
+ }
+
+ private void renderDrawable(Drawable drawable, int x, int y, ShaderMaterial material) {
+ renderDrawable(drawable, x, y, material, false);
+ }
+
+ private void renderDrawable(Drawable drawable, int x, int y, ShaderMaterial material, boolean forceFullscreen) {
+ synchronized (drawable.renderLock) {
+ Texture texture = drawable.getTexture();
+ texture.updateFromDrawable(drawable);
+
+ if (forceFullscreen) {
+ short newHeight = (short)Math.min(xServer.screenInfo.height, ((float)xServer.screenInfo.width / drawable.width) * drawable.height);
+ short newWidth = (short)(((float)newHeight / drawable.height) * drawable.width);
+ XForm.set(tmpXForm1, (xServer.screenInfo.width - newWidth) * 0.5f, (xServer.screenInfo.height - newHeight) * 0.5f, newWidth, newHeight);
+ }
+ else XForm.set(tmpXForm1, x, y, drawable.width, drawable.height);
+
+ XForm.multiply(tmpXForm1, tmpXForm1, tmpXForm2);
+
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture.getTextureId());
+ GLES20.glUniform1i(material.getUniformLocation("texture"), 0);
+ GLES20.glUniform1fv(material.getUniformLocation("xform"), tmpXForm1.length, tmpXForm1, 0);
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, quadVertices.count());
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+ }
+ }
+
+ private void renderWindows() {
+ windowMaterial.use();
+ GLES20.glUniform2f(windowMaterial.getUniformLocation("viewSize"), xServer.screenInfo.width, xServer.screenInfo.height);
+ quadVertices.bind(windowMaterial.programId);
+
+ try (XLock lock = xServer.lock(XServer.Lockable.DRAWABLE_MANAGER)) {
+ for (RenderableWindow window : renderableWindows) {
+ renderDrawable(window.content, window.rootX, window.rootY, windowMaterial, window.forceFullscreen);
+ }
+ }
+
+ quadVertices.disable();
+ }
+
+ private void renderCursor() {
+ cursorMaterial.use();
+ GLES20.glUniform2f(cursorMaterial.getUniformLocation("viewSize"), xServer.screenInfo.width, xServer.screenInfo.height);
+ quadVertices.bind(cursorMaterial.programId);
+
+ try (XLock lock = xServer.lock(XServer.Lockable.DRAWABLE_MANAGER)) {
+ Window pointWindow = xServer.inputDeviceManager.getPointWindow();
+ Cursor cursor = pointWindow != null ? pointWindow.attributes.getCursor() : null;
+ short x = xServer.pointer.getClampedX();
+ short y = xServer.pointer.getClampedY();
+
+ if (cursor != null) {
+ if (cursor.isVisible()) renderDrawable(cursor.cursorImage, x - cursor.hotSpotX, y - cursor.hotSpotY, cursorMaterial);
+ }
+ else renderDrawable(rootCursorDrawable, x, y, cursorMaterial);
+ }
+
+ quadVertices.disable();
+ }
+
+ public void toggleFullscreen() {
+ toggleFullscreen = true;
+ xServerView.requestRender();
+ }
+
+ private Drawable createRootCursorDrawable() {
+ Context context = xServerView.getContext();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inScaled = false;
+ Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.cursor, options);
+ return Drawable.fromBitmap(bitmap);
+ }
+
+ private void updateScene() {
+ try (XLock lock = xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.DRAWABLE_MANAGER)) {
+ renderableWindows.clear();
+ collectRenderableWindows(xServer.windowManager.rootWindow, xServer.windowManager.rootWindow.getX(), xServer.windowManager.rootWindow.getY());
+ }
+ }
+
+ private void collectRenderableWindows(Window window, int x, int y) {
+ if (!window.attributes.isMapped()) return;
+ if (window != xServer.windowManager.rootWindow) {
+ boolean viewable = true;
+
+ if (unviewableWMClasses != null) {
+ String wmClass = window.getPropertyValue("WM_CLASS");
+ for (String unviewableWMClass : unviewableWMClasses) {
+ if (wmClass.contains(unviewableWMClass)) {
+ if (window.attributes.isEnabled()) window.disableAllDescendants();
+ viewable = false;
+ break;
+ }
+ }
+ }
+
+ if (viewable) {
+ if (forceFullscreenWMClass != null) {
+ short width = window.getWidth();
+ short height = window.getHeight();
+ boolean forceFullscreen= false;
+
+ if (width >= 320 && height >= 200 && width < xServer.screenInfo.width && height < xServer.screenInfo.height) {
+ Window parent = window.getParent();
+ boolean parentHasWMClass = parent.getPropertyValue("WM_CLASS").contains(forceFullscreenWMClass);
+ boolean hasWMClass = window.getPropertyValue("WM_CLASS").contains(forceFullscreenWMClass);
+ if (hasWMClass) {
+ forceFullscreen = !parentHasWMClass && window.getChildCount() == 0;
+ }
+ else {
+ short borderX = (short)(parent.getWidth() - width);
+ short borderY = (short)(parent.getHeight() - height);
+ if (parent.getChildCount() == 1 && borderX > 0 && borderY > 0 && borderX <= 12) {
+ forceFullscreen = true;
+ removeRenderableWindow(parent);
+ }
+ }
+ }
+
+ renderableWindows.add(new RenderableWindow(window.getContent(), x, y, forceFullscreen));
+ }
+ else renderableWindows.add(new RenderableWindow(window.getContent(), x, y));
+ }
+ }
+
+ for (Window child : window.getChildren()) {
+ collectRenderableWindows(child, child.getX() + x, child.getY() + y);
+ }
+ }
+
+ private void removeRenderableWindow(Window window) {
+ for (int i = 0; i < renderableWindows.size(); i++) {
+ if (renderableWindows.get(i).content == window.getContent()) {
+ renderableWindows.remove(i);
+ break;
+ }
+ }
+ }
+
+ private void updateWindowPosition(Window window) {
+ for (RenderableWindow renderableWindow : renderableWindows) {
+ if (renderableWindow.content == window.getContent()) {
+ renderableWindow.rootX = window.getRootX();
+ renderableWindow.rootY = window.getRootY();
+ break;
+ }
+ }
+ }
+
+ public void setCursorVisible(boolean cursorVisible) {
+ this.cursorVisible = cursorVisible;
+ xServerView.requestRender();
+ }
+
+ public boolean isCursorVisible() {
+ return cursorVisible;
+ }
+
+ public boolean isScreenOffsetYRelativeToCursor() {
+ return screenOffsetYRelativeToCursor;
+ }
+
+ public void setScreenOffsetYRelativeToCursor(boolean screenOffsetYRelativeToCursor) {
+ this.screenOffsetYRelativeToCursor = screenOffsetYRelativeToCursor;
+ xServerView.requestRender();
+ }
+
+ public String getForceFullscreenWMClass() {
+ return forceFullscreenWMClass;
+ }
+
+ public void setForceFullscreenWMClass(String forceFullscreenWMClass) {
+ this.forceFullscreenWMClass = forceFullscreenWMClass;
+ }
+
+ public String[] getUnviewableWMClasses() {
+ return unviewableWMClasses;
+ }
+
+ public void setUnviewableWMClasses(String... unviewableWMNames) {
+ this.unviewableWMClasses = unviewableWMNames;
+ }
+}
diff --git a/app/src/main/java/com/winlator/renderer/GPUImage.java b/app/src/main/java/com/winlator/renderer/GPUImage.java
new file mode 100644
index 0000000..2573297
--- /dev/null
+++ b/app/src/main/java/com/winlator/renderer/GPUImage.java
@@ -0,0 +1,82 @@
+package com.winlator.renderer;
+
+import androidx.annotation.Keep;
+
+import com.winlator.xserver.Drawable;
+
+import java.nio.ByteBuffer;
+
+public class GPUImage extends Texture {
+ private long hardwareBufferPtr;
+ private long imageKHRPtr;
+ private ByteBuffer virtualData;
+ private short stride;
+ private static boolean supported = false;
+
+ static {
+ System.loadLibrary("winlator");
+ }
+
+ public GPUImage(short width, short height) {
+ hardwareBufferPtr = createHardwareBuffer(width, height);
+ if (hardwareBufferPtr != 0) virtualData = lockHardwareBuffer(hardwareBufferPtr);
+ }
+
+ @Override
+ public void allocateTexture(short width, short height, ByteBuffer data) {
+ if (isAllocated()) return;
+ super.allocateTexture(width, height, null);
+ imageKHRPtr = createImageKHR(hardwareBufferPtr, textureId);
+ }
+
+ @Override
+ public void updateFromDrawable(Drawable drawable) {
+ if (!isAllocated()) allocateTexture(drawable.width, drawable.height, null);
+ needsUpdate = false;
+ }
+
+ public short getStride() {
+ return stride;
+ }
+
+ @Keep
+ private void setStride(short stride) {
+ this.stride = stride;
+ }
+
+ public ByteBuffer getVirtualData() {
+ return virtualData;
+ }
+
+ @Override
+ public void destroy() {
+ destroyImageKHR(imageKHRPtr);
+ destroyHardwareBuffer(hardwareBufferPtr);
+ virtualData = null;
+ imageKHRPtr = 0;
+ hardwareBufferPtr = 0;
+ super.destroy();
+ }
+
+ public static boolean isSupported() {
+ return supported;
+ }
+
+ public static void checkIsSupported() {
+ final short size = 8;
+ GPUImage gpuImage = new GPUImage(size, size);
+ gpuImage.allocateTexture(size, size, null);
+ supported = gpuImage.hardwareBufferPtr != 0 && gpuImage.imageKHRPtr != 0 && gpuImage.virtualData != null;
+ gpuImage.destroy();
+ }
+
+ private native long createHardwareBuffer(short width, short height);
+
+ private native void destroyHardwareBuffer(long hardwareBufferPtr);
+
+ private native ByteBuffer lockHardwareBuffer(long hardwareBufferPtr);
+
+ private native long createImageKHR(long hardwareBufferPtr, int textureId);
+
+ private native void destroyImageKHR(long imageKHRPtr);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/renderer/RenderableWindow.java b/app/src/main/java/com/winlator/renderer/RenderableWindow.java
new file mode 100644
index 0000000..207a8d4
--- /dev/null
+++ b/app/src/main/java/com/winlator/renderer/RenderableWindow.java
@@ -0,0 +1,21 @@
+package com.winlator.renderer;
+
+import com.winlator.xserver.Drawable;
+
+class RenderableWindow {
+ final Drawable content;
+ short rootX;
+ short rootY;
+ final boolean forceFullscreen;
+
+ public RenderableWindow(Drawable content, int rootX, int rootY) {
+ this(content, rootX, rootY, false);
+ }
+
+ public RenderableWindow(Drawable content, int rootX, int rootY, boolean forceFullscreen) {
+ this.content = content;
+ this.rootX = (short)rootX;
+ this.rootY = (short)rootY;
+ this.forceFullscreen = forceFullscreen;
+ }
+}
diff --git a/app/src/main/java/com/winlator/renderer/Texture.java b/app/src/main/java/com/winlator/renderer/Texture.java
new file mode 100644
index 0000000..3d0da67
--- /dev/null
+++ b/app/src/main/java/com/winlator/renderer/Texture.java
@@ -0,0 +1,128 @@
+package com.winlator.renderer;
+
+import android.opengl.GLES11Ext;
+import android.opengl.GLES20;
+
+import com.winlator.xserver.Drawable;
+
+import java.nio.ByteBuffer;
+
+public class Texture {
+ protected int textureId = 0;
+ private int wrapS = GLES20.GL_CLAMP_TO_EDGE;
+ private int wrapT = GLES20.GL_CLAMP_TO_EDGE;
+ private int magFilter = GLES20.GL_LINEAR;
+ private int minFilter = GLES20.GL_LINEAR;
+ private int format = GLES11Ext.GL_BGRA;
+ protected boolean needsUpdate = true;
+
+ public void allocateTexture(short width, short height, ByteBuffer data) {
+ int[] textureIds = new int[1];
+ GLES20.glGenTextures(1, textureIds, 0);
+ textureId = textureIds[0];
+
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 4);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
+
+ if (data != null) {
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, format, width, height, 0, format, GLES20.GL_UNSIGNED_BYTE, data);
+ }
+
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, wrapS);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, wrapT);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, magFilter);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, minFilter);
+
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+ }
+
+ public int getWrapS() {
+ return wrapS;
+ }
+
+ public void setWrapS(int wrapS) {
+ this.wrapS = wrapS;
+ }
+
+ public int getWrapT() {
+ return wrapT;
+ }
+
+ public void setWrapT(int wrapT) {
+ this.wrapT = wrapT;
+ }
+
+ public int getMagFilter() {
+ return magFilter;
+ }
+
+ public void setMagFilter(int magFilter) {
+ this.magFilter = magFilter;
+ }
+
+ public int getMinFilter() {
+ return minFilter;
+ }
+
+ public void setMinFilter(int minFilter) {
+ this.minFilter = minFilter;
+ }
+
+ public int getFormat() {
+ return format;
+ }
+
+ public void setFormat(int format) {
+ this.format = format;
+ }
+
+ public boolean isNeedsUpdate() {
+ return needsUpdate;
+ }
+
+ public void setNeedsUpdate(boolean needsUpdate) {
+ this.needsUpdate = needsUpdate;
+ }
+
+ public void updateFromDrawable(Drawable drawable) {
+ ByteBuffer data = drawable.getData();
+ if (data == null) return;
+
+ if (!isAllocated()) {
+ allocateTexture(drawable.width, drawable.height, data);
+ }
+ else if (needsUpdate) {
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
+ GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, drawable.width, drawable.height, format, GLES20.GL_UNSIGNED_BYTE, data);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+ needsUpdate = false;
+ }
+ }
+
+ public boolean isAllocated() {
+ return textureId > 0;
+ }
+
+ public int getTextureId() {
+ return textureId;
+ }
+
+ public void copyFromFramebuffer(int framebuffer, short width, short height) {
+ if (!isAllocated()) allocateTexture(width, height, null);
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer);
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
+ GLES20.glCopyTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, 0, 0, width, height, 0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
+ }
+
+ public void destroy() {
+ if (textureId > 0) {
+ int[] textureIds = new int[]{textureId};
+ GLES20.glDeleteTextures(textureIds.length, textureIds, 0);
+ textureId = 0;
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/renderer/TransformationInfo.java b/app/src/main/java/com/winlator/renderer/TransformationInfo.java
new file mode 100644
index 0000000..258f59d
--- /dev/null
+++ b/app/src/main/java/com/winlator/renderer/TransformationInfo.java
@@ -0,0 +1,26 @@
+package com.winlator.renderer;
+
+class TransformationInfo {
+ int viewWidth;
+ int viewHeight;
+ int viewOffsetX;
+ int viewOffsetY;
+ float sceneScaleX;
+ float sceneScaleY;
+ float sceneOffsetX;
+ float sceneOffsetY;
+
+ void update(int outerWidth, int outerHeight, int innerWidth, int innerHeight) {
+ float scale = Math.min((float)outerWidth / innerWidth, (float)outerHeight / innerHeight);
+
+ viewWidth = (int)Math.ceil(innerWidth * scale);
+ viewHeight = (int)Math.ceil(innerHeight * scale);
+ viewOffsetX = (int)((outerWidth - innerWidth * scale) * 0.5f);
+ viewOffsetY = (int)((outerHeight - innerHeight * scale) * 0.5f);
+
+ sceneScaleX = (innerWidth * scale) / outerWidth;
+ sceneScaleY = (innerHeight * scale) / outerHeight;
+ sceneOffsetX = (innerWidth - innerWidth * sceneScaleX) * 0.5f;
+ sceneOffsetY = (innerHeight - innerHeight * sceneScaleY) * 0.5f;
+ }
+}
diff --git a/app/src/main/java/com/winlator/renderer/VertexAttribute.java b/app/src/main/java/com/winlator/renderer/VertexAttribute.java
new file mode 100644
index 0000000..e325b4f
--- /dev/null
+++ b/app/src/main/java/com/winlator/renderer/VertexAttribute.java
@@ -0,0 +1,79 @@
+package com.winlator.renderer;
+
+import android.opengl.GLES20;
+
+import java.nio.Buffer;
+import java.nio.FloatBuffer;
+
+public class VertexAttribute {
+ private Buffer buffer;
+ private int bufferId = 0;
+ private int location = -1;
+ private final String name;
+ private final byte itemSize;
+ private boolean needsUpdate = true;
+
+ public VertexAttribute(String name, int itemSize) {
+ this.name = name;
+ this.itemSize = (byte)itemSize;
+ }
+
+ public int getLocation() {
+ return location;
+ }
+
+ public void put(float[] array) {
+ buffer = FloatBuffer.wrap(array);
+ needsUpdate = true;
+ }
+
+ public void update() {
+ if (!needsUpdate || buffer == null) return;
+ if (bufferId == 0) {
+ int[] bufferIds = new int[1];
+ GLES20.glGenBuffers(1, bufferIds, 0);
+ bufferId = bufferIds[0];
+ }
+
+ int size = buffer.limit() * 4;
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId);
+ GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, size, buffer, GLES20.GL_STATIC_DRAW);
+
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+ needsUpdate = false;
+ }
+
+ public void bind(int programId) {
+ update();
+ if (location == -1) location = GLES20.glGetAttribLocation(programId, name);
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId);
+ GLES20.glEnableVertexAttribArray(location);
+ GLES20.glVertexAttribPointer(location, itemSize, GLES20.GL_FLOAT, false, 0, 0);
+ }
+
+ public void disable() {
+ if (location == -1) return;
+ GLES20.glDisableVertexAttribArray(location);
+ }
+
+ public void destroy() {
+ clear();
+
+ if (bufferId > 0) {
+ int[] bufferIds = new int[]{bufferId};
+ GLES20.glDeleteBuffers(bufferIds.length, bufferIds, 0);
+ bufferId = 0;
+ }
+ }
+
+ public void clear() {
+ if (buffer != null) {
+ buffer.limit(0);
+ buffer = null;
+ }
+ }
+
+ public int count() {
+ return buffer != null ? buffer.limit() / itemSize : 0;
+ }
+}
diff --git a/app/src/main/java/com/winlator/renderer/material/CursorMaterial.java b/app/src/main/java/com/winlator/renderer/material/CursorMaterial.java
new file mode 100644
index 0000000..554abee
--- /dev/null
+++ b/app/src/main/java/com/winlator/renderer/material/CursorMaterial.java
@@ -0,0 +1,37 @@
+package com.winlator.renderer.material;
+
+public class CursorMaterial extends ShaderMaterial {
+ public CursorMaterial() {
+ setUniformNames("xform", "viewSize", "texture");
+ }
+
+ @Override
+ protected String getVertexShader() {
+ return
+ "uniform float xform[6];\n" +
+ "uniform vec2 viewSize;\n" +
+ "attribute vec2 position;\n" +
+ "varying vec2 vUV;\n" +
+
+ "void main() {\n" +
+ "vUV = position;\n" +
+ "vec2 transformedPos = applyXForm(position, xform);\n" +
+ "gl_Position = vec4(2.0 * transformedPos.x / viewSize.x - 1.0, 1.0 - 2.0 * transformedPos.y / viewSize.y, 0.0, 1.0);\n" +
+ "}"
+ ;
+ }
+
+ @Override
+ protected String getFragmentShader() {
+ return
+ "precision mediump float;\n" +
+
+ "uniform sampler2D texture;\n" +
+ "varying vec2 vUV;\n" +
+
+ "void main() {\n" +
+ "gl_FragColor = texture2D(texture, vUV);\n" +
+ "}"
+ ;
+ }
+}
diff --git a/app/src/main/java/com/winlator/renderer/material/ShaderMaterial.java b/app/src/main/java/com/winlator/renderer/material/ShaderMaterial.java
new file mode 100644
index 0000000..700fb06
--- /dev/null
+++ b/app/src/main/java/com/winlator/renderer/material/ShaderMaterial.java
@@ -0,0 +1,84 @@
+package com.winlator.renderer.material;
+
+import android.opengl.GLES20;
+
+import androidx.collection.ArrayMap;
+
+public class ShaderMaterial {
+ public int programId;
+ private final ArrayMap uniforms = new ArrayMap<>();
+
+ public void setUniformNames(String... names) {
+ uniforms.clear();
+ for (String name : names) uniforms.put(name, -1);
+ }
+
+ protected static int compileShaders(String vertexShader, String fragmentShader) {
+ int beginIndex = vertexShader.indexOf("void main() {");
+ vertexShader = vertexShader.substring(0, beginIndex) +
+ "vec2 applyXForm(vec2 p, float xform[6]) {\n" +
+ "return vec2(xform[0] * p.x + xform[2] * p.y + xform[4], xform[1] * p.x + xform[3] * p.y + xform[5]);\n" +
+ "}\n" +
+ vertexShader.substring(beginIndex);
+
+ int programId = GLES20.glCreateProgram();
+ int[] compiled = new int[1];
+
+ int vertexShaderId = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
+ GLES20.glShaderSource(vertexShaderId, vertexShader);
+ GLES20.glCompileShader(vertexShaderId);
+
+ GLES20.glGetShaderiv(vertexShaderId, GLES20.GL_COMPILE_STATUS, compiled, 0);
+ if (compiled[0] == 0) {
+ throw new RuntimeException("Could not compile vertex shader: \n" + GLES20.glGetShaderInfoLog(vertexShaderId));
+ }
+ GLES20.glAttachShader(programId, vertexShaderId);
+
+ int fragmentShaderId = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
+ GLES20.glShaderSource(fragmentShaderId, fragmentShader);
+ GLES20.glCompileShader(fragmentShaderId);
+
+ GLES20.glGetShaderiv(fragmentShaderId, GLES20.GL_COMPILE_STATUS, compiled, 0);
+ if (compiled[0] == 0) {
+ throw new RuntimeException("Could not compile fragment shader: \n" + GLES20.glGetShaderInfoLog(fragmentShaderId));
+ }
+ GLES20.glAttachShader(programId, fragmentShaderId);
+
+ GLES20.glLinkProgram(programId);
+
+ GLES20.glDeleteShader(vertexShaderId);
+ GLES20.glDeleteShader(fragmentShaderId);
+ return programId;
+ }
+
+ protected String getVertexShader() {
+ return "";
+ }
+
+ protected String getFragmentShader() {
+ return "";
+ }
+
+ public void use() {
+ if (programId == 0) programId = compileShaders(getVertexShader(), getFragmentShader());
+ GLES20.glUseProgram(programId);
+
+ for (int i = 0; i < uniforms.size(); i++) {
+ int location = uniforms.valueAt(i);
+ if (location == -1) {
+ String name = uniforms.keyAt(i);
+ uniforms.put(name, GLES20.glGetUniformLocation(programId, name));
+ }
+ }
+ }
+
+ public int getUniformLocation(String name) {
+ Integer location = uniforms.get(name);
+ return location != null ? location : -1;
+ }
+
+ public void destroy() {
+ GLES20.glDeleteProgram(programId);
+ programId = 0;
+ }
+}
diff --git a/app/src/main/java/com/winlator/renderer/material/WindowMaterial.java b/app/src/main/java/com/winlator/renderer/material/WindowMaterial.java
new file mode 100644
index 0000000..fe5cc7a
--- /dev/null
+++ b/app/src/main/java/com/winlator/renderer/material/WindowMaterial.java
@@ -0,0 +1,37 @@
+package com.winlator.renderer.material;
+
+public class WindowMaterial extends ShaderMaterial {
+ public WindowMaterial() {
+ setUniformNames("xform", "viewSize", "texture");
+ }
+
+ @Override
+ protected String getVertexShader() {
+ return
+ "uniform float xform[6];\n" +
+ "uniform vec2 viewSize;\n" +
+ "attribute vec2 position;\n" +
+ "varying vec2 vUV;\n" +
+
+ "void main() {\n" +
+ "vUV = position;\n" +
+ "vec2 transformedPos = applyXForm(position, xform);\n" +
+ "gl_Position = vec4(2.0 * transformedPos.x / viewSize.x - 1.0, 1.0 - 2.0 * transformedPos.y / viewSize.y, 0.0, 1.0);\n" +
+ "}"
+ ;
+ }
+
+ @Override
+ protected String getFragmentShader() {
+ return
+ "precision mediump float;\n" +
+
+ "uniform sampler2D texture;\n" +
+ "varying vec2 vUV;\n" +
+
+ "void main() {\n" +
+ "gl_FragColor = vec4(texture2D(texture, vUV).rgb, 1.0);\n" +
+ "}"
+ ;
+ }
+}
diff --git a/app/src/main/java/com/winlator/sysvshm/RequestCodes.java b/app/src/main/java/com/winlator/sysvshm/RequestCodes.java
new file mode 100644
index 0000000..7b915b4
--- /dev/null
+++ b/app/src/main/java/com/winlator/sysvshm/RequestCodes.java
@@ -0,0 +1,7 @@
+package com.winlator.sysvshm;
+
+public abstract class RequestCodes {
+ public static final byte SHMGET = 0;
+ public static final byte GET_FD = 1;
+ public static final byte DELETE = 2;
+}
diff --git a/app/src/main/java/com/winlator/sysvshm/SysVSHMConnectionHandler.java b/app/src/main/java/com/winlator/sysvshm/SysVSHMConnectionHandler.java
new file mode 100644
index 0000000..f64c31b
--- /dev/null
+++ b/app/src/main/java/com/winlator/sysvshm/SysVSHMConnectionHandler.java
@@ -0,0 +1,21 @@
+package com.winlator.sysvshm;
+
+import com.winlator.xconnector.Client;
+import com.winlator.xconnector.ConnectionHandler;
+
+public class SysVSHMConnectionHandler implements ConnectionHandler {
+ private final SysVSharedMemory sysVSharedMemory;
+
+ public SysVSHMConnectionHandler(SysVSharedMemory sysVSharedMemory) {
+ this.sysVSharedMemory = sysVSharedMemory;
+ }
+
+ @Override
+ public void handleNewConnection(Client client) {
+ client.createIOStreams();
+ client.setTag(sysVSharedMemory);
+ }
+
+ @Override
+ public void handleConnectionShutdown(Client client) {}
+}
diff --git a/app/src/main/java/com/winlator/sysvshm/SysVSHMRequestHandler.java b/app/src/main/java/com/winlator/sysvshm/SysVSHMRequestHandler.java
new file mode 100644
index 0000000..0f3b97d
--- /dev/null
+++ b/app/src/main/java/com/winlator/sysvshm/SysVSHMRequestHandler.java
@@ -0,0 +1,48 @@
+package com.winlator.sysvshm;
+
+import com.winlator.xconnector.Client;
+import com.winlator.xconnector.RequestHandler;
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+
+import java.io.IOException;
+
+public class SysVSHMRequestHandler implements RequestHandler {
+ @Override
+ public boolean handleRequest(Client client) throws IOException {
+ SysVSharedMemory sysVSharedMemory = (SysVSharedMemory)client.getTag();
+ XInputStream inputStream = client.getInputStream();
+ XOutputStream outputStream = client.getOutputStream();
+
+ if (inputStream.available() < 5) return false;
+ byte requestCode = inputStream.readByte();
+
+ switch (requestCode) {
+ case RequestCodes.SHMGET: {
+ long size = inputStream.readUnsignedInt();
+ int shmid = sysVSharedMemory.get(size);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeInt(shmid);
+ }
+ break;
+ }
+ case RequestCodes.GET_FD: {
+ int shmid = inputStream.readInt();
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte((byte)0);
+ outputStream.setAncillaryFd(sysVSharedMemory.getFd(shmid));
+ }
+ break;
+ }
+ case RequestCodes.DELETE: {
+ int shmid = inputStream.readInt();
+ sysVSharedMemory.delete(shmid);
+ break;
+ }
+ }
+ return true;
+ }
+}
diff --git a/app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java b/app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java
new file mode 100644
index 0000000..7d2242a
--- /dev/null
+++ b/app/src/main/java/com/winlator/sysvshm/SysVSharedMemory.java
@@ -0,0 +1,116 @@
+package com.winlator.sysvshm;
+
+import android.os.SharedMemory;
+import android.system.ErrnoException;
+import android.util.SparseArray;
+
+import com.winlator.xconnector.XConnectorEpoll;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+public class SysVSharedMemory {
+ private final SparseArray shmemories = new SparseArray<>();
+ private int maxSHMemoryId = 0;
+
+ static {
+ System.loadLibrary("winlator");
+ }
+
+ private static class SHMemory {
+ private int fd;
+ private long size;
+ private ByteBuffer data;
+ }
+
+ public int getFd(int shmid) {
+ synchronized (shmemories) {
+ SHMemory shmemory = shmemories.get(shmid);
+ return shmemory != null ? shmemory.fd : -1;
+ }
+ }
+
+ public int get(long size) {
+ synchronized (shmemories) {
+ int index = shmemories.size();
+ int fd = ashmemCreateRegion(index, size);
+ if (fd < 0) fd = createSharedMemory("sysvshm-"+index, (int)size);
+ if (fd < 0) return -1;
+
+ SHMemory shmemory = new SHMemory();
+ int id = ++maxSHMemoryId;
+ shmemory.fd = fd;
+ shmemory.size = size;
+ shmemories.put(id, shmemory);
+ return id;
+ }
+ }
+
+ public void delete(int shmid) {
+ SHMemory shmemory = shmemories.get(shmid);
+ if (shmemory != null) {
+ if (shmemory.fd != -1) {
+ XConnectorEpoll.closeFd(shmemory.fd);
+ shmemory.fd = -1;
+ }
+ shmemories.remove(shmid);
+ }
+ }
+
+ public void deleteAll() {
+ synchronized (shmemories) {
+ for (int i = shmemories.size() - 1; i >= 0; i--) delete(shmemories.keyAt(i));
+ }
+ }
+
+ public ByteBuffer attach(int shmid) {
+ synchronized (shmemories) {
+ SHMemory shmemory = shmemories.get(shmid);
+ if (shmemory != null) {
+ if (shmemory.data == null) shmemory.data = mapSHMSegment(shmemory.fd, shmemory.size, 0, true);
+ return shmemory.data;
+ }
+ else return null;
+ }
+ }
+
+ public void detach(ByteBuffer data) {
+ synchronized (shmemories) {
+ for (int i = 0; i < shmemories.size(); i++) {
+ SHMemory shmemory = shmemories.valueAt(i);
+ if (shmemory.data == data) {
+ if (shmemory.data != null) {
+ unmapSHMSegment(shmemory.data, shmemory.size);
+ shmemory.data = null;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private static int createSharedMemory(String name, int size) {
+ try {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O_MR1) {
+ SharedMemory sharedMemory = SharedMemory.create(name, size);
+ try {
+ Method method = sharedMemory.getClass().getMethod("getFd");
+ Object ret = method.invoke(sharedMemory);
+ if (ret != null) return (int)ret;
+ }
+ catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {}
+ }
+ }
+ catch (ErrnoException e) {}
+ return -1;
+ }
+
+ public static native int createMemoryFd(String name, int size);
+
+ private static native int ashmemCreateRegion(int index, long size);
+
+ public static native ByteBuffer mapSHMSegment(int fd, long size, int offset, boolean readonly);
+
+ public static native void unmapSHMSegment(ByteBuffer data, long size);
+}
diff --git a/app/src/main/java/com/winlator/widget/CPUListView.java b/app/src/main/java/com/winlator/widget/CPUListView.java
new file mode 100644
index 0000000..eb2adbe
--- /dev/null
+++ b/app/src/main/java/com/winlator/widget/CPUListView.java
@@ -0,0 +1,77 @@
+package com.winlator.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.winlator.R;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class CPUListView extends LinearLayout {
+ private List checkedCPUList;
+ private final byte numProcessors;
+
+ public CPUListView(Context context) {
+ this(context, null);
+ }
+
+ public CPUListView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CPUListView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setOrientation(HORIZONTAL);
+ numProcessors = (byte)Runtime.getRuntime().availableProcessors();
+ refreshContent();
+ }
+
+ private void refreshContent() {
+ removeAllViews();
+ int numProcessors = Runtime.getRuntime().availableProcessors();
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+
+ for (int i = 0; i < numProcessors; i++) {
+ View itemView = inflater.inflate(R.layout.cpu_list_item, this, false);
+ String tag = "CPU"+i;
+ CheckBox checkBox = itemView.findViewById(R.id.CheckBox);
+ checkBox.setTag(tag);
+ checkBox.setChecked(checkedCPUList != null ? checkedCPUList.contains(String.valueOf(i)) : i >= numProcessors / 2);
+
+ ((TextView)itemView.findViewById(R.id.TextView)).setText(tag);
+ addView(itemView);
+ }
+ }
+
+ public void setCheckedCPUList(String checkedCPUList) {
+ this.checkedCPUList = Arrays.asList(checkedCPUList.split(","));
+ refreshContent();
+ }
+
+ public String getCheckedCPUListAsString() {
+ String cpuList = "";
+
+ for (int i = 0; i < numProcessors; i++) {
+ CheckBox checkBox = findViewWithTag("CPU"+i);
+ if (checkBox.isChecked()) cpuList += (!cpuList.isEmpty() ? "," : "")+i;
+ }
+ return cpuList;
+ }
+
+ public boolean[] getCheckedCPUList() {
+ boolean[] cpuList = new boolean[numProcessors];
+ for (int i = 0; i < numProcessors; i++) {
+ CheckBox checkBox = findViewWithTag("CPU"+i);
+ cpuList[i] = checkBox.isChecked();
+ }
+ return cpuList;
+ }
+}
diff --git a/app/src/main/java/com/winlator/widget/InputControlsView.java b/app/src/main/java/com/winlator/widget/InputControlsView.java
new file mode 100644
index 0000000..13e1c1f
--- /dev/null
+++ b/app/src/main/java/com/winlator/widget/InputControlsView.java
@@ -0,0 +1,476 @@
+package com.winlator.widget;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.winlator.inputcontrols.Binding;
+import com.winlator.inputcontrols.ControlElement;
+import com.winlator.inputcontrols.ControlsProfile;
+import com.winlator.inputcontrols.ExternalController;
+import com.winlator.inputcontrols.ExternalControllerBinding;
+import com.winlator.math.Mathf;
+import com.winlator.xserver.Pointer;
+import com.winlator.xserver.XServer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class InputControlsView extends View {
+ public static final float DEFAULT_OVERLAY_OPACITY = 0.4f;
+ public static final float JOYSTICK_DEAD_ZONE = 0.15f;
+ public static final float DPAD_DEAD_ZONE = 0.3f;
+ private boolean editMode = false;
+ private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Path path = new Path();
+ private final ColorFilter colorFilter = new PorterDuffColorFilter(0xffffffff, PorterDuff.Mode.SRC_IN);
+ private final Point cursor = new Point();
+ private boolean readyToDraw = false;
+ private boolean moveCursor = false;
+ private int snappingSize;
+ private float offsetX;
+ private float offsetY;
+ private ControlElement selectedElement;
+ private ControlsProfile profile;
+ private float overlayOpacity = DEFAULT_OVERLAY_OPACITY;
+ private TouchpadView touchpadView;
+ private XServer xServer;
+ private final Bitmap[] icons = new Bitmap[16];
+ private Timer mouseMoveTimer;
+ private final PointF mouseMoveOffset = new PointF();
+ private boolean showTouchscreenControls = true;
+
+ public InputControlsView(Context context) {
+ super(context);
+ setClickable(true);
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ setBackgroundColor(0x00000000);
+ setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ }
+
+ public void setEditMode(boolean editMode) {
+ this.editMode = editMode;
+ }
+
+ public void setOverlayOpacity(float overlayOpacity) {
+ this.overlayOpacity = overlayOpacity;
+ }
+
+ public int getSnappingSize() {
+ return snappingSize;
+ }
+
+ @Override
+ protected synchronized void onDraw(Canvas canvas) {
+ int width = getWidth();
+ int height = getHeight();
+
+ if (width == 0 || height == 0) {
+ readyToDraw = false;
+ return;
+ }
+
+ snappingSize = width / 100;
+ readyToDraw = true;
+
+ if (editMode) {
+ drawGrid(canvas);
+ drawCursor(canvas);
+ }
+
+ if (profile != null) {
+ if (!profile.isElementsLoaded()) profile.loadElements(this);
+ if (showTouchscreenControls) for (ControlElement element : profile.getElements()) element.draw(canvas);
+ }
+
+ super.onDraw(canvas);
+ }
+
+ private void drawGrid(Canvas canvas) {
+ paint.setStyle(Paint.Style.FILL);
+ paint.setStrokeWidth(snappingSize * 0.0625f);
+ paint.setColor(0xff000000);
+ canvas.drawColor(Color.BLACK);
+
+ paint.setAntiAlias(false);
+ paint.setColor(0xff303030);
+
+ int width = getMaxWidth();
+ int height = getMaxHeight();
+
+ for (int i = 0; i < width; i += snappingSize) {
+ canvas.drawLine(i, 0, i, height, paint);
+ canvas.drawLine(0, i, width, i, paint);
+ }
+
+ float cx = Mathf.roundTo(width * 0.5f, snappingSize);
+ float cy = Mathf.roundTo(height * 0.5f, snappingSize);
+ paint.setColor(0xff424242);
+
+ for (int i = 0; i < width; i += snappingSize * 2) {
+ canvas.drawLine(cx, i, cx, i + snappingSize, paint);
+ canvas.drawLine(i, cy, i + snappingSize, cy, paint);
+ }
+
+ paint.setAntiAlias(true);
+ }
+
+ private void drawCursor(Canvas canvas) {
+ paint.setStyle(Paint.Style.FILL);
+ paint.setStrokeWidth(snappingSize * 0.0625f);
+ paint.setColor(0xffc62828);
+
+ paint.setAntiAlias(false);
+ canvas.drawLine(0, cursor.y, getMaxWidth(), cursor.y, paint);
+ canvas.drawLine(cursor.x, 0, cursor.x, getMaxHeight(), paint);
+
+ paint.setAntiAlias(true);
+ }
+
+ public synchronized boolean addElement() {
+ if (editMode && profile != null) {
+ ControlElement element = new ControlElement(this);
+ element.setX(cursor.x);
+ element.setY(cursor.y);
+ profile.addElement(element);
+ profile.save();
+ selectElement(element);
+ return true;
+ }
+ else return false;
+ }
+
+ public synchronized boolean removeElement() {
+ if (editMode && selectedElement != null && profile != null) {
+ profile.removeElement(selectedElement);
+ selectedElement = null;
+ profile.save();
+ invalidate();
+ return true;
+ }
+ else return false;
+ }
+
+ public ControlElement getSelectedElement() {
+ return selectedElement;
+ }
+
+ private synchronized void deselectAllElements() {
+ selectedElement = null;
+ if (profile != null) {
+ for (ControlElement element : profile.getElements()) element.setSelected(false);
+ }
+ }
+
+ private void selectElement(ControlElement element) {
+ deselectAllElements();
+ if (element != null) {
+ selectedElement = element;
+ selectedElement.setSelected(true);
+ }
+ invalidate();
+ }
+
+ public synchronized ControlsProfile getProfile() {
+ return profile;
+ }
+
+ public synchronized void setProfile(ControlsProfile profile) {
+ if (profile != null) {
+ this.profile = profile;
+ deselectAllElements();
+ }
+ else this.profile = null;
+ }
+
+ public boolean isShowTouchscreenControls() {
+ return showTouchscreenControls;
+ }
+
+ public void setShowTouchscreenControls(boolean showTouchscreenControls) {
+ this.showTouchscreenControls = showTouchscreenControls;
+ }
+
+ public int getPrimaryColor() {
+ return Color.argb((int)(overlayOpacity * 255), 255, 255, 255);
+ }
+
+ public int getSecondaryColor() {
+ return Color.argb((int)(overlayOpacity * 255), 2, 119, 189);
+ }
+
+ private synchronized ControlElement intersectElement(float x, float y) {
+ if (profile != null) {
+ for (ControlElement element : profile.getElements()) {
+ if (element.containsPoint(x, y)) return element;
+ }
+ }
+ return null;
+ }
+
+ public Paint getPaint() {
+ return paint;
+ }
+
+ public Path getPath() {
+ return path;
+ }
+
+ public ColorFilter getColorFilter() {
+ return colorFilter;
+ }
+
+ public void setTouchpadView(TouchpadView touchpadView) {
+ this.touchpadView = touchpadView;
+ }
+
+ public void setXServer(XServer xServer) {
+ this.xServer = xServer;
+ createMouseMoveTimer();
+ }
+
+ public int getMaxWidth() {
+ return (int)Mathf.roundTo(getWidth(), snappingSize);
+ }
+
+ public int getMaxHeight() {
+ return (int)Mathf.roundTo(getHeight(), snappingSize);
+ }
+
+ private void createMouseMoveTimer() {
+ if (profile != null && mouseMoveTimer == null) {
+ final float cursorSpeed = profile.getCursorSpeed();
+ mouseMoveTimer = new Timer();
+ mouseMoveTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ xServer.injectPointerMoveDelta((int)(mouseMoveOffset.x * 10 * cursorSpeed), (int)(mouseMoveOffset.y * 10 * cursorSpeed));
+ }
+ }, 0, 1000 / 60);
+ }
+ }
+
+ private void processJoystickInput(MotionEvent event, ExternalController controller, int historyPos) {
+ ExternalControllerBinding controllerBinding;
+ final int[] axes = {MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ, MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y};
+ for (int axis : axes) {
+ float value = ExternalController.getCenteredAxis(event, axis, historyPos);
+ if (Math.abs(value) > JOYSTICK_DEAD_ZONE) {
+ controllerBinding = controller.getControllerBinding(ExternalControllerBinding.getKeyCodeForAxis(axis, Mathf.sign(value)));
+ if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), true, value);
+ }
+ else {
+ controllerBinding = controller.getControllerBinding(ExternalControllerBinding.getKeyCodeForAxis(axis, (byte) 1));
+ if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), false, value);
+ controllerBinding = controller.getControllerBinding(ExternalControllerBinding.getKeyCodeForAxis(axis, (byte)-1));
+ if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), false, value);
+ }
+ }
+ }
+
+ private void processTriggerButton(MotionEvent event, ExternalController controller) {
+ ExternalControllerBinding controllerBinding;
+ controllerBinding = controller.getControllerBinding(ExternalControllerBinding.AXIS_LTRIGGER);
+ if (controllerBinding != null) {
+ boolean pressed = event.getAxisValue(MotionEvent.AXIS_LTRIGGER) == 1 || event.getAxisValue(MotionEvent.AXIS_BRAKE) == 1;
+ handleInputEvent(controllerBinding.getBinding(), pressed);
+ }
+
+ controllerBinding = controller.getControllerBinding(ExternalControllerBinding.AXIS_RTRIGGER);
+ if (controllerBinding != null) {
+ boolean pressed = event.getAxisValue(MotionEvent.AXIS_RTRIGGER) == 1 || event.getAxisValue(MotionEvent.AXIS_GAS) == 1;
+ handleInputEvent(controllerBinding.getBinding(), pressed);
+ }
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (!editMode && profile != null) {
+ ExternalController controller = profile.getController(event.getDeviceId());
+ if (controller != null) {
+ if (ExternalController.isDPadDevice(event)) {
+ processTriggerButton(event, controller);
+ processJoystickInput(event, controller, -1);
+ return true;
+ }
+ else if (ExternalController.isJoystickDevice(event)) {
+ int historySize = event.getHistorySize();
+ for (int i = 0; i < historySize; i++) processJoystickInput(event, controller, i);
+ processJoystickInput(event, controller, -1);
+ return true;
+ }
+ }
+ }
+
+ return super.onGenericMotionEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (editMode && readyToDraw) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ float x = event.getX();
+ float y = event.getY();
+
+ ControlElement element = intersectElement(x, y);
+ moveCursor = true;
+ if (element != null) {
+ offsetX = x - element.getX();
+ offsetY = y - element.getY();
+ moveCursor = false;
+ }
+
+ selectElement(element);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (selectedElement != null) {
+ selectedElement.setX((int)Mathf.roundTo(event.getX() - offsetX, snappingSize));
+ selectedElement.setY((int)Mathf.roundTo(event.getY() - offsetY, snappingSize));
+ invalidate();
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ if (selectedElement != null && profile != null) profile.save();
+ if (moveCursor) cursor.set((int)Mathf.roundTo(event.getX(), snappingSize), (int)Mathf.roundTo(event.getY(), snappingSize));
+ invalidate();
+ break;
+ }
+ }
+ }
+
+ if (!editMode && profile != null) {
+ int actionIndex = event.getActionIndex();
+ int pointerId = event.getPointerId(actionIndex);
+ int actionMasked = event.getActionMasked();
+ boolean handled = false;
+
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ float x = event.getX(actionIndex);
+ float y = event.getY(actionIndex);
+
+ touchpadView.setPointerButtonLeftEnabled(true);
+ for (ControlElement element : profile.getElements()) {
+ if (element.handleTouchDown(pointerId, x, y)) handled = true;
+ if (element.getBindingAt(0) == Binding.MOUSE_LEFT_BUTTON) {
+ touchpadView.setPointerButtonLeftEnabled(false);
+ }
+ }
+ if (!handled) touchpadView.onTouchEvent(event);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ for (byte i = 0, count = (byte)event.getPointerCount(); i < count; i++) {
+ float x = event.getX(i);
+ float y = event.getY(i);
+
+ handled = false;
+ for (ControlElement element : profile.getElements()) {
+ if (element.handleTouchMove(i, x, y)) handled = true;
+ }
+ if (!handled) touchpadView.onTouchEvent(event);
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ for (ControlElement element : profile.getElements()) if (element.handleTouchUp(pointerId)) handled = true;
+ if (!handled) touchpadView.onTouchEvent(event);
+ break;
+ }
+ }
+ return true;
+ }
+
+ public boolean onKeyEvent(KeyEvent event) {
+ if (profile != null && event.getRepeatCount() == 0) {
+ ExternalController controller = profile.getController(event.getDeviceId());
+ if (controller != null) {
+ ExternalControllerBinding controllerBinding = controller.getControllerBinding(event.getKeyCode());
+ if (controllerBinding != null) {
+ int action = event.getAction();
+
+ if (action == KeyEvent.ACTION_DOWN) {
+ handleInputEvent(controllerBinding.getBinding(), true);
+ }
+ else if (action == KeyEvent.ACTION_UP) {
+ handleInputEvent(controllerBinding.getBinding(), false);
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public void handleInputEvent(Binding binding, boolean isActionDown) {
+ handleInputEvent(binding, isActionDown, 0);
+ }
+
+ public void handleInputEvent(Binding binding, boolean isActionDown, float offset) {
+ if (isActionDown) {
+ if (binding == Binding.MOUSE_MOVE_LEFT || binding == Binding.MOUSE_MOVE_RIGHT) {
+ mouseMoveOffset.x = offset != 0 ? offset : (binding == Binding.MOUSE_MOVE_LEFT ? -1 : 1);
+ createMouseMoveTimer();
+ }
+ else if (binding == Binding.MOUSE_MOVE_DOWN || binding == Binding.MOUSE_MOVE_UP) {
+ mouseMoveOffset.y = offset != 0 ? offset : (binding == Binding.MOUSE_MOVE_UP ? -1 : 1);
+ createMouseMoveTimer();
+ }
+ else {
+ Pointer.Button pointerButton = binding.getPointerButton();
+ if (pointerButton != null) {
+ xServer.injectPointerButtonPress(pointerButton);
+ }
+ else xServer.injectKeyPress(binding.keycode);
+ }
+ }
+ else {
+ if (binding == Binding.MOUSE_MOVE_LEFT || binding == Binding.MOUSE_MOVE_RIGHT) {
+ if (Math.abs(offset) < InputControlsView.JOYSTICK_DEAD_ZONE) mouseMoveOffset.x = 0;
+ }
+ else if (binding == Binding.MOUSE_MOVE_UP || binding == Binding.MOUSE_MOVE_DOWN) {
+ if (Math.abs(offset) < InputControlsView.JOYSTICK_DEAD_ZONE) mouseMoveOffset.y = 0;
+ }
+ else {
+ Pointer.Button pointerButton = binding.getPointerButton();
+ if (pointerButton != null) {
+ xServer.injectPointerButtonRelease(pointerButton);
+ }
+ else xServer.injectKeyRelease(binding.keycode);
+ }
+ }
+ }
+
+ public Bitmap getIcon(byte id) {
+ if (icons[id] == null) {
+ Context context = getContext();
+ try (InputStream is = context.getAssets().open("inputcontrols/icons/"+id+".png")) {
+ icons[id] = BitmapFactory.decodeStream(is);
+ }
+ catch (IOException e) {}
+ }
+ return icons[id];
+ }
+}
diff --git a/app/src/main/java/com/winlator/widget/TouchpadView.java b/app/src/main/java/com/winlator/widget/TouchpadView.java
new file mode 100644
index 0000000..0028d05
--- /dev/null
+++ b/app/src/main/java/com/winlator/widget/TouchpadView.java
@@ -0,0 +1,255 @@
+package com.winlator.widget;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.winlator.core.CursorLocker;
+import com.winlator.core.UnitUtils;
+import com.winlator.winhandler.MouseEventFlags;
+import com.winlator.winhandler.WinHandler;
+import com.winlator.xserver.Pointer;
+import com.winlator.xserver.XServer;
+
+public class TouchpadView extends View {
+ private static final float DEFAULT_SENSITIVITY = 2.5f;
+ private static final byte MAX_FINGERS = 4;
+ private static final short MAX_TWO_FINGERS_SCROLL_DISTANCE = 150;
+ private static final byte MAX_TAP_TRAVEL_DISTANCE = 10;
+ private final Finger[] fingers = new Finger[MAX_FINGERS];
+ private byte numFingers = 0;
+ private float sensitivity = 1.0f;
+ private boolean pointerButtonLeftEnabled = true;
+ private boolean pointerButtonRightEnabled = true;
+ private Finger fingerPointerButtonLeft;
+ private Finger fingerPointerButtonRight;
+ private float scrollAccumY = 0;
+ private boolean scrolling = false;
+ private final XServer xServer;
+ private Runnable fourFingersTapCallback;
+
+ public TouchpadView(Context context, XServer xServer) {
+ super(context);
+ this.xServer = xServer;
+ setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ setClickable(true);
+ setFocusable(false);
+ setFocusableInTouchMode(false);
+ }
+
+ private class Finger {
+ private float x;
+ private float y;
+ private final float startX;
+ private final float startY;
+ private float lastX;
+ private float lastY;
+ private final long touchTime;
+
+ public Finger(float x, float y) {
+ this.x = this.startX = this.lastX = UnitUtils.pxToDp(x);
+ this.y = this.startY = this.lastY = UnitUtils.pxToDp(y);
+ touchTime = System.currentTimeMillis();
+ }
+
+ public void update(float x, float y) {
+ lastX = this.x;
+ lastY = this.y;
+ this.x = UnitUtils.pxToDp(x);
+ this.y = UnitUtils.pxToDp(y);
+ }
+
+ private int deltaX() {
+ float dx = (x - lastX) * DEFAULT_SENSITIVITY * sensitivity;
+ return (int)(x <= lastX ? Math.floor(dx) : Math.ceil(dx));
+ }
+
+ private int deltaY() {
+ float dy = (y - lastY) * DEFAULT_SENSITIVITY * sensitivity;
+ return (int)(y <= lastY ? Math.floor(dy) : Math.ceil(dy));
+ }
+
+ private boolean isTap() {
+ return (System.currentTimeMillis() - touchTime) < 200 && travelDistance() < MAX_TAP_TRAVEL_DISTANCE;
+ }
+
+ private float travelDistance() {
+ return (float)Math.hypot(x - startX, y - startY);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int actionIndex = event.getActionIndex();
+ int pointerId = event.getPointerId(actionIndex);
+ int actionMasked = event.getActionMasked();
+ if (pointerId >= MAX_FINGERS) return true;
+
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ scrollAccumY = 0;
+ scrolling = false;
+ fingers[pointerId] = new Finger(event.getX(actionIndex), event.getY(actionIndex));
+ numFingers++;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ for (byte i = 0; i < MAX_FINGERS; i++) {
+ if (fingers[i] != null) {
+ int pointerIndex = event.findPointerIndex(i);
+ if (pointerIndex >= 0) {
+ fingers[i].update(event.getX(pointerIndex), event.getY(pointerIndex));
+ handleFingerMove(fingers[i]);
+ }
+ else {
+ handleFingerUp(fingers[i]);
+ fingers[i] = null;
+ numFingers--;
+ }
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ if (fingers[pointerId] != null) {
+ fingers[pointerId].update(event.getX(actionIndex), event.getY(actionIndex));
+ handleFingerUp(fingers[pointerId]);
+ fingers[pointerId] = null;
+ numFingers--;
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ private void handleFingerUp(Finger finger1) {
+ switch (numFingers) {
+ case 1:
+ if (finger1.isTap()) pressPointerButtonLeft(finger1);
+ break;
+ case 2:
+ Finger finger2 = findSecondFinger(finger1);
+ if (finger2 != null && finger1.isTap()) pressPointerButtonRight(finger1);
+ break;
+ case 4:
+ if (fourFingersTapCallback != null) {
+ for (byte i = 0; i < 4; i++) {
+ if (fingers[i] != null && !fingers[i].isTap()) return;
+ }
+ fourFingersTapCallback.run();
+ }
+ break;
+ }
+
+ releasePointerButtonLeft(finger1);
+ releasePointerButtonRight(finger1);
+ }
+
+ private void handleFingerMove(Finger finger1) {
+ boolean skipPointerMove = false;
+
+ Finger finger2 = numFingers == 2 ? findSecondFinger(finger1) : null;
+ if (finger2 != null) {
+ float lastDistance = (float)Math.hypot(finger1.lastX - finger2.lastX, finger1.lastY - finger2.lastY);
+ float currDistance = (float)Math.hypot(finger1.x - finger2.x, finger1.y - finger2.y);
+
+ if (currDistance < MAX_TWO_FINGERS_SCROLL_DISTANCE && Math.abs(currDistance - lastDistance) < 20) {
+ scrollAccumY += ((finger1.y + finger2.y) * 0.5f) - (finger1.lastY + finger2.lastY) * 0.5f;
+
+ if (scrollAccumY < -100) {
+ xServer.injectPointerButtonPress(Pointer.Button.BUTTON_SCROLL_DOWN);
+ xServer.injectPointerButtonRelease(Pointer.Button.BUTTON_SCROLL_DOWN);
+ scrollAccumY = 0;
+ }
+ else if (scrollAccumY > 100) {
+ xServer.injectPointerButtonPress(Pointer.Button.BUTTON_SCROLL_UP);
+ xServer.injectPointerButtonRelease(Pointer.Button.BUTTON_SCROLL_UP);
+ scrollAccumY = 0;
+ }
+ scrolling = true;
+ }
+ else if (currDistance >= MAX_TWO_FINGERS_SCROLL_DISTANCE && !xServer.pointer.isButtonPressed(Pointer.Button.BUTTON_LEFT) &&
+ finger2.travelDistance() < MAX_TAP_TRAVEL_DISTANCE) {
+ pressPointerButtonLeft(finger1);
+ skipPointerMove = true;
+ }
+ }
+
+ if (!scrolling && numFingers <= 2 && !skipPointerMove) {
+ int dx = finger1.deltaX();
+ int dy = finger1.deltaY();
+
+ if (xServer.cursorLocker.getState() == CursorLocker.State.LOCKED) {
+ WinHandler winHandler = xServer.getWinHandler();
+ winHandler.mouseEvent(MouseEventFlags.MOVE, dx, dy, 0);
+ }
+ else xServer.injectPointerMoveDelta(dx, dy);
+ }
+ }
+
+ private Finger findSecondFinger(Finger finger) {
+ for (byte i = 0; i < MAX_FINGERS; i++) {
+ if (fingers[i] != null && fingers[i] != finger) return fingers[i];
+ }
+ return null;
+ }
+
+ private void pressPointerButtonLeft(Finger finger) {
+ if (pointerButtonLeftEnabled && !xServer.pointer.isButtonPressed(Pointer.Button.BUTTON_LEFT)) {
+ xServer.injectPointerButtonPress(Pointer.Button.BUTTON_LEFT);
+ fingerPointerButtonLeft = finger;
+ }
+ }
+
+ private void pressPointerButtonRight(Finger finger) {
+ if (pointerButtonRightEnabled && !xServer.pointer.isButtonPressed(Pointer.Button.BUTTON_RIGHT)) {
+ xServer.injectPointerButtonPress(Pointer.Button.BUTTON_RIGHT);
+ fingerPointerButtonRight = finger;
+ }
+ }
+
+ private void releasePointerButtonLeft(final Finger finger) {
+ if (pointerButtonLeftEnabled && finger == fingerPointerButtonLeft && xServer.pointer.isButtonPressed(Pointer.Button.BUTTON_LEFT)) {
+ postDelayed(() -> {
+ xServer.injectPointerButtonRelease(Pointer.Button.BUTTON_LEFT);
+ fingerPointerButtonLeft = null;
+ }, 30);
+ }
+ }
+
+ private void releasePointerButtonRight(final Finger finger) {
+ if (pointerButtonRightEnabled && finger == fingerPointerButtonRight && xServer.pointer.isButtonPressed(Pointer.Button.BUTTON_RIGHT)) {
+ postDelayed(() -> {
+ xServer.injectPointerButtonRelease(Pointer.Button.BUTTON_RIGHT);
+ fingerPointerButtonRight = null;
+ }, 30);
+ }
+ }
+
+ public void setSensitivity(float sensitivity) {
+ this.sensitivity = sensitivity;
+ }
+
+ public boolean isPointerButtonLeftEnabled() {
+ return pointerButtonLeftEnabled;
+ }
+
+ public void setPointerButtonLeftEnabled(boolean pointerButtonLeftEnabled) {
+ this.pointerButtonLeftEnabled = pointerButtonLeftEnabled;
+ }
+
+ public boolean isPointerButtonRightEnabled() {
+ return pointerButtonRightEnabled;
+ }
+
+ public void setPointerButtonRightEnabled(boolean pointerButtonRightEnabled) {
+ this.pointerButtonRightEnabled = pointerButtonRightEnabled;
+ }
+
+ public void setFourFingersTapCallback(Runnable fourFingersTapCallback) {
+ this.fourFingersTapCallback = fourFingersTapCallback;
+ }
+}
diff --git a/app/src/main/java/com/winlator/widget/XServerView.java b/app/src/main/java/com/winlator/widget/XServerView.java
new file mode 100644
index 0000000..b651bc1
--- /dev/null
+++ b/app/src/main/java/com/winlator/widget/XServerView.java
@@ -0,0 +1,30 @@
+package com.winlator.widget;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.opengl.GLSurfaceView;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.winlator.renderer.GLRenderer;
+import com.winlator.xserver.XServer;
+
+@SuppressLint("ViewConstructor")
+public class XServerView extends GLSurfaceView {
+ private final GLRenderer renderer;
+
+ public XServerView(Context context, XServer xServer) {
+ super(context);
+ setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ setEGLContextClientVersion(3);
+ setEGLConfigChooser(8, 8, 8, 8, 0, 0);
+ setPreserveEGLContextOnPause(true);
+ renderer = new GLRenderer(this, xServer);
+ setRenderer(renderer);
+ setRenderMode(RENDERMODE_WHEN_DIRTY);
+ }
+
+ public GLRenderer getRenderer() {
+ return renderer;
+ }
+}
diff --git a/app/src/main/java/com/winlator/winhandler/MouseEventFlags.java b/app/src/main/java/com/winlator/winhandler/MouseEventFlags.java
new file mode 100644
index 0000000..f314d88
--- /dev/null
+++ b/app/src/main/java/com/winlator/winhandler/MouseEventFlags.java
@@ -0,0 +1,34 @@
+package com.winlator.winhandler;
+
+import com.winlator.xserver.Pointer;
+
+public abstract class MouseEventFlags {
+ public static final int MOVE = 0x0001;
+ public static final int LEFTDOWN = 0x0002;
+ public static final int LEFTUP = 0x0004;
+ public static final int RIGHTDOWN = 0x0008;
+ public static final int RIGHTUP = 0x0010;
+ public static final int MIDDLEDOWN = 0x0020;
+ public static final int MIDDLEUP = 0x0040;
+ public static final int XDOWN = 0x0080;
+ public static final int XUP = 0x0100;
+ public static final int WHEEL = 0x0800;
+ public static final int VIRTUALDESK = 0x4000;
+ public static final int ABSOLUTE = 0x8000;
+
+ public static int getFlagFor(Pointer.Button button, boolean isActionDown) {
+ switch (button) {
+ case BUTTON_LEFT:
+ return isActionDown ? MouseEventFlags.LEFTDOWN : MouseEventFlags.LEFTUP;
+ case BUTTON_MIDDLE:
+ return isActionDown ? MouseEventFlags.MIDDLEDOWN : MouseEventFlags.MIDDLEUP;
+ case BUTTON_RIGHT:
+ return isActionDown ? MouseEventFlags.RIGHTDOWN : MouseEventFlags.RIGHTUP;
+ case BUTTON_SCROLL_DOWN:
+ case BUTTON_SCROLL_UP:
+ return MouseEventFlags.WHEEL;
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/winhandler/ProcessInfo.java b/app/src/main/java/com/winlator/winhandler/ProcessInfo.java
new file mode 100644
index 0000000..95c4e6a
--- /dev/null
+++ b/app/src/main/java/com/winlator/winhandler/ProcessInfo.java
@@ -0,0 +1,32 @@
+package com.winlator.winhandler;
+
+import com.winlator.core.StringUtils;
+
+import java.util.ArrayList;
+
+public class ProcessInfo {
+ public final int pid;
+ public final String name;
+ public final long memoryUsage;
+ public final int affinityMask;
+
+ public ProcessInfo(int pid, String name, long memoryUsage, int affinityMask) {
+ this.pid = pid;
+ this.name = name;
+ this.memoryUsage = memoryUsage;
+ this.affinityMask = affinityMask;
+ }
+
+ public String getFormattedMemoryUsage() {
+ return StringUtils.formatBytes(memoryUsage);
+ }
+
+ public String getCPUList() {
+ int numProcessors = Runtime.getRuntime().availableProcessors();
+ ArrayList cpuList = new ArrayList<>();
+ for (byte i = 0; i < numProcessors; i++) {
+ if ((affinityMask & (1 << i)) != 0) cpuList.add(String.valueOf(i));
+ }
+ return String.join(",", cpuList.toArray(new String[0]));
+ }
+}
diff --git a/app/src/main/java/com/winlator/winhandler/RequestCodes.java b/app/src/main/java/com/winlator/winhandler/RequestCodes.java
new file mode 100644
index 0000000..5dabb48
--- /dev/null
+++ b/app/src/main/java/com/winlator/winhandler/RequestCodes.java
@@ -0,0 +1,10 @@
+package com.winlator.winhandler;
+
+abstract class RequestCodes {
+ public static final byte EXIT = 0;
+ public static final byte EXEC = 1;
+ public static final byte KILL_PROCESS = 2;
+ public static final byte GET_PROCESSES = 3;
+ public static final byte SET_PROCESS_AFFINITY = 4;
+ public static final byte MOUSE_EVENT = 5;
+}
diff --git a/app/src/main/java/com/winlator/winhandler/TaskManagerDialog.java b/app/src/main/java/com/winlator/winhandler/TaskManagerDialog.java
new file mode 100644
index 0000000..bc21432
--- /dev/null
+++ b/app/src/main/java/com/winlator/winhandler/TaskManagerDialog.java
@@ -0,0 +1,132 @@
+package com.winlator.winhandler;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.os.Build;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import com.winlator.R;
+import com.winlator.XServerDisplayActivity;
+import com.winlator.core.ProcessHelper;
+import com.winlator.widget.CPUListView;
+import com.winlator.contentdialog.ContentDialog;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class TaskManagerDialog extends ContentDialog {
+ private final XServerDisplayActivity activity;
+ private final LayoutInflater inflater;
+ private Timer timer;
+
+ public TaskManagerDialog(XServerDisplayActivity activity) {
+ super(activity, R.layout.task_manager_dialog);
+ this.activity = activity;
+ setCancelable(false);
+ setTitle(R.string.task_manager);
+ setIcon(R.drawable.icon_task_manager);
+
+ Button cancelButton = findViewById(R.id.BTCancel);
+ cancelButton.setText(R.string.new_task);
+ cancelButton.setOnClickListener((v) -> {
+ dismiss();
+ ContentDialog.prompt(activity, R.string.new_task, "taskmgr.exe", (command) -> activity.getWinHandler().exec(command));
+ });
+
+ setOnDismissListener((dialog) -> {
+ if (timer != null) {
+ timer.cancel();
+ timer = null;
+ }
+ });
+
+ inflater = LayoutInflater.from(activity);
+ }
+
+ private void update() {
+ WinHandler winHandler = activity.getWinHandler();
+ winHandler.getProcesses((processes) -> activity.runOnUiThread(() -> {
+ final LinearLayout container = findViewById(R.id.LLProcessList);
+ container.removeAllViews();
+ setBottomBarText(activity.getString(R.string.processes)+": "+processes.size());
+
+ if (!processes.isEmpty()) {
+ findViewById(R.id.TVEmptyText).setVisibility(View.GONE);
+
+ for (final ProcessInfo processInfo : processes) {
+ View itemView = inflater.inflate(R.layout.process_info_list_item, container, false);
+ ((TextView)itemView.findViewById(R.id.TVName)).setText(processInfo.name);
+ ((TextView)itemView.findViewById(R.id.TVPID)).setText(String.valueOf(processInfo.pid));
+ ((TextView)itemView.findViewById(R.id.TVMemoryUsage)).setText(processInfo.getFormattedMemoryUsage());
+ itemView.findViewById(R.id.BTMenu).setOnClickListener((v) -> showListItemMenu(v, processInfo));
+ container.addView(itemView);
+ }
+ }
+ else findViewById(R.id.TVEmptyText).setVisibility(View.VISIBLE);
+ }));
+
+ LinearLayout tableHead = findViewById(R.id.LLTableHead);
+ TextView tvMemoryInfo = (TextView)tableHead.getChildAt(2);
+ tvMemoryInfo.setText(activity.getString(R.string.memory)+" ("+ getMemoryUsagePercent()+"%)");
+ }
+
+ private void showListItemMenu(final View anchorView, final ProcessInfo processInfo) {
+ PopupMenu listItemMenu = new PopupMenu(activity, anchorView);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) listItemMenu.setForceShowIcon(true);
+
+ listItemMenu.inflate(R.menu.process_popup_menu);
+ listItemMenu.setOnMenuItemClickListener((menuItem) -> {
+ int itemId = menuItem.getItemId();
+ if (itemId == R.id.process_affinity) {
+ showProcessorAffinityDialog(processInfo);
+ }
+ else if (itemId == R.id.process_end) {
+ ContentDialog.confirm(activity, R.string.do_you_want_to_end_this_process, () -> {
+ activity.getWinHandler().killProcess(processInfo.name);
+ });
+ }
+ return true;
+ });
+ listItemMenu.show();
+ }
+
+ private void showProcessorAffinityDialog(final ProcessInfo processInfo) {
+ ContentDialog dialog = new ContentDialog(activity, R.layout.cpu_list_dialog);
+ dialog.setTitle(processInfo.name);
+ dialog.setIcon(R.drawable.icon_cpu);
+ final CPUListView cpuListView = dialog.findViewById(R.id.CPUListView);
+ cpuListView.setCheckedCPUList(processInfo.getCPUList());
+ dialog.setOnConfirmCallback(() -> {
+ WinHandler winHandler = activity.getWinHandler();
+ winHandler.setProcessAffinity(processInfo.pid, ProcessHelper.getAffinityMask(cpuListView.getCheckedCPUList()));
+ update();
+ });
+ dialog.show();
+ }
+
+ private byte getMemoryUsagePercent() {
+ ActivityManager activityManager = (ActivityManager)activity.getSystemService(Context.ACTIVITY_SERVICE);
+ ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
+ activityManager.getMemoryInfo(memoryInfo);
+ long usedMem = memoryInfo.totalMem - memoryInfo.availMem;
+ return (byte)(((double)usedMem / memoryInfo.totalMem) * 100.0f);
+ }
+
+ @Override
+ public void show() {
+ update();
+ timer = new Timer();
+ timer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ update();
+ }
+ }, 0, 1000);
+ super.show();
+ }
+}
diff --git a/app/src/main/java/com/winlator/winhandler/WinHandler.java b/app/src/main/java/com/winlator/winhandler/WinHandler.java
new file mode 100644
index 0000000..db1c234
--- /dev/null
+++ b/app/src/main/java/com/winlator/winhandler/WinHandler.java
@@ -0,0 +1,194 @@
+package com.winlator.winhandler;
+
+import com.winlator.core.Callback;
+import com.winlator.core.StringUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executors;
+
+public class WinHandler {
+ private static final short DEFAULT_PORT = 7946;
+ private Socket socket;
+ private final Object lock = new Object();
+ private ByteBuffer data = ByteBuffer.allocate(128).order(ByteOrder.LITTLE_ENDIAN);
+ private final ArrayDeque actions = new ArrayDeque<>();
+
+ private boolean sendData() {
+ try {
+ int size = data.position();
+ if (size == 0) return true;
+ OutputStream outStream = socket.getOutputStream();
+ outStream.write(data.array(), 0, size);
+ return true;
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+
+ private boolean receiveData(int length) {
+ try {
+ while (length > data.capacity()) {
+ data = ByteBuffer.allocate(data.capacity() * 2).order(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ data.rewind();
+ InputStream inStream = socket.getInputStream();
+ int bytesRead = inStream.read(data.array(), 0, length);
+ if (bytesRead > 0) {
+ data.limit(length);
+ return true;
+ }
+ }
+ catch (IOException e) {}
+ return false;
+ }
+
+ public void exec(String command) {
+ command = command.trim();
+ if (command.isEmpty()) return;
+ String[] cmdList = command.split(" ", 2);
+ final String filename = cmdList[0];
+ final String parameters = cmdList.length > 1 ? cmdList[1] : "";
+
+ synchronized (lock) {
+ actions.add(() -> {
+ byte[] filenameBytes = filename.getBytes();
+ byte[] parametersBytes = parameters.getBytes();
+
+ data.rewind();
+ data.put(RequestCodes.EXEC);
+ data.putInt(filenameBytes.length + parametersBytes.length + 8);
+ data.putInt(filenameBytes.length);
+ data.putInt(parametersBytes.length);
+ data.put(filenameBytes);
+ data.put(parametersBytes);
+ sendData();
+ });
+ }
+ }
+
+ public void killProcess(final String processName) {
+ synchronized (lock) {
+ actions.add(() -> {
+ data.rewind();
+ data.put(RequestCodes.KILL_PROCESS);
+ byte[] bytes = processName.getBytes();
+ data.putInt(bytes.length);
+ data.put(bytes);
+ sendData();
+ });
+ }
+ }
+
+ public void getProcesses(final Callback> callback) {
+ synchronized (lock) {
+ actions.add(() -> {
+ data.rewind();
+ data.put(RequestCodes.GET_PROCESSES);
+ data.putInt(0);
+
+ if (sendData() && receiveData(5)) {
+ boolean success = data.get() == 1;
+ int responseLength = data.getInt();
+ if (success && receiveData(responseLength)) {
+ ArrayList processes = new ArrayList<>();
+ while (data.position() < data.limit()) {
+ int numBytesBefore = data.position();
+ int pid = data.getInt();
+ long memoryUsage = data.getLong();
+ int affinityMask = data.getInt();
+
+ byte[] bytes = new byte[32];
+ data.get(bytes);
+ String name = StringUtils.fromANSIString(bytes);
+
+ processes.add(new ProcessInfo(pid, name, memoryUsage, affinityMask));
+
+ int bytesRead = data.position() - numBytesBefore;
+ data.position(data.position() + (64 - bytesRead));
+ }
+ callback.call(processes);
+ }
+ else callback.call(Collections.emptyList());
+ }
+ else callback.call(Collections.emptyList());
+ });
+ }
+ }
+
+ public void setProcessAffinity(final int pid, final int affinityMask) {
+ synchronized (lock) {
+ actions.add(() -> {
+ data.rewind();
+ data.put(RequestCodes.SET_PROCESS_AFFINITY);
+ data.putInt(8);
+ data.putInt(pid);
+ data.putInt(affinityMask);
+ sendData();
+ });
+ }
+ }
+
+ public void mouseEvent(int flags, int dx, int dy, int wheelDelta) {
+ synchronized (lock) {
+ if (!isConnected()) return;
+ actions.add(() -> {
+ data.rewind();
+ data.put(RequestCodes.MOUSE_EVENT);
+ data.putInt(10);
+ data.putInt(flags);
+ data.putShort((short)dx);
+ data.putShort((short)dy);
+ data.putShort((short)wheelDelta);
+ sendData();
+ });
+ }
+ }
+
+ public boolean isConnected() {
+ return socket != null && !socket.isClosed() && socket.isConnected();
+ }
+
+ private void connect() {
+ InetAddress address = null;
+ try {
+ address = InetAddress.getLocalHost();
+ }
+ catch (UnknownHostException e) {}
+
+ try {
+ socket = new Socket(address.getHostAddress(), DEFAULT_PORT);
+ }
+ catch (IOException e) {}
+ }
+
+ public void start() {
+ Executors.newSingleThreadExecutor().execute(() -> {
+ while (true) {
+ synchronized (lock) {
+ if (!isConnected()) connect();
+ if (isConnected()) while (!actions.isEmpty()) actions.poll().run();
+ }
+
+ if (!isConnected()) {
+ try {
+ Thread.sleep(100);
+ }
+ catch (InterruptedException e) {}
+ }
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/com/winlator/xconnector/Client.java b/app/src/main/java/com/winlator/xconnector/Client.java
new file mode 100644
index 0000000..c303b4d
--- /dev/null
+++ b/app/src/main/java/com/winlator/xconnector/Client.java
@@ -0,0 +1,54 @@
+package com.winlator.xconnector;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class Client {
+ public final ClientSocket clientSocket;
+ private final XConnectorEpoll connector;
+ private XInputStream inputStream;
+ private XOutputStream outputStream;
+ private Object tag;
+ protected Thread pollThread;
+ protected int shutdownFd;
+ protected boolean connected;
+
+ public Client(XConnectorEpoll connector, ClientSocket clientSocket) {
+ this.connector = connector;
+ this.clientSocket = clientSocket;
+ }
+
+ public void createIOStreams() {
+ if (inputStream != null || outputStream != null) return;
+ inputStream = new XInputStream(clientSocket, connector.getInitialInputBufferCapacity());
+ outputStream = new XOutputStream(clientSocket, connector.getInitialOutputBufferCapacity());
+ inputStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ outputStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ public XInputStream getInputStream() {
+ return inputStream;
+ }
+
+ public XOutputStream getOutputStream() {
+ return outputStream;
+ }
+
+ public Object getTag() {
+ return tag;
+ }
+
+ public void setTag(Object tag) {
+ this.tag = tag;
+ }
+
+ protected void requestShutdown() {
+ try {
+ ByteBuffer data = ByteBuffer.allocateDirect(8);
+ data.asLongBuffer().put(1);
+ (new ClientSocket(shutdownFd)).write(data);
+ }
+ catch (IOException e) {}
+ }
+}
diff --git a/app/src/main/java/com/winlator/xconnector/ClientSocket.java b/app/src/main/java/com/winlator/xconnector/ClientSocket.java
new file mode 100644
index 0000000..74e9330
--- /dev/null
+++ b/app/src/main/java/com/winlator/xconnector/ClientSocket.java
@@ -0,0 +1,83 @@
+package com.winlator.xconnector;
+
+import androidx.annotation.Keep;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+
+public class ClientSocket {
+ public final int fd;
+ private final ArrayDeque ancillaryFds = new ArrayDeque<>();
+
+ static {
+ System.loadLibrary("winlator");
+ }
+
+ public ClientSocket(int fd) {
+ this.fd = fd;
+ }
+
+ public boolean hasAncillaryFds() {
+ return !ancillaryFds.isEmpty();
+ }
+
+ public int getAncillaryFd() {
+ return hasAncillaryFds() ? ancillaryFds.poll() : -1;
+ }
+
+ @Keep
+ public void addAncillaryFd(int ancillaryFd) {
+ ancillaryFds.add(ancillaryFd);
+ }
+
+ public int read(ByteBuffer data) throws IOException {
+ int position = data.position();
+ int bytesRead = read(fd, data, position, data.remaining());
+ if (bytesRead > 0) {
+ data.position(position + bytesRead);
+ return bytesRead;
+ }
+ else if (bytesRead == 0) {
+ return -1;
+ }
+ else throw new IOException("Failed to read data.");
+ }
+
+ public void write(ByteBuffer data) throws IOException {
+ int bytesWritten = write(fd, data, data.limit());
+ if (bytesWritten >= 0) {
+ data.position(bytesWritten);
+ }
+ else throw new IOException("Failed to write data.");
+ }
+
+ public int recvAncillaryMsg(ByteBuffer data) throws IOException {
+ int position = data.position();
+ int bytesRead = recvAncillaryMsg(fd, data, position, data.remaining());
+ if (bytesRead > 0) {
+ data.position(position + bytesRead);
+ return bytesRead;
+ }
+ else if (bytesRead == 0) {
+ return -1;
+ }
+ else throw new IOException("Failed to receive ancillary messages.");
+ }
+
+ public void sendAncillaryMsg(ByteBuffer data, int ancillaryFd) throws IOException {
+ int bytesSent = sendAncillaryMsg(fd, data, data.limit(), ancillaryFd);
+ if (bytesSent >= 0) {
+ data.position(bytesSent);
+ }
+ else throw new IOException("Failed to send ancillary messages.");
+ }
+
+ private native int read(int fd, ByteBuffer data, int offset, int length);
+
+ private native int write(int fd, ByteBuffer data, int length);
+
+ private native int recvAncillaryMsg(int clientFd, ByteBuffer data, int offset, int length);
+
+ private native int sendAncillaryMsg(int clientFd, ByteBuffer data, int length, int ancillaryFd);
+}
diff --git a/app/src/main/java/com/winlator/xconnector/ConnectionHandler.java b/app/src/main/java/com/winlator/xconnector/ConnectionHandler.java
new file mode 100644
index 0000000..3e2d6ea
--- /dev/null
+++ b/app/src/main/java/com/winlator/xconnector/ConnectionHandler.java
@@ -0,0 +1,7 @@
+package com.winlator.xconnector;
+
+public interface ConnectionHandler {
+ void handleConnectionShutdown(Client client);
+
+ void handleNewConnection(Client client);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xconnector/RequestHandler.java b/app/src/main/java/com/winlator/xconnector/RequestHandler.java
new file mode 100644
index 0000000..936957b
--- /dev/null
+++ b/app/src/main/java/com/winlator/xconnector/RequestHandler.java
@@ -0,0 +1,7 @@
+package com.winlator.xconnector;
+
+import java.io.IOException;
+
+public interface RequestHandler {
+ boolean handleRequest(Client client) throws IOException;
+}
diff --git a/app/src/main/java/com/winlator/xconnector/UnixSocketConfig.java b/app/src/main/java/com/winlator/xconnector/UnixSocketConfig.java
new file mode 100644
index 0000000..728fea2
--- /dev/null
+++ b/app/src/main/java/com/winlator/xconnector/UnixSocketConfig.java
@@ -0,0 +1,32 @@
+package com.winlator.xconnector;
+
+import com.winlator.core.FileUtils;
+
+import java.io.File;
+
+public class UnixSocketConfig {
+ public static final String SYSVSHM_SERVER_PATH = "/tmp/.sysvshm/SM0";
+ public static final String ALSA_SERVER_PATH = "/tmp/.sound/AS0";
+ public static final String PULSE_SERVER_PATH = "/tmp/.sound/PS0";
+ public static final String XSERVER_PATH = "/tmp/.X11-unix/X0";
+ public static final String VIRGL_SERVER_PATH = "/tmp/.virgl/V0";
+ public final String path;
+
+ private UnixSocketConfig(String path) {
+ this.path = path;
+ }
+
+ public static UnixSocketConfig createSocket(String rootPath, String relativePath) {
+ File socketFile = new File(rootPath, relativePath);
+
+ String dirname = FileUtils.getDirname(relativePath);
+ if (dirname.lastIndexOf("/") > 0) {
+ File socketDir = new File(rootPath, FileUtils.getDirname(relativePath));
+ FileUtils.delete(socketDir);
+ socketDir.mkdirs();
+ }
+ else socketFile.delete();
+
+ return new UnixSocketConfig(socketFile.getPath());
+ }
+}
diff --git a/app/src/main/java/com/winlator/xconnector/XConnectorEpoll.java b/app/src/main/java/com/winlator/xconnector/XConnectorEpoll.java
new file mode 100644
index 0000000..fd4990d
--- /dev/null
+++ b/app/src/main/java/com/winlator/xconnector/XConnectorEpoll.java
@@ -0,0 +1,227 @@
+package com.winlator.xconnector;
+
+import android.util.SparseArray;
+
+import androidx.annotation.Keep;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+public class XConnectorEpoll implements Runnable {
+ private final ConnectionHandler connectionHandler;
+ private final RequestHandler requestHandler;
+ private final int epollFd;
+ private final int serverFd;
+ private final int shutdownFd;
+ private Thread epollThread;
+ private boolean running = false;
+ private boolean multithreadedClients = false;
+ private boolean canReceiveAncillaryMessages = false;
+ private int initialInputBufferCapacity = 4096;
+ private int initialOutputBufferCapacity = 4096;
+ private final SparseArray connectedClients = new SparseArray<>();
+
+ static {
+ System.loadLibrary("winlator");
+ }
+
+ public XConnectorEpoll(UnixSocketConfig socketConfig, ConnectionHandler connectionHandler, RequestHandler requestHandler) {
+ this.connectionHandler = connectionHandler;
+ this.requestHandler = requestHandler;
+
+ serverFd = createAFUnixSocket(socketConfig.path);
+ if (serverFd < 0) {
+ throw new RuntimeException("Failed to create an AF_UNIX socket.");
+ }
+
+ epollFd = createEpollFd();
+ if (epollFd < 0) {
+ closeFd(serverFd);
+ throw new RuntimeException("Failed to create epoll fd.");
+ }
+
+ if (!addFdToEpoll(epollFd, serverFd)) {
+ closeFd(serverFd);
+ closeFd(epollFd);
+ throw new RuntimeException("Failed to add server fd to epoll.");
+ }
+
+ shutdownFd = createEventFd();
+ if (!addFdToEpoll(epollFd, shutdownFd)) {
+ closeFd(serverFd);
+ closeFd(shutdownFd);
+ closeFd(epollFd);
+ throw new RuntimeException("Failed to add shutdown fd to epoll.");
+ }
+
+ epollThread = new Thread(this);
+ }
+
+ public synchronized void start() {
+ if (running || epollThread == null) return;
+ running = true;
+ epollThread.start();
+ }
+
+ public synchronized void stop() {
+ if (!running || epollThread == null) return;
+ running = false;
+ requestShutdown();
+
+ while (epollThread.isAlive()) {
+ try {
+ epollThread.join();
+ }
+ catch (InterruptedException e) {}
+ }
+ epollThread = null;
+ }
+
+ @Override
+ public void run() {
+ while (running && doEpollIndefinitely(epollFd, serverFd, !multithreadedClients));
+ shutdown();
+ }
+
+ @Keep
+ private void handleNewConnection(int fd) {
+ final Client client = new Client(this, new ClientSocket(fd));
+ client.connected = true;
+ if (multithreadedClients) {
+ client.shutdownFd = createEventFd();
+ client.pollThread = new Thread(() -> {
+ connectionHandler.handleNewConnection(client);
+ while (client.connected && waitForSocketRead(client.clientSocket.fd, client.shutdownFd));
+ });
+ client.pollThread.start();
+ }
+ else connectionHandler.handleNewConnection(client);
+ connectedClients.put(fd, client);
+ }
+
+ @Keep
+ private void handleExistingConnection(int fd) {
+ Client client = connectedClients.get(fd);
+ if (client == null) return;
+
+ XInputStream inputStream = client.getInputStream();
+ try {
+ if (inputStream != null) {
+ if (inputStream.readMoreData(canReceiveAncillaryMessages) > 0) {
+ int activePosition = 0;
+ while (running && requestHandler.handleRequest(client)) activePosition = inputStream.getActivePosition();
+ inputStream.setActivePosition(activePosition);
+ }
+ else killConnection(client);
+ }
+ else requestHandler.handleRequest(client);
+ }
+ catch (IOException e) {
+ killConnection(client);
+ }
+ }
+
+ public Client getClient(int fd) {
+ return connectedClients.get(fd);
+ }
+
+ public ArrayList getConnectedClients() {
+ ArrayList clients = new ArrayList<>();
+ for (int i = 0; i < connectedClients.size(); i++) clients.add(connectedClients.valueAt(i));
+ return clients;
+ }
+
+ public void killConnection(Client client) {
+ client.connected = false;
+ connectionHandler.handleConnectionShutdown(client);
+ if (multithreadedClients) {
+ if (Thread.currentThread() != client.pollThread) {
+ client.requestShutdown();
+
+ while (client.pollThread.isAlive()) {
+ try {
+ client.pollThread.join();
+ }
+ catch (InterruptedException e) {}
+ }
+
+ client.pollThread = null;
+ }
+ closeFd(client.shutdownFd);
+ }
+ else removeFdFromEpoll(epollFd, client.clientSocket.fd);
+ closeFd(client.clientSocket.fd);
+ connectedClients.remove(client.clientSocket.fd);
+ }
+
+ private void shutdown() {
+ while (connectedClients.size() > 0) {
+ Client client = connectedClients.valueAt(connectedClients.size()-1);
+ killConnection(client);
+ }
+
+ removeFdFromEpoll(epollFd, serverFd);
+ removeFdFromEpoll(epollFd, shutdownFd);
+ closeFd(serverFd);
+ closeFd(shutdownFd);
+ closeFd(epollFd);
+ }
+
+ public int getInitialInputBufferCapacity() {
+ return initialInputBufferCapacity;
+ }
+
+ public void setInitialInputBufferCapacity(int initialInputBufferCapacity) {
+ this.initialInputBufferCapacity = initialInputBufferCapacity;
+ }
+
+ public int getInitialOutputBufferCapacity() {
+ return initialOutputBufferCapacity;
+ }
+
+ public void setInitialOutputBufferCapacity(int initialOutputBufferCapacity) {
+ this.initialOutputBufferCapacity = initialOutputBufferCapacity;
+ }
+
+ public boolean isMultithreadedClients() {
+ return multithreadedClients;
+ }
+
+ public void setMultithreadedClients(boolean multithreadedClients) {
+ this.multithreadedClients = multithreadedClients;
+ }
+
+ public boolean isCanReceiveAncillaryMessages() {
+ return canReceiveAncillaryMessages;
+ }
+
+ public void setCanReceiveAncillaryMessages(boolean canReceiveAncillaryMessages) {
+ this.canReceiveAncillaryMessages = canReceiveAncillaryMessages;
+ }
+
+ private void requestShutdown() {
+ try {
+ ByteBuffer data = ByteBuffer.allocateDirect(8);
+ data.asLongBuffer().put(1);
+ (new ClientSocket(shutdownFd)).write(data);
+ }
+ catch (IOException e) {}
+ }
+
+ public static native void closeFd(int fd);
+
+ private native int createEpollFd();
+
+ private native int createEventFd();
+
+ private native boolean doEpollIndefinitely(int epollFd, int serverFd, boolean addClientToEpoll);
+
+ private native boolean addFdToEpoll(int epollFd, int fd);
+
+ private native void removeFdFromEpoll(int epollFd, int fd);
+
+ private native boolean waitForSocketRead(int clientFd, int shutdownFd);
+
+ private native int createAFUnixSocket(String path);
+}
diff --git a/app/src/main/java/com/winlator/xconnector/XInputStream.java b/app/src/main/java/com/winlator/xconnector/XInputStream.java
new file mode 100644
index 0000000..6a108a9
--- /dev/null
+++ b/app/src/main/java/com/winlator/xconnector/XInputStream.java
@@ -0,0 +1,128 @@
+package com.winlator.xconnector;
+
+import com.winlator.xserver.XServer;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class XInputStream {
+ private ByteBuffer activeBuffer;
+ private ByteBuffer buffer;
+ public final ClientSocket clientSocket;
+
+ public XInputStream(int initialCapacity) {
+ this(null, initialCapacity);
+ }
+
+ public XInputStream(ClientSocket clientSocket, int initialCapacity) {
+ this.clientSocket = clientSocket;
+ this.buffer = ByteBuffer.allocateDirect(initialCapacity);
+ }
+
+ public int readMoreData(boolean canReceiveAncillaryMessages) throws IOException {
+ if (activeBuffer != null) {
+ if (!activeBuffer.hasRemaining()) {
+ buffer.clear();
+ }
+ else if (activeBuffer.position() > 0) {
+ int newLimit = buffer.position();
+ buffer.position(activeBuffer.position()).limit(newLimit);
+ buffer.compact();
+ }
+ activeBuffer = null;
+ }
+
+ growInputBufferIfNecessary();
+ int bytesRead = canReceiveAncillaryMessages ? clientSocket.recvAncillaryMsg(buffer) : clientSocket.read(buffer);
+
+ if (bytesRead > 0) {
+ int position = buffer.position();
+ buffer.flip();
+ activeBuffer = buffer.slice().order(buffer.order());
+ buffer.limit(buffer.capacity()).position(position);
+ }
+ return bytesRead;
+ }
+
+ public int getAncillaryFd() {
+ return clientSocket.getAncillaryFd();
+ }
+
+ private void growInputBufferIfNecessary() {
+ if (buffer.position() == buffer.capacity()) {
+ ByteBuffer newBuffer = ByteBuffer.allocateDirect(buffer.capacity() * 2).order(buffer.order());
+ buffer.rewind();
+ newBuffer.put(buffer);
+ buffer = newBuffer;
+ }
+ }
+
+ public void setByteOrder(ByteOrder byteOrder) {
+ buffer.order(byteOrder);
+ if (activeBuffer != null) activeBuffer.order(byteOrder);
+ }
+
+ public int getActivePosition() {
+ return activeBuffer.position();
+ }
+
+ public void setActivePosition(int activePosition) {
+ activeBuffer.position(activePosition);
+ }
+
+ public int available() {
+ return activeBuffer.remaining();
+ }
+
+ public byte readByte() {
+ return activeBuffer.get();
+ }
+
+ public int readUnsignedByte() {
+ return Byte.toUnsignedInt(activeBuffer.get());
+ }
+
+ public short readShort() {
+ return activeBuffer.getShort();
+ }
+
+ public int readUnsignedShort() {
+ return Short.toUnsignedInt(activeBuffer.getShort());
+ }
+
+ public int readInt() {
+ return activeBuffer.getInt();
+ }
+
+ public long readUnsignedInt() {
+ return Integer.toUnsignedLong(activeBuffer.getInt());
+ }
+
+ public long readLong() {
+ return activeBuffer.getLong();
+ }
+
+ public void read(byte[] result) {
+ activeBuffer.get(result);
+ }
+
+ public ByteBuffer readByteBuffer(int length) {
+ ByteBuffer newBuffer = activeBuffer.slice().order(activeBuffer.order());
+ newBuffer.limit(length);
+ activeBuffer.position(activeBuffer.position() + length);
+ return newBuffer;
+ }
+
+ public String readString8(int length) {
+ byte[] bytes = new byte[length];
+ read(bytes);
+ String str = new String(bytes, XServer.LATIN1_CHARSET);
+ if ((-length & 3) > 0) skip(-length & 3);
+ return str;
+ }
+
+ public void skip(int length) {
+ activeBuffer.position(activeBuffer.position() + length);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xconnector/XOutputStream.java b/app/src/main/java/com/winlator/xconnector/XOutputStream.java
new file mode 100644
index 0000000..042c7e0
--- /dev/null
+++ b/app/src/main/java/com/winlator/xconnector/XOutputStream.java
@@ -0,0 +1,122 @@
+package com.winlator.xconnector;
+
+import com.winlator.xserver.XServer;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class XOutputStream {
+ private static final byte[] ZERO = new byte[64];
+ private ByteBuffer buffer;
+ public final ClientSocket clientSocket;
+ private final ReentrantLock lock = new ReentrantLock();
+ private int ancillaryFd = -1;
+
+ public XOutputStream(int initialCapacity) {
+ this(null, initialCapacity);
+ }
+
+ public XOutputStream(ClientSocket clientSocket, int initialCapacity) {
+ this.clientSocket = clientSocket;
+ buffer = ByteBuffer.allocateDirect(initialCapacity);
+ }
+
+ public void setByteOrder(ByteOrder byteOrder) {
+ buffer.order(byteOrder);
+ }
+
+ public void setAncillaryFd(int ancillaryFd) {
+ this.ancillaryFd = ancillaryFd;
+ }
+
+ public void writeByte(byte value) {
+ ensureSpaceIsAvailable(1);
+ buffer.put(value);
+ }
+
+ public void writeShort(short value) {
+ ensureSpaceIsAvailable(2);
+ buffer.putShort(value);
+ }
+
+ public void writeInt(int value) {
+ ensureSpaceIsAvailable(4);
+ buffer.putInt(value);
+ }
+
+ public void writeLong(long value) {
+ ensureSpaceIsAvailable(8);
+ buffer.putLong(value);
+ }
+
+ public void writeString8(String str) {
+ byte[] bytes = str.getBytes(XServer.LATIN1_CHARSET);
+ int length = -str.length() & 3;
+ ensureSpaceIsAvailable(bytes.length + length);
+ buffer.put(bytes);
+ if (length > 0) writePad(length);
+ }
+
+ public void write(byte[] data) {
+ write(data, 0, data.length);
+ }
+
+ public void write(byte[] data, int offset, int length) {
+ ensureSpaceIsAvailable(length);
+ buffer.put(data, offset, length);
+ }
+
+ public void write(ByteBuffer data) {
+ ensureSpaceIsAvailable(data.remaining());
+ buffer.put(data);
+ }
+
+ public void writePad(int length) {
+ write(ZERO, 0, length);
+ }
+
+ private void flush() throws IOException {
+ if (buffer.position() != 0) {
+ buffer.flip();
+
+ if (ancillaryFd != -1) {
+ clientSocket.sendAncillaryMsg(buffer, ancillaryFd);
+ ancillaryFd = -1;
+ }
+ else clientSocket.write(buffer);
+
+ buffer.clear();
+ }
+ }
+
+ public XStreamLock lock() {
+ return new OutputStreamLock();
+ }
+
+ private void ensureSpaceIsAvailable(int length) {
+ int position = buffer.position();
+ if ((buffer.capacity() - position) >= length) return;
+ ByteBuffer newBuffer = ByteBuffer.allocateDirect(buffer.capacity() + length).order(buffer.order());
+ buffer.rewind();
+ newBuffer.put(buffer).position(position);
+ buffer = newBuffer;
+ }
+
+ private class OutputStreamLock implements XStreamLock {
+ public OutputStreamLock() {
+ lock.lock();
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ flush();
+ }
+ finally {
+ lock.unlock();
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xconnector/XStreamLock.java b/app/src/main/java/com/winlator/xconnector/XStreamLock.java
new file mode 100644
index 0000000..4c39734
--- /dev/null
+++ b/app/src/main/java/com/winlator/xconnector/XStreamLock.java
@@ -0,0 +1,7 @@
+package com.winlator.xconnector;
+
+import java.io.IOException;
+
+public interface XStreamLock extends AutoCloseable {
+ void close() throws IOException;
+}
diff --git a/app/src/main/java/com/winlator/xenvironment/EnvironmentComponent.java b/app/src/main/java/com/winlator/xenvironment/EnvironmentComponent.java
new file mode 100644
index 0000000..9d374bb
--- /dev/null
+++ b/app/src/main/java/com/winlator/xenvironment/EnvironmentComponent.java
@@ -0,0 +1,9 @@
+package com.winlator.xenvironment;
+
+public abstract class EnvironmentComponent {
+ protected XEnvironment environment;
+
+ public abstract void start();
+
+ public abstract void stop();
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xenvironment/ImageFs.java b/app/src/main/java/com/winlator/xenvironment/ImageFs.java
new file mode 100644
index 0000000..5626a2f
--- /dev/null
+++ b/app/src/main/java/com/winlator/xenvironment/ImageFs.java
@@ -0,0 +1,83 @@
+package com.winlator.xenvironment;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.winlator.core.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Locale;
+
+public class ImageFs {
+ public static final String USER = "xuser";
+ public static final String HOME_PATH = "/home/"+USER;
+ public static final String CACHE_PATH = "/home/"+USER+"/.cache";
+ public static final String WINEPREFIX = "/home/"+USER+"/.wine";
+ private final File rootDir;
+ private String winePath = "/opt/wine";
+
+ private ImageFs(File rootDir) {
+ this.rootDir = rootDir;
+ }
+
+ public static ImageFs find(Context context) {
+ return new ImageFs(new File(context.getFilesDir(), "imagefs"));
+ }
+
+ public File getRootDir() {
+ return rootDir;
+ }
+
+ public boolean isValid() {
+ return rootDir.isDirectory() && getImgVersionFile().exists();
+ }
+
+ public int getVersion() {
+ File imgVersionFile = getImgVersionFile();
+ return imgVersionFile.exists() ? Integer.parseInt(FileUtils.readLines(imgVersionFile).get(0)) : 0;
+ }
+
+ public String getFormattedVersion() {
+ return String.format(Locale.ENGLISH, "%.1f", (float)getVersion());
+ }
+
+ public void createImgVersionFile(int version) {
+ getConfigDir().mkdirs();
+ File file = getImgVersionFile();
+ try {
+ file.createNewFile();
+ FileUtils.writeString(file, String.valueOf(version));
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public String getWinePath() {
+ return winePath;
+ }
+
+ public void setWinePath(String winePath) {
+ this.winePath = FileUtils.toRelativePath(rootDir.getPath(), winePath);
+ }
+
+ public File getConfigDir() {
+ return new File(rootDir, ".winlator");
+ }
+
+ public File getImgVersionFile() {
+ return new File(getConfigDir(), ".img_version");
+ }
+
+ public File getInstalledWineDir() {
+ return new File(rootDir, "/opt/installed-wine");
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return rootDir.getPath();
+ }
+}
diff --git a/app/src/main/java/com/winlator/xenvironment/XEnvironment.java b/app/src/main/java/com/winlator/xenvironment/XEnvironment.java
new file mode 100644
index 0000000..b985db8
--- /dev/null
+++ b/app/src/main/java/com/winlator/xenvironment/XEnvironment.java
@@ -0,0 +1,74 @@
+package com.winlator.xenvironment;
+
+import android.content.Context;
+
+import com.winlator.core.FileUtils;
+import com.winlator.xenvironment.components.GuestProgramLauncherComponent;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+public class XEnvironment implements Iterable {
+ private final Context context;
+ private final ImageFs imageFs;
+ private final ArrayList components = new ArrayList<>();
+
+ public XEnvironment(Context context, ImageFs imageFs) {
+ this.context = context;
+ this.imageFs = imageFs;
+ }
+
+ public Context getContext() {
+ return context;
+ }
+
+ public ImageFs getImageFs() {
+ return imageFs;
+ }
+
+ public void addComponent(EnvironmentComponent environmentComponent) {
+ environmentComponent.environment = this;
+ components.add(environmentComponent);
+ }
+
+ public T getComponent(Class componentClass) {
+ for (EnvironmentComponent component : components) {
+ if (component.getClass() == componentClass) return (T)component;
+ }
+ return null;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return components.iterator();
+ }
+
+ public File getTmpDir() {
+ File tmpDir = new File(context.getFilesDir(), "tmp");
+ if (!tmpDir.isDirectory()) {
+ tmpDir.mkdirs();
+ FileUtils.chmod(tmpDir, 0771);
+ }
+ return tmpDir;
+ }
+
+ public void startEnvironmentComponents() {
+ FileUtils.clear(getTmpDir());
+ for (EnvironmentComponent environmentComponent : this) environmentComponent.start();
+ }
+
+ public void stopEnvironmentComponents() {
+ for (EnvironmentComponent environmentComponent : this) environmentComponent.stop();
+ }
+
+ public void onPause() {
+ GuestProgramLauncherComponent guestProgramLauncherComponent = getComponent(GuestProgramLauncherComponent.class);
+ if (guestProgramLauncherComponent != null) guestProgramLauncherComponent.suspendProcess();
+ }
+
+ public void onResume() {
+ GuestProgramLauncherComponent guestProgramLauncherComponent = getComponent(GuestProgramLauncherComponent.class);
+ if (guestProgramLauncherComponent != null) guestProgramLauncherComponent.resumeProcess();
+ }
+}
diff --git a/app/src/main/java/com/winlator/xenvironment/components/ALSAServerComponent.java b/app/src/main/java/com/winlator/xenvironment/components/ALSAServerComponent.java
new file mode 100644
index 0000000..f7025fb
--- /dev/null
+++ b/app/src/main/java/com/winlator/xenvironment/components/ALSAServerComponent.java
@@ -0,0 +1,32 @@
+package com.winlator.xenvironment.components;
+
+import com.winlator.alsaserver.ALSAClientConnectionHandler;
+import com.winlator.alsaserver.ALSARequestHandler;
+import com.winlator.xconnector.UnixSocketConfig;
+import com.winlator.xconnector.XConnectorEpoll;
+import com.winlator.xenvironment.EnvironmentComponent;
+
+public class ALSAServerComponent extends EnvironmentComponent {
+ private XConnectorEpoll connector;
+ private final UnixSocketConfig socketConfig;
+
+ public ALSAServerComponent(UnixSocketConfig socketConfig) {
+ this.socketConfig = socketConfig;
+ }
+
+ @Override
+ public void start() {
+ if (connector != null) return;
+ connector = new XConnectorEpoll(socketConfig, new ALSAClientConnectionHandler(), new ALSARequestHandler());
+ connector.setMultithreadedClients(true);
+ connector.start();
+ }
+
+ @Override
+ public void stop() {
+ if (connector != null) {
+ connector.stop();
+ connector = null;
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xenvironment/components/EtcHostsFileUpdateComponent.java b/app/src/main/java/com/winlator/xenvironment/components/EtcHostsFileUpdateComponent.java
new file mode 100644
index 0000000..bf02058
--- /dev/null
+++ b/app/src/main/java/com/winlator/xenvironment/components/EtcHostsFileUpdateComponent.java
@@ -0,0 +1,57 @@
+package com.winlator.xenvironment.components;
+
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+
+import com.winlator.xenvironment.EnvironmentComponent;
+import com.winlator.core.FileUtils;
+
+import java.io.File;
+
+public class EtcHostsFileUpdateComponent extends EnvironmentComponent {
+ private BroadcastReceiver broadcastReceiver;
+
+ @SuppressLint("WifiManagerLeak")
+ @Override
+ public void start() {
+ Context context = environment.getContext();
+ final ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ final WifiManager wifiManager = (WifiManager)context.getSystemService(Context.WIFI_SERVICE);
+
+ broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String ip = "127.0.0.1";
+ NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ if (networkInfo != null && networkInfo.isAvailable() && networkInfo.isConnected() && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+ int ipAddress = wifiManager.getConnectionInfo().getIpAddress();
+ if (ipAddress != 0) ip = (ipAddress & 255)+"."+((ipAddress >> 8) & 255)+"."+((ipAddress >> 16) & 255)+"."+((ipAddress >> 24) & 255);
+ }
+ updateEtcHostsFile(ip);
+ }
+ };
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ context.registerReceiver(broadcastReceiver, filter);
+ }
+
+ @Override
+ public void stop() {
+ if (broadcastReceiver != null) {
+ environment.getContext().unregisterReceiver(broadcastReceiver);
+ broadcastReceiver = null;
+ }
+ }
+
+ private void updateEtcHostsFile(String ip) {
+ File file = new File(environment.getImageFs().getRootDir(), "etc/hosts");
+ FileUtils.writeString(file, ip+"\tlocalhost\n");
+ }
+}
diff --git a/app/src/main/java/com/winlator/xenvironment/components/GuestProgramLauncherComponent.java b/app/src/main/java/com/winlator/xenvironment/components/GuestProgramLauncherComponent.java
new file mode 100644
index 0000000..f22e2f7
--- /dev/null
+++ b/app/src/main/java/com/winlator/xenvironment/components/GuestProgramLauncherComponent.java
@@ -0,0 +1,206 @@
+package com.winlator.xenvironment.components;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Process;
+
+import androidx.preference.PreferenceManager;
+
+import com.winlator.MainActivity;
+import com.winlator.SettingsFragment;
+import com.winlator.box86_64.Box86_64Preset;
+import com.winlator.box86_64.Box86_64PresetManager;
+import com.winlator.core.Callback;
+import com.winlator.core.EnvVars;
+import com.winlator.core.ProcessHelper;
+import com.winlator.core.TarZstdUtils;
+import com.winlator.xconnector.UnixSocketConfig;
+import com.winlator.xenvironment.EnvironmentComponent;
+import com.winlator.xenvironment.ImageFs;
+
+import java.io.File;
+
+public class GuestProgramLauncherComponent extends EnvironmentComponent {
+ private String guestExecutable;
+ private static int pid = -1;
+ private String[] bindingPaths;
+ private EnvVars envVars;
+ private String cpuList;
+ private String box86Preset = Box86_64Preset.COMPATIBILITY;
+ private String box64Preset = Box86_64Preset.COMPATIBILITY;
+ private Callback terminationCallback;
+ private static final Object lock = new Object();
+
+ @Override
+ public void start() {
+ synchronized (lock) {
+ stop();
+ extractBox86_64Files();
+ pid = execGuestProgram();
+ }
+ }
+
+ @Override
+ public void stop() {
+ synchronized (lock) {
+ if (pid != -1) {
+ Process.killProcess(pid);
+ pid = -1;
+ }
+ }
+ }
+
+ public Callback getTerminationCallback() {
+ return terminationCallback;
+ }
+
+ public void setTerminationCallback(Callback terminationCallback) {
+ this.terminationCallback = terminationCallback;
+ }
+
+ public String getGuestExecutable() {
+ return guestExecutable;
+ }
+
+ public void setGuestExecutable(String guestExecutable) {
+ this.guestExecutable = guestExecutable;
+ }
+
+ public String[] getBindingPaths() {
+ return bindingPaths;
+ }
+
+ public void setBindingPaths(String[] bindingPaths) {
+ this.bindingPaths = bindingPaths;
+ }
+
+ public EnvVars getEnvVars() {
+ return envVars;
+ }
+
+ public void setEnvVars(EnvVars envVars) {
+ this.envVars = envVars;
+ }
+
+ public String getCpuList() {
+ return cpuList;
+ }
+
+ public void setCpuList(String cpuList) {
+ this.cpuList = cpuList;
+ }
+
+ public String getBox86Preset() {
+ return box86Preset;
+ }
+
+ public void setBox86Preset(String box86Preset) {
+ this.box86Preset = box86Preset;
+ }
+
+ public String getBox64Preset() {
+ return box64Preset;
+ }
+
+ public void setBox64Preset(String box64Preset) {
+ this.box64Preset = box64Preset;
+ }
+
+ private int execGuestProgram() {
+ Context context = environment.getContext();
+ ImageFs imageFs = environment.getImageFs();
+ File rootDir = imageFs.getRootDir();
+ File tmpDir = environment.getTmpDir();
+ String nativeLibraryDir = context.getApplicationInfo().nativeLibraryDir;
+
+ EnvVars envVars = new EnvVars();
+ addBox86EnvVars(envVars);
+ addBox64EnvVars(envVars);
+ envVars.put("HOME", ImageFs.HOME_PATH);
+ envVars.put("USER", ImageFs.USER);
+ envVars.put("TMPDIR", "/tmp");
+ envVars.put("LC_ALL", "en_US.utf8");
+ envVars.put("DISPLAY", ":0");
+ envVars.put("PATH", imageFs.getWinePath()+"/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin");
+ envVars.put("LD_LIBRARY_PATH", "/usr/lib/aarch64-linux-gnu:/usr/lib/arm-linux-gnueabihf");
+ envVars.put("LD_PRELOAD", "libandroid-sysvshm.so");
+ envVars.put("ANDROID_SYSVSHM_SERVER", UnixSocketConfig.SYSVSHM_SERVER_PATH);
+ if (this.envVars != null) envVars.putAll(this.envVars);
+
+ String command = nativeLibraryDir+"/libproot.so";
+ command += " --kill-on-exit";
+ command += " --rootfs="+rootDir;
+ command += " --cwd="+ImageFs.HOME_PATH;
+ command += " --bind=/dev";
+ command += " --bind=/proc";
+ command += " --bind=/sys";
+
+ if (bindingPaths != null) {
+ for (String path : bindingPaths) command += " --bind="+(new File(path)).getAbsolutePath();
+ }
+
+ command += " /bin/env "+envVars.toEscapedString()+(cpuList != null ? " taskset -c "+cpuList : "")+" box64 "+guestExecutable;
+
+ envVars.clear();
+ envVars.put("PROOT_TMP_DIR", tmpDir);
+ envVars.put("PROOT_LOADER", nativeLibraryDir+"/libproot-loader.so");
+ envVars.put("PROOT_LOADER_32", nativeLibraryDir+"/libproot-loader32.so");
+
+ if (MainActivity.DEBUG_LEVEL >= 1) ProcessHelper.debugMode = true;
+ return ProcessHelper.exec(command, envVars.toStringArray(), rootDir, (status) -> {
+ synchronized (lock) {
+ pid = -1;
+ }
+ if (terminationCallback != null) terminationCallback.call(status);
+ });
+ }
+
+ private void extractBox86_64Files() {
+ ImageFs imageFs = environment.getImageFs();
+ Context context = environment.getContext();
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String box86Version = preferences.getString("box86_version", SettingsFragment.DEFAULT_BOX86_VERSION);
+ String box64Version = preferences.getString("box64_version", SettingsFragment.DEFAULT_BOX64_VERSION);
+ String currentBox86Version = preferences.getString("current_box86_version", "");
+ String currentBox64Version = preferences.getString("current_box64_version", "");
+ File rootDir = imageFs.getRootDir();
+
+ if (!box86Version.equals(currentBox86Version)) {
+ TarZstdUtils.extract(context, "box86_64/box86-"+box86Version+".tzst", rootDir);
+ preferences.edit().putString("current_box86_version", box86Version).apply();
+ }
+
+ if (!box64Version.equals(currentBox64Version)) {
+ TarZstdUtils.extract(context, "box86_64/box64-"+box64Version+".tzst", rootDir);
+ preferences.edit().putString("current_box64_version", box64Version).apply();
+ }
+ }
+
+ private void addBox86EnvVars(EnvVars envVars) {
+ envVars.put("BOX86_NOBANNER", "1");
+ envVars.put("BOX86_DYNAREC", "1");
+ envVars.putAll(Box86_64PresetManager.getEnvVars("box86", environment.getContext(), box86Preset));
+ envVars.put("BOX86_X11GLX", "1");
+ envVars.put("BOX86_NORCFILES", "1");
+ }
+
+ private void addBox64EnvVars(EnvVars envVars) {
+ envVars.put("BOX64_NOBANNER", "1");
+ envVars.put("BOX64_DYNAREC", "1");
+ envVars.putAll(Box86_64PresetManager.getEnvVars("box64", environment.getContext(), box64Preset));
+ envVars.put("BOX64_X11GLX", "1");
+ envVars.put("BOX64_NORCFILES", "1");
+ }
+
+ public void suspendProcess() {
+ synchronized (lock) {
+ if (pid != -1) ProcessHelper.suspendProcess(pid);
+ }
+ }
+
+ public void resumeProcess() {
+ synchronized (lock) {
+ if (pid != -1) ProcessHelper.resumeProcess(pid);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java b/app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java
new file mode 100644
index 0000000..7cd252d
--- /dev/null
+++ b/app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java
@@ -0,0 +1,76 @@
+package com.winlator.xenvironment.components;
+
+import android.content.Context;
+import android.os.Process;
+
+import com.winlator.core.AppUtils;
+import com.winlator.core.FileUtils;
+import com.winlator.core.ProcessHelper;
+import com.winlator.xconnector.UnixSocketConfig;
+import com.winlator.xenvironment.EnvironmentComponent;
+
+import java.io.File;
+import java.util.ArrayList;
+
+public class PulseAudioComponent extends EnvironmentComponent {
+ private final UnixSocketConfig socketConfig;
+ private static int pid = -1;
+ private static final Object lock = new Object();
+
+ public PulseAudioComponent(UnixSocketConfig socketConfig) {
+ this.socketConfig = socketConfig;
+ }
+
+ @Override
+ public void start() {
+ synchronized (lock) {
+ stop();
+ pid = execPulseAudio();
+ }
+ }
+
+ @Override
+ public void stop() {
+ synchronized (lock) {
+ if (pid != -1) {
+ Process.killProcess(pid);
+ pid = -1;
+ }
+ }
+ }
+
+ private int execPulseAudio() {
+ Context context = environment.getContext();
+ String nativeLibraryDir = context.getApplicationInfo().nativeLibraryDir;
+ File workingDir = new File(context.getFilesDir(), "/pulseaudio");
+ if (!workingDir.isDirectory()) {
+ workingDir.mkdirs();
+ FileUtils.chmod(workingDir, 0771);
+ }
+
+ File configFile = new File(workingDir, "default.pa");
+ FileUtils.writeString(configFile, String.join("\n",
+ "load-module module-native-protocol-unix auth-anonymous=1 auth-cookie-enabled=0 socket=\""+socketConfig.path+"\"",
+ "load-module module-aaudio-sink",
+ "set-default-sink AAudioSink"
+ ));
+
+ File modulesDir = new File(workingDir, "modules/"+ AppUtils.getArchName());
+
+ ArrayList envVars = new ArrayList<>();
+ envVars.add("LD_LIBRARY_PATH="+nativeLibraryDir+":"+modulesDir);
+ envVars.add("HOME="+workingDir);
+ envVars.add("TMPDIR="+environment.getTmpDir());
+
+ String command = nativeLibraryDir+"/libpulseaudio.so";
+ command += " --system=false";
+ command += " --disable-shm=true";
+ command += " --fail=false";
+ command += " -n --file=default.pa";
+ command += " --daemonize=false";
+ command += " --use-pid-file=false";
+ command += " --exit-idle-time=-1";
+
+ return ProcessHelper.exec(command, envVars.toArray(new String[0]), workingDir);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xenvironment/components/SysVSharedMemoryComponent.java b/app/src/main/java/com/winlator/xenvironment/components/SysVSharedMemoryComponent.java
new file mode 100644
index 0000000..94716a2
--- /dev/null
+++ b/app/src/main/java/com/winlator/xenvironment/components/SysVSharedMemoryComponent.java
@@ -0,0 +1,42 @@
+package com.winlator.xenvironment.components;
+
+import com.winlator.sysvshm.SysVSHMConnectionHandler;
+import com.winlator.sysvshm.SysVSHMRequestHandler;
+import com.winlator.sysvshm.SysVSharedMemory;
+import com.winlator.xconnector.UnixSocketConfig;
+import com.winlator.xconnector.XConnectorEpoll;
+import com.winlator.xenvironment.EnvironmentComponent;
+import com.winlator.xserver.SHMSegmentManager;
+import com.winlator.xserver.XServer;
+
+public class SysVSharedMemoryComponent extends EnvironmentComponent {
+ private XConnectorEpoll connector;
+ public final UnixSocketConfig socketConfig;
+ private SysVSharedMemory sysVSharedMemory;
+ private final XServer xServer;
+
+ public SysVSharedMemoryComponent(XServer xServer, UnixSocketConfig socketConfig) {
+ this.xServer = xServer;
+ this.socketConfig = socketConfig;
+ }
+
+ @Override
+ public void start() {
+ if (connector != null) return;
+ sysVSharedMemory = new SysVSharedMemory();
+ connector = new XConnectorEpoll(socketConfig, new SysVSHMConnectionHandler(sysVSharedMemory), new SysVSHMRequestHandler());
+ connector.start();
+
+ xServer.setSHMSegmentManager(new SHMSegmentManager(sysVSharedMemory));
+ }
+
+ @Override
+ public void stop() {
+ if (connector != null) {
+ connector.stop();
+ connector = null;
+ }
+
+ sysVSharedMemory.deleteAll();
+ }
+}
diff --git a/app/src/main/java/com/winlator/xenvironment/components/VirGLRendererComponent.java b/app/src/main/java/com/winlator/xenvironment/components/VirGLRendererComponent.java
new file mode 100644
index 0000000..615fa57
--- /dev/null
+++ b/app/src/main/java/com/winlator/xenvironment/components/VirGLRendererComponent.java
@@ -0,0 +1,127 @@
+package com.winlator.xenvironment.components;
+
+import androidx.annotation.Keep;
+
+import com.winlator.renderer.GLRenderer;
+import com.winlator.renderer.Texture;
+import com.winlator.xconnector.Client;
+import com.winlator.xconnector.ConnectionHandler;
+import com.winlator.xconnector.RequestHandler;
+import com.winlator.xconnector.UnixSocketConfig;
+import com.winlator.xconnector.XConnectorEpoll;
+import com.winlator.xenvironment.EnvironmentComponent;
+import com.winlator.xserver.Drawable;
+import com.winlator.xserver.XServer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class VirGLRendererComponent extends EnvironmentComponent implements ConnectionHandler, RequestHandler {
+ private final XServer xServer;
+ private final UnixSocketConfig socketConfig;
+ private XConnectorEpoll connector;
+ private long sharedEGLContextPtr;
+
+ static {
+ System.loadLibrary("virglrenderer");
+ }
+
+ public VirGLRendererComponent(XServer xServer, UnixSocketConfig socketConfig) {
+ this.xServer = xServer;
+ this.socketConfig = socketConfig;
+ }
+
+ @Override
+ public void start() {
+ if (connector != null) return;
+ connector = new XConnectorEpoll(socketConfig, this, this);
+ connector.start();
+ }
+
+ @Override
+ public void stop() {
+ if (connector != null) {
+ connector.stop();
+ connector = null;
+ }
+ }
+
+ @Keep
+ private void killConnection(int fd) {
+ connector.killConnection(connector.getClient(fd));
+ }
+
+ @Keep
+ private long getSharedEGLContext() {
+ if (sharedEGLContextPtr != 0) return sharedEGLContextPtr;
+ final Thread thread = Thread.currentThread();
+ try {
+ GLRenderer renderer = xServer.getRenderer();
+ renderer.xServerView.queueEvent(() -> {
+ sharedEGLContextPtr = getCurrentEGLContextPtr();
+
+ synchronized(thread) {
+ thread.notify();
+ }
+ });
+ synchronized (thread) {
+ thread.wait();
+ }
+ }
+ catch (Exception e) {
+ return 0;
+ }
+ return sharedEGLContextPtr;
+ }
+
+ @Override
+ public void handleConnectionShutdown(Client client) {
+ long clientPtr = (long)client.getTag();
+ destroyClient(clientPtr);
+ }
+
+ @Override
+ public void handleNewConnection(Client client) {
+ getSharedEGLContext();
+
+ ArrayList clients = connector.getConnectedClients();
+ if (!clients.isEmpty()) {
+ long clientPtr = (long)clients.get(clients.size()-1).getTag();
+ destroyRenderer(clientPtr);
+ }
+
+ long clientPtr = handleNewConnection(client.clientSocket.fd);
+ client.setTag(clientPtr);
+ }
+
+ @Override
+ public boolean handleRequest(Client client) throws IOException {
+ long clientPtr = (long)client.getTag();
+ handleRequest(clientPtr);
+ return true;
+ }
+
+ @Keep
+ private void flushFrontbuffer(int drawableId, int framebuffer) {
+ Drawable drawable = xServer.drawableManager.getDrawable(drawableId);
+ if (drawable == null) return;
+
+ synchronized (drawable.renderLock) {
+ drawable.setData(null);
+ Texture texture = drawable.getTexture();
+ texture.copyFromFramebuffer(framebuffer, drawable.width, drawable.height);
+ }
+
+ xServer.getRenderer().xServerView.requestRender();
+ }
+
+ private native long handleNewConnection(int fd);
+
+ private native void handleRequest(long clientPtr);
+
+ private native long getCurrentEGLContextPtr();
+
+ private native void destroyClient(long clientPtr);
+
+ private native void destroyRenderer(long clientPtr);
+}
diff --git a/app/src/main/java/com/winlator/xenvironment/components/XServerComponent.java b/app/src/main/java/com/winlator/xenvironment/components/XServerComponent.java
new file mode 100644
index 0000000..3bb9f85
--- /dev/null
+++ b/app/src/main/java/com/winlator/xenvironment/components/XServerComponent.java
@@ -0,0 +1,40 @@
+package com.winlator.xenvironment.components;
+
+import com.winlator.xenvironment.EnvironmentComponent;
+import com.winlator.xconnector.XConnectorEpoll;
+import com.winlator.xconnector.UnixSocketConfig;
+import com.winlator.xserver.XClientConnectionHandler;
+import com.winlator.xserver.XClientRequestHandler;
+import com.winlator.xserver.XServer;
+
+public class XServerComponent extends EnvironmentComponent {
+ private XConnectorEpoll connector;
+ private final XServer xServer;
+ private final UnixSocketConfig socketConfig;
+
+ public XServerComponent(XServer xServer, UnixSocketConfig socketConfig) {
+ this.xServer = xServer;
+ this.socketConfig = socketConfig;
+ }
+
+ @Override
+ public void start() {
+ if (connector != null) return;
+ connector = new XConnectorEpoll(socketConfig, new XClientConnectionHandler(xServer), new XClientRequestHandler());
+ connector.setInitialInputBufferCapacity(262144);
+ connector.setCanReceiveAncillaryMessages(true);
+ connector.start();
+ }
+
+ @Override
+ public void stop() {
+ if (connector != null) {
+ connector.stop();
+ connector = null;
+ }
+ }
+
+ public XServer getXServer() {
+ return xServer;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/Atom.java b/app/src/main/java/com/winlator/xserver/Atom.java
new file mode 100644
index 0000000..4d77d5a
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/Atom.java
@@ -0,0 +1,31 @@
+package com.winlator.xserver;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public abstract class Atom {
+ private static final ArrayList atoms = new ArrayList<>(Arrays.asList(null, "PRIMARY", "SECONDARY", "ARC", "ATOM", "BITMAP", "CARDINAL", "COLORMAP", "CURSOR", "CUT_BUFFER0", "CUT_BUFFER1", "CUT_BUFFER2", "CUT_BUFFER3", "CUT_BUFFER4", "CUT_BUFFER5", "CUT_BUFFER6", "CUT_BUFFER7", "DRAWABLE", "FONT", "INTEGER", "PIXMAP", "POINT", "RECTANGLE", "RESOURCE_MANAGER", "RGB_COLOR_MAP", "RGB_BEST_MAP", "RGB_BLUE_MAP", "RGB_DEFAULT_MAP", "RGB_GRAY_MAP", "RGB_GREEN_MAP", "RGB_RED_MAP", "STRING", "VISUALID", "WINDOW", "WM_COMMAND", "WM_HINTS", "WM_CLIENT_MACHINE", "WM_ICON_NAME", "WM_ICON_SIZE", "WM_NAME", "WM_NORMAL_HINTS", "WM_SIZE_HINTS", "WM_ZOOM_HINTS", "MIN_SPACE", "NORM_SPACE", "MAX_SPACE", "END_SPACE", "SUPERSC.LPT_X", "SUPERSC.LPT_Y", "SUBSC.LPT_X", "SUBSC.LPT_Y", "UNDERLINE_POSITION", "UNDERLINE_THICKNESS", "STRIKEOUT_ASCENT", "STRIKEOUT_DESCENT", "ITALIC_ANGLE", "X_HEIGHT", "QUAD_WIDTH", "WEIGHT", "POINT_SIZE", "RESOLUTION", "COPYRIGHT", "NOTICE", "FONT_NAME", "FAMILY_NAME", "FULL_NAME", "CAP_HEIGHT", "WM_CLASS", "WM_TRANSIENT_FOR"));
+
+ public synchronized static String getName(int id) {
+ return atoms.get(id);
+ }
+
+ public synchronized static int getId(String name) {
+ if (name == null) return 0;
+ for (int i = 0; i < atoms.size(); i++) if (name.equals(atoms.get(i))) return i;
+ return -1;
+ }
+
+ public synchronized static int internAtom(String name) {
+ int id = getId(name);
+ if (id == -1) {
+ id = atoms.size();
+ atoms.add(name);
+ }
+ return id;
+ }
+
+ public synchronized static boolean isValid(int id) {
+ return id > 0 && id < atoms.size();
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/Bitmask.java b/app/src/main/java/com/winlator/xserver/Bitmask.java
new file mode 100644
index 0000000..ff77efd
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/Bitmask.java
@@ -0,0 +1,74 @@
+package com.winlator.xserver;
+
+import androidx.annotation.NonNull;
+
+import java.util.Iterator;
+
+public class Bitmask implements Iterable {
+ private int bits = 0;
+
+ public Bitmask() {}
+
+ public Bitmask(int bits) {
+ this.bits = bits;
+ }
+
+ public boolean isSet(int flag) {
+ return (flag & this.bits) != 0;
+ }
+
+ public boolean intersects(Bitmask mask) {
+ return (mask.bits & this.bits) != 0;
+ }
+
+ public void set(int flag) {
+ bits |= flag;
+ }
+
+ public void set(int flag, boolean value) {
+ if (value) {
+ set(flag);
+ }
+ else unset(flag);
+ }
+
+ public void unset(int flag) {
+ bits &= ~flag;
+ }
+
+ public boolean isEmpty() {
+ return bits == 0;
+ }
+
+ public int getBits() {
+ return bits;
+ }
+
+ public void join(Bitmask mask) {
+ this.bits = mask.bits | this.bits;
+ }
+
+ @NonNull
+ @Override
+ public Iterator iterator() {
+ final int[] bits = {this.bits};
+ return new Iterator() {
+ @Override
+ public boolean hasNext() {
+ return bits[0] != 0;
+ }
+
+ @Override
+ public Integer next() {
+ int index = Integer.lowestOneBit(bits[0]);
+ bits[0] &= ~index;
+ return index;
+ }
+ };
+ }
+
+ @Override
+ public int hashCode() {
+ return bits;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/ClientOpcodes.java b/app/src/main/java/com/winlator/xserver/ClientOpcodes.java
new file mode 100644
index 0000000..ff8e794
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/ClientOpcodes.java
@@ -0,0 +1,56 @@
+package com.winlator.xserver;
+
+public abstract class ClientOpcodes {
+ public static final byte CREATE_WINDOW = 1;
+ public static final byte CHANGE_WINDOW_ATTRIBUTES = 2;
+ public static final byte GET_WINDOW_ATTRIBUTES = 3;
+ public static final byte DESTROY_WINDOW = 4;
+ public static final byte REPARENT_WINDOW = 7;
+ public static final byte MAP_WINDOW = 8;
+ public static final byte UNMAP_WINDOW = 10;
+ public static final byte CONFIGURE_WINDOW = 12;
+ public static final byte GET_GEOMETRY = 14;
+ public static final byte QUERY_TREE = 15;
+ public static final byte INTERN_ATOM = 16;
+ public static final byte CHANGE_PROPERTY = 18;
+ public static final byte DELETE_PROPERTY = 19;
+ public static final byte GET_PROPERTY = 20;
+ public static final byte SET_SELECTION_OWNER = 22;
+ public static final byte GET_SELECTION_OWNER = 23;
+ public static final byte SEND_EVENT = 25;
+ public static final byte GRAB_POINTER = 26;
+ public static final byte UNGRAB_POINTER = 27;
+ public static final byte QUERY_POINTER = 38;
+ public static final byte TRANSLATE_COORDINATES = 40;
+ public static final byte WARP_POINTER = 41;
+ public static final byte SET_INPUT_FOCUS = 42;
+ public static final byte GET_INPUT_FOCUS = 43;
+ public static final byte OPEN_FONT = 45;
+ public static final byte LIST_FONTS = 49;
+ public static final byte CREATE_PIXMAP = 53;
+ public static final byte FREE_PIXMAP = 54;
+ public static final byte CREATE_GC = 55;
+ public static final byte CHANGE_GC = 56;
+ public static final byte SET_CLIP_RECTANGLES = 59;
+ public static final byte FREE_GC = 60;
+ public static final byte COPY_AREA = 62;
+ public static final byte POLY_LINE = 65;
+ public static final byte POLY_SEGMENT = 66;
+ public static final byte POLY_RECTANGLE = 67;
+ public static final byte POLY_FILL_RECTANGLE = 70;
+ public static final byte PUT_IMAGE = 72;
+ public static final byte GET_IMAGE = 73;
+ public static final byte CREATE_COLORMAP = 78;
+ public static final byte FREE_COLORMAP = 79;
+ public static final byte CREATE_CURSOR = 93;
+ public static final byte CREATE_GLYPH_CURSOR = 94;
+ public static final byte FREE_CURSOR = 95;
+ public static final byte QUERY_EXTENSION = 98;
+ public static final byte GET_KEYBOARD_MAPPING = 101;
+ public static final byte BELL = 104;
+ public static final byte SET_SCREEN_SAVER = 107;
+ public static final byte GET_SCREEN_SAVER = 108;
+ public static final byte FORCE_SCREEN_SAVER = 115;
+ public static final byte GET_MODIFIER_MAPPING = 119;
+ public static final byte NO_OPERATION = 127;
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/Cursor.java b/app/src/main/java/com/winlator/xserver/Cursor.java
new file mode 100644
index 0000000..d98f46a
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/Cursor.java
@@ -0,0 +1,27 @@
+package com.winlator.xserver;
+
+public class Cursor extends XResource {
+ public final int hotSpotX;
+ public final int hotSpotY;
+ public final Drawable cursorImage;
+ public final Drawable sourceImage;
+ public final Drawable maskImage;
+ private boolean visible = true;
+
+ public Cursor(int id, int hotSpotX, int hotSpotY, Drawable cursorImage, Drawable sourceImage, Drawable maskImage) {
+ super(id);
+ this.hotSpotX = hotSpotX;
+ this.hotSpotY = hotSpotY;
+ this.cursorImage = cursorImage;
+ this.sourceImage = sourceImage;
+ this.maskImage = maskImage;
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ public void setVisible(boolean visible) {
+ this.visible = visible;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/CursorManager.java b/app/src/main/java/com/winlator/xserver/CursorManager.java
new file mode 100644
index 0000000..ded9ce7
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/CursorManager.java
@@ -0,0 +1,52 @@
+package com.winlator.xserver;
+
+import android.util.SparseArray;
+
+import java.nio.IntBuffer;
+
+public class CursorManager extends XResourceManager {
+ private final SparseArray cursors = new SparseArray<>();
+ private final DrawableManager drawableManager;
+
+ public CursorManager(DrawableManager drawableManager) {
+ this.drawableManager = drawableManager;
+ }
+
+ public Cursor getCursor(int id) {
+ return cursors.get(id);
+ }
+
+ public Cursor createCursor(int id, short x, short y, Pixmap sourcePixmap, Pixmap maskPixmap) {
+ if (cursors.indexOfKey(id) >= 0) return null;
+ Drawable drawable = drawableManager.createDrawable(0, sourcePixmap.drawable.width, sourcePixmap.drawable.height, sourcePixmap.drawable.visual);
+ Cursor cursor = new Cursor(id, x, y, drawable, sourcePixmap.drawable, maskPixmap != null ? maskPixmap.drawable : null);
+ cursors.put(id, cursor);
+ triggerOnCreateResourceListener(cursor);
+ return cursor;
+ }
+
+ public void freeCursor(int id) {
+ triggerOnFreeResourceListener(cursors.get(id));
+ cursors.remove(id);
+ }
+
+ private static boolean isEmptyMaskImage(Drawable maskImage) {
+ IntBuffer maskData = maskImage.getData().asIntBuffer();
+ boolean result = true;
+ for (int i = 0; i < maskData.capacity(); i++) {
+ if (maskData.get(i) != 0x000000) {
+ result = false;
+ break;
+ }
+ }
+ return result;
+ }
+
+ public void recolorCursor(Cursor cursor, byte foreRed, byte foreGreen, byte foreBlue, byte backRed, byte backGreen, byte backBlue) {
+ if (cursor.maskImage != null) {
+ boolean visible = !isEmptyMaskImage(cursor.maskImage);
+ cursor.setVisible(visible);
+ if (visible) cursor.cursorImage.drawAlphaMaskedBitmap(foreRed, foreGreen, foreBlue, backRed, backGreen, backBlue, cursor.sourceImage, cursor.maskImage);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/DesktopHelper.java b/app/src/main/java/com/winlator/xserver/DesktopHelper.java
new file mode 100644
index 0000000..2d66d18
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/DesktopHelper.java
@@ -0,0 +1,67 @@
+package com.winlator.xserver;
+
+import androidx.collection.ArrayMap;
+
+import java.util.Map;
+
+public abstract class DesktopHelper {
+ public static void attachTo(final XServer xServer) {
+ setupXResources(xServer);
+
+ xServer.pointer.addOnPointerMotionListener(new Pointer.OnPointerMotionListener() {
+ @Override
+ public void onPointerButtonPress(Pointer.Button button) {
+ updateFocusedWindow(xServer);
+ }
+ });
+
+ xServer.windowManager.addOnWindowModificationListener(new WindowManager.OnWindowModificationListener() {
+ @Override
+ public void onMapWindow(Window window) {
+ setFocusedWindow(xServer, window);
+ }
+ });
+ }
+
+ private static void updateFocusedWindow(XServer xServer) {
+ try (XLock lock = xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.INPUT_DEVICE)) {
+ Window focusedWindow = xServer.windowManager.getFocusedWindow();
+ Window child = xServer.windowManager.rootWindow.getChildByCoords(xServer.pointer.getClampedX(), xServer.pointer.getClampedY());
+ if (child == null && focusedWindow != xServer.windowManager.rootWindow) {
+ xServer.windowManager.setFocus(xServer.windowManager.rootWindow, WindowManager.FocusRevertTo.NONE);
+ }
+ else if (child != null && child != focusedWindow) {
+ setFocusedWindow(xServer, child);
+ }
+ }
+ }
+
+ private static void setFocusedWindow(XServer xServer, Window window) {
+ boolean parentIsRoot = window.getParent() == xServer.windowManager.rootWindow;
+ xServer.windowManager.setFocus(window, parentIsRoot ? WindowManager.FocusRevertTo.POINTER_ROOT : WindowManager.FocusRevertTo.PARENT);
+ }
+
+ private static void setupXResources(XServer xServer) {
+ int atom = Atom.getId("RESOURCE_MANAGER");
+ int type = Atom.getId("STRING");
+
+ ArrayMap values = new ArrayMap<>();
+ values.put("size", "20");
+ values.put("theme", "dmz");
+ values.put("theme_core", "true");
+
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry entry : values.entrySet()) {
+ sb.append("Xcursor")
+ .append('.')
+ .append(entry.getKey())
+ .append(':')
+ .append('\t')
+ .append(entry.getValue())
+ .append('\n');
+ }
+
+ byte[] data = sb.toString().getBytes(XServer.LATIN1_CHARSET);
+ xServer.windowManager.rootWindow.modifyProperty(atom, type, Property.Format.BYTE_ARRAY, Property.Mode.APPEND, data);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/Drawable.java b/app/src/main/java/com/winlator/xserver/Drawable.java
new file mode 100644
index 0000000..e7ad8ff
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/Drawable.java
@@ -0,0 +1,193 @@
+package com.winlator.xserver;
+
+import android.graphics.Bitmap;
+
+import com.winlator.core.Callback;
+import com.winlator.math.Mathf;
+import com.winlator.renderer.GPUImage;
+import com.winlator.renderer.Texture;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class Drawable extends XResource {
+ public final short width;
+ public final short height;
+ public final Visual visual;
+ private Texture texture = new Texture();
+ private ByteBuffer data;
+ private Runnable onDrawListener;
+ private Callback onDestroyListener;
+ public final Object renderLock = new Object();
+
+ static {
+ System.loadLibrary("winlator");
+ }
+
+ public Drawable(int id, int width, int height, Visual visual) {
+ super(id);
+ this.width = (short)width;
+ this.height = (short)height;
+ this.visual = visual;
+ this.data = ByteBuffer.allocateDirect(width * height * 4).order(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ public static Drawable fromBitmap(Bitmap bitmap) {
+ Drawable drawable = new Drawable(0, bitmap.getWidth(), bitmap.getHeight(), null);
+ fromBitmap(bitmap, drawable.data);
+ return drawable;
+ }
+
+ public Texture getTexture() {
+ return texture;
+ }
+
+ public void setTexture(Texture texture) {
+ if (texture instanceof GPUImage) data = ((GPUImage)texture).getVirtualData();
+ this.texture = texture;
+ }
+
+ public ByteBuffer getData() {
+ return data;
+ }
+
+ public void setData(ByteBuffer data) {
+ this.data = data;
+ }
+
+ private short getStride() {
+ return texture instanceof GPUImage ? ((GPUImage)texture).getStride() : width;
+ }
+
+ public Runnable getOnDrawListener() {
+ return onDrawListener;
+ }
+
+ public void setOnDrawListener(Runnable onDrawListener) {
+ this.onDrawListener = onDrawListener;
+ }
+
+ public Callback getOnDestroyListener() {
+ return onDestroyListener;
+ }
+
+ public void setOnDestroyListener(Callback onDestroyListener) {
+ this.onDestroyListener = onDestroyListener;
+ }
+
+ public void drawImage(short srcX, short srcY, short dstX, short dstY, short width, short height, byte depth, ByteBuffer data, short totalWidth, short totalHeight) {
+ if (depth == 1) {
+ drawBitmap(width, height, data, this.data);
+ }
+ else if (depth == 24 || depth == 32) {
+ dstX = (short)Mathf.clamp(dstX, 0, this.width-1);
+ dstY = (short)Mathf.clamp(dstY, 0, this.height-1);
+ if ((dstX + width) > this.width) width = (short)((this.width - dstX));
+ if ((dstY + height) > this.height) height = (short)((this.height - dstY));
+
+ copyArea(srcX, srcY, dstX, dstY, width, height, totalWidth, this.getStride(), data, this.data);
+ }
+
+ this.data.rewind();
+ data.rewind();
+
+ texture.setNeedsUpdate(true);
+ if (onDrawListener != null) onDrawListener.run();
+ }
+
+ public ByteBuffer getImage(short x, short y, short width, short height) {
+ ByteBuffer dstData = ByteBuffer.allocateDirect(width * height * 4).order(ByteOrder.LITTLE_ENDIAN);
+
+ x = (short)Mathf.clamp(x, 0, this.width-1);
+ y = (short)Mathf.clamp(y, 0, this.height-1);
+ if ((x + width) > this.width) width = (short)(this.width - x);
+ if ((y + height) > this.height) height = (short)(this.height - y);
+
+ copyArea(x, y, (short)0, (short)0, width, height, this.getStride(), width, this.data, dstData);
+
+ this.data.rewind();
+ dstData.rewind();
+ return dstData;
+ }
+
+ public void copyArea(short srcX, short srcY, short dstX, short dstY, short width, short height, Drawable drawable) {
+ copyArea(srcX, srcY, dstX, dstY, width, height, drawable, GraphicsContext.Function.COPY);
+ }
+
+ public void copyArea(short srcX, short srcY, short dstX, short dstY, short width, short height, Drawable drawable, GraphicsContext.Function gcFunction) {
+ dstX = (short)Mathf.clamp(dstX, 0, this.width-1);
+ dstY = (short)Mathf.clamp(dstY, 0, this.height-1);
+ if ((dstX + width) > this.width) width = (short)(this.width - dstX);
+ if ((dstY + height) > this.height) height = (short)(this.height - dstY);
+
+ if (gcFunction == GraphicsContext.Function.COPY) {
+ copyArea(srcX, srcY, dstX, dstY, width, height, drawable.getStride(), this.getStride(), drawable.data, this.data);
+ }
+ else copyAreaOp(srcX, srcY, dstX, dstY, width, height, drawable.getStride(), this.getStride(), drawable.data, this.data, gcFunction.ordinal());
+
+ this.data.rewind();
+ drawable.data.rewind();
+
+ texture.setNeedsUpdate(true);
+ if (onDrawListener != null) onDrawListener.run();
+ }
+
+ public void fillColor(int color) {
+ fillRect(0, 0, width, height, color);
+ }
+
+ public void fillRect(int x, int y, int width, int height, int color) {
+ x = (short)Mathf.clamp(x, 0, this.width-1);
+ y = (short)Mathf.clamp(y, 0, this.height-1);
+ if ((x + width) > this.width) width = (short)((this.width - x));
+ if ((y + height) > this.height) height = (short)((this.height - y));
+
+ fillRect((short)x, (short)y, (short)width, (short)height, color, this.getStride(), this.data);
+ this.data.rewind();
+
+ texture.setNeedsUpdate(true);
+ if (onDrawListener != null) onDrawListener.run();
+ }
+
+ public void drawLines(int color, int lineWidth, short... points) {
+ for (int i = 2; i < points.length; i += 2) {
+ drawLine(points[i-2], points[i-1], points[i+0], points[i+1], color, (short)lineWidth);
+ }
+ }
+
+ public void drawLine(int x0, int y0, int x1, int y1, int color, int lineWidth) {
+ x0 = Mathf.clamp(x0, 0, width-lineWidth);
+ y0 = Mathf.clamp(y0, 0, height-lineWidth);
+ x1 = Mathf.clamp(x1, 0, width-lineWidth);
+ y1 = Mathf.clamp(y1, 0, height-lineWidth);
+
+ drawLine((short)x0, (short)y0, (short)x1, (short)y1, color, (short)lineWidth, this.getStride(), this.data);
+
+ this.data.rewind();
+
+ texture.setNeedsUpdate(true);
+ if (onDrawListener != null) onDrawListener.run();
+ }
+
+ public void drawAlphaMaskedBitmap(byte foreRed, byte foreGreen, byte foreBlue, byte backRed, byte backGreen, byte backBlue, Drawable srcDrawable, Drawable maskDrawable) {
+ drawAlphaMaskedBitmap(foreRed, foreGreen, foreBlue, backRed, backGreen, backBlue, srcDrawable.data, maskDrawable.data, this.data);
+ this.data.rewind();
+
+ texture.setNeedsUpdate(true);
+ if (onDrawListener != null) onDrawListener.run();
+ }
+
+ private static native void drawBitmap(short width, short height, ByteBuffer srcData, ByteBuffer dstData);
+
+ private static native void drawAlphaMaskedBitmap(byte foreRed, byte foreGreen, byte foreBlue, byte backRed, byte backGreen, byte backBlue, ByteBuffer srcData, ByteBuffer maskData, ByteBuffer dstData);
+
+ private static native void copyArea(short srcX, short srcY, short dstX, short dstY, short width, short height, short srcStride, short dstStride, ByteBuffer srcData, ByteBuffer dstData);
+
+ private static native void copyAreaOp(short srcX, short srcY, short dstX, short dstY, short width, short height, short srcStride, short dstStride, ByteBuffer srcData, ByteBuffer dstData, int gcFunction);
+
+ private static native void fillRect(short x, short y, short width, short height, int color, short stride, ByteBuffer data);
+
+ private static native void drawLine(short x0, short y0, short x1, short y1, int color, short lineWidth, short stride, ByteBuffer data);
+
+ private static native void fromBitmap(Bitmap bitmap, ByteBuffer data);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/DrawableManager.java b/app/src/main/java/com/winlator/xserver/DrawableManager.java
new file mode 100644
index 0000000..e86a186
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/DrawableManager.java
@@ -0,0 +1,54 @@
+package com.winlator.xserver;
+
+import android.util.SparseArray;
+
+import com.winlator.core.Callback;
+import com.winlator.renderer.Texture;
+
+public class DrawableManager extends XResourceManager implements XResourceManager.OnResourceLifecycleListener {
+ private final XServer xServer;
+ private final SparseArray drawables = new SparseArray<>();
+
+ public DrawableManager(XServer xServer) {
+ this.xServer = xServer;
+ xServer.pixmapManager.addOnResourceLifecycleListener(this);
+ }
+
+ public Drawable getDrawable(int id) {
+ return drawables.get(id);
+ }
+
+ public Drawable createDrawable(int id, short width, short height, byte depth) {
+ return createDrawable(id, width, height, xServer.pixmapManager.getVisualForDepth(depth));
+ }
+
+ public Drawable createDrawable(int id, short width, short height, Visual visual) {
+ if (id == 0) return new Drawable(id, width, height, visual);
+ if (drawables.indexOfKey(id) >= 0) return null;
+ Drawable drawable = new Drawable(id, width, height, visual);
+ drawables.put(id, drawable);
+ return drawable;
+ }
+
+ public void removeDrawable(int id) {
+ Drawable drawable = drawables.get(id);
+
+ final Texture texture = drawable.getTexture();
+ if (texture != null) xServer.getRenderer().xServerView.queueEvent(texture::destroy);
+
+ Callback onDestroyListener = drawable.getOnDestroyListener();
+ if (onDestroyListener != null) onDestroyListener.call(drawable);
+
+ drawable.setOnDrawListener(null);
+ drawables.remove(id);
+ }
+
+ @Override
+ public void onFreeResource(XResource resource) {
+ if (resource instanceof Pixmap) removeDrawable(((Pixmap)resource).drawable.id);
+ }
+
+ public Visual getVisual() {
+ return xServer.pixmapManager.visual;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/EventListener.java b/app/src/main/java/com/winlator/xserver/EventListener.java
new file mode 100644
index 0000000..646a022
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/EventListener.java
@@ -0,0 +1,32 @@
+package com.winlator.xserver;
+
+import com.winlator.xserver.events.Event;
+
+import java.io.IOException;
+
+public class EventListener {
+ public final XClient client;
+ public final Bitmask eventMask;
+
+ public EventListener(XClient client, Bitmask eventMask) {
+ this.client = client;
+ this.eventMask = eventMask;
+ }
+
+ public boolean isInterestedIn(int eventId) {
+ return eventMask.isSet(eventId);
+ }
+
+ public boolean isInterestedIn(Bitmask mask) {
+ return this.eventMask.intersects(mask);
+ }
+
+ public void sendEvent(Event event) {
+ try {
+ event.send(client.getSequenceNumber(), client.getOutputStream());
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/GrabManager.java b/app/src/main/java/com/winlator/xserver/GrabManager.java
new file mode 100644
index 0000000..a7fa431
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/GrabManager.java
@@ -0,0 +1,71 @@
+package com.winlator.xserver;
+
+import com.winlator.xserver.events.Event;
+import com.winlator.xserver.events.PointerWindowEvent;
+
+public class GrabManager implements WindowManager.OnWindowModificationListener {
+ private Window window;
+ private boolean ownerEvents;
+ private boolean releaseWithButtons;
+ private EventListener eventListener;
+ private final XServer xServer;
+
+ public GrabManager(XServer xServer) {
+ this.xServer = xServer;
+ xServer.windowManager.addOnWindowModificationListener(this);
+ }
+
+ @Override
+ public void onUnmapWindow(Window window) {
+ if (window != null && window.getMapState() != Window.MapState.VIEWABLE) {
+ deactivatePointerGrab();
+ }
+ }
+
+ public Window getWindow() {
+ return window;
+ }
+
+ public boolean isOwnerEvents() {
+ return ownerEvents;
+ }
+
+ public boolean isReleaseWithButtons() {
+ return releaseWithButtons;
+ }
+
+ public EventListener getEventListener() {
+ return eventListener;
+ }
+
+ public XClient getClient() {
+ return eventListener != null ? eventListener.client : null;
+ }
+
+ public void deactivatePointerGrab() {
+ if (window != null) {
+ xServer.inputDeviceManager.sendEnterLeaveNotify(window, xServer.inputDeviceManager.getPointWindow(), PointerWindowEvent.Mode.UNGRAB);
+ window = null;
+ eventListener = null;
+ }
+ }
+
+ private void activatePointerGrab(Window window, EventListener eventListener, boolean ownerEvents, boolean releaseWithButtons) {
+ if (this.window == null) {
+ xServer.inputDeviceManager.sendEnterLeaveNotify(xServer.inputDeviceManager.getPointWindow(), window, PointerWindowEvent.Mode.GRAB);
+ }
+ this.window = window;
+ this.releaseWithButtons = releaseWithButtons;
+ this.ownerEvents = ownerEvents;
+ this.eventListener = eventListener;
+ }
+
+ public void activatePointerGrab(Window window, boolean ownerEvents, Bitmask eventMask, XClient client) {
+ activatePointerGrab(window, new EventListener(client, eventMask), ownerEvents, false);
+ }
+
+ public void activatePointerGrab(Window window) {
+ EventListener eventListener = window.getButtonPressListener();
+ activatePointerGrab(window, eventListener, eventListener.isInterestedIn(Event.OWNER_GRAB_BUTTON), true);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/GraphicsContext.java b/app/src/main/java/com/winlator/xserver/GraphicsContext.java
new file mode 100644
index 0000000..9a8b0cb
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/GraphicsContext.java
@@ -0,0 +1,89 @@
+package com.winlator.xserver;
+
+public class GraphicsContext extends XResource {
+ public static final int FLAG_FUNCTION = 1<<0;
+ public static final int FLAG_PLANE_MASK = 1<<1;
+ public static final int FLAG_FOREGROUND = 1<<2;
+ public static final int FLAG_BACKGROUND = 1<<3;
+ public static final int FLAG_LINE_WIDTH = 1<<4;
+ public static final int FLAG_LINE_STYLE = 1<<5;
+ public static final int FLAG_CAP_STYLE = 1<<6;
+ public static final int FLAG_JOIN_STYLE = 1<<7;
+ public static final int FLAG_FILL_STYLE = 1<<8;
+ public static final int FLAG_FILL_RULE = 1<<9;
+ public static final int FLAG_TILE = 1<<10;
+ public static final int FLAG_STIPPLE = 1<<11;
+ public static final int FLAG_TILE_STIPPLE_X_ORIGIN = 1<<12;
+ public static final int FLAG_TILE_STIPPLE_Y_ORIGIN = 1<<13;
+ public static final int FLAG_FONT = 1<<14;
+ public static final int FLAG_SUBWINDOW_MODE = 1<<15;
+ public static final int FLAG_GRAPHICS_EXPOSURES = 1<<16;
+ public static final int FLAG_CLIP_X_ORIGIN = 1<<17;
+ public static final int FLAG_CLIP_Y_ORIGIN = 1<<18;
+ public static final int FLAG_CLIP_MASK = 1<<19;
+ public static final int FLAG_DASH_OFFSET = 1<<20;
+ public static final int FLAG_DASHES = 1<<21;
+ public static final int FLAG_ARC_MODE = 1<<22;
+ public enum Function {CLEAR, AND, AND_REVERSE, COPY, AND_INVERTED, NO_OP, XOR, OR, NOR, EQUIV, INVERT, OR_REVERSE, COPY_INVERTED, OR_INVERTED, NAND, SET}
+ public enum SubwindowMode {CLIP_BY_CHILDREN, INCLUDE_INFERIORS}
+ public final Drawable drawable;
+ private Function function = Function.COPY;
+ private int background = 0xffffff;
+ private int foreground = 0x000000;
+ private int lineWidth = 1;
+ private int planeMask = -1;
+ private SubwindowMode subwindowMode = SubwindowMode.CLIP_BY_CHILDREN;
+
+ public GraphicsContext(int id, Drawable drawable) {
+ super(id);
+ this.drawable = drawable;
+ }
+
+ public int getForeground() {
+ return foreground;
+ }
+
+ public void setForeground(int foreground) {
+ this.foreground = foreground;
+ }
+
+ public int getBackground() {
+ return this.background;
+ }
+
+ public void setBackground(int background) {
+ this.background = background;
+ }
+
+ public int getLineWidth() {
+ return lineWidth;
+ }
+
+ public void setLineWidth(int lineWidth) {
+ this.lineWidth = lineWidth;
+ }
+
+ public int getPlaneMask() {
+ return planeMask;
+ }
+
+ public void setPlaneMask(int planeMask) {
+ this.planeMask = planeMask;
+ }
+
+ public Function getFunction() {
+ return function;
+ }
+
+ public void setFunction(Function function) {
+ this.function = function;
+ }
+
+ public SubwindowMode getSubwindowMode() {
+ return subwindowMode;
+ }
+
+ public void setSubwindowMode(SubwindowMode subwindowMode) {
+ this.subwindowMode = subwindowMode;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/GraphicsContextManager.java b/app/src/main/java/com/winlator/xserver/GraphicsContextManager.java
new file mode 100644
index 0000000..3f4f8a0
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/GraphicsContextManager.java
@@ -0,0 +1,70 @@
+package com.winlator.xserver;
+
+import android.util.SparseArray;
+
+import com.winlator.xconnector.XInputStream;
+
+public class GraphicsContextManager extends XResourceManager {
+ private final SparseArray graphicsContexts = new SparseArray<>();
+
+ public GraphicsContext getGraphicsContext(int id) {
+ return graphicsContexts.get(id);
+ }
+
+ public GraphicsContext createGraphicsContext(int id, Drawable drawable) {
+ if (graphicsContexts.indexOfKey(id) >= 0) return null;
+ GraphicsContext graphicsContext = new GraphicsContext(id, drawable);
+ graphicsContexts.put(id, graphicsContext);
+ triggerOnCreateResourceListener(graphicsContext);
+ return graphicsContext;
+ }
+
+ public void freeGraphicsContext(int id) {
+ triggerOnFreeResourceListener(graphicsContexts.get(id));
+ graphicsContexts.remove(id);
+ }
+
+ public void updateGraphicsContext(GraphicsContext graphicsContext, Bitmask valueMask, XInputStream inputStream) {
+ for (int index : valueMask) {
+ switch (index) {
+ case GraphicsContext.FLAG_FUNCTION:
+ graphicsContext.setFunction(GraphicsContext.Function.values()[inputStream.readInt()]);
+ break;
+ case GraphicsContext.FLAG_PLANE_MASK:
+ graphicsContext.setPlaneMask(inputStream.readInt());
+ break;
+ case GraphicsContext.FLAG_FOREGROUND:
+ graphicsContext.setForeground(inputStream.readInt());
+ break;
+ case GraphicsContext.FLAG_BACKGROUND:
+ graphicsContext.setBackground(inputStream.readInt());
+ break;
+ case GraphicsContext.FLAG_LINE_WIDTH:
+ graphicsContext.setLineWidth(inputStream.readInt());
+ break;
+ case GraphicsContext.FLAG_SUBWINDOW_MODE:
+ graphicsContext.setSubwindowMode(GraphicsContext.SubwindowMode.values()[inputStream.readInt()]);
+ break;
+ case GraphicsContext.FLAG_LINE_STYLE:
+ case GraphicsContext.FLAG_CAP_STYLE:
+ case GraphicsContext.FLAG_JOIN_STYLE:
+ case GraphicsContext.FLAG_FILL_STYLE:
+ case GraphicsContext.FLAG_FILL_RULE:
+ case GraphicsContext.FLAG_GRAPHICS_EXPOSURES:
+ case GraphicsContext.FLAG_DASHES:
+ case GraphicsContext.FLAG_ARC_MODE:
+ case GraphicsContext.FLAG_TILE:
+ case GraphicsContext.FLAG_STIPPLE:
+ case GraphicsContext.FLAG_FONT:
+ case GraphicsContext.FLAG_CLIP_MASK:
+ case GraphicsContext.FLAG_TILE_STIPPLE_X_ORIGIN:
+ case GraphicsContext.FLAG_TILE_STIPPLE_Y_ORIGIN:
+ case GraphicsContext.FLAG_CLIP_X_ORIGIN:
+ case GraphicsContext.FLAG_CLIP_Y_ORIGIN:
+ case GraphicsContext.FLAG_DASH_OFFSET:
+ inputStream.skip(4);
+ break;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/IDGenerator.java b/app/src/main/java/com/winlator/xserver/IDGenerator.java
new file mode 100644
index 0000000..b675959
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/IDGenerator.java
@@ -0,0 +1,9 @@
+package com.winlator.xserver;
+
+public abstract class IDGenerator {
+ private static int id = 0;
+
+ public static int generate() {
+ return ++id;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/InputDeviceManager.java b/app/src/main/java/com/winlator/xserver/InputDeviceManager.java
new file mode 100644
index 0000000..973edac
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/InputDeviceManager.java
@@ -0,0 +1,294 @@
+package com.winlator.xserver;
+
+import com.winlator.core.CursorLocker;
+import com.winlator.winhandler.MouseEventFlags;
+import com.winlator.winhandler.WinHandler;
+import com.winlator.xserver.events.ButtonPress;
+import com.winlator.xserver.events.ButtonRelease;
+import com.winlator.xserver.events.EnterNotify;
+import com.winlator.xserver.events.Event;
+import com.winlator.xserver.events.KeyPress;
+import com.winlator.xserver.events.KeyRelease;
+import com.winlator.xserver.events.LeaveNotify;
+import com.winlator.xserver.events.MappingNotify;
+import com.winlator.xserver.events.MotionNotify;
+import com.winlator.xserver.events.PointerWindowEvent;
+
+public class InputDeviceManager implements Pointer.OnPointerMotionListener, Keyboard.OnKeyboardListener, WindowManager.OnWindowModificationListener, XResourceManager.OnResourceLifecycleListener {
+ private static final byte MOUSE_WHEEL_DELTA = 120;
+ private Window pointWindow;
+ private final XServer xServer;
+
+ public InputDeviceManager(XServer xServer) {
+ this.xServer = xServer;
+ pointWindow = xServer.windowManager.rootWindow;
+ xServer.windowManager.addOnWindowModificationListener(this);
+ xServer.windowManager.addOnResourceLifecycleListener(this);
+ xServer.pointer.addOnPointerMotionListener(this);
+ xServer.keyboard.addOnKeyboardListener(this);
+ }
+
+ @Override
+ public void onMapWindow(Window window) {
+ updatePointWindow();
+ }
+
+ @Override
+ public void onUnmapWindow(Window window) {
+ updatePointWindow();
+ }
+
+ @Override
+ public void onChangeWindowZOrder(Window window) {
+ updatePointWindow();
+ }
+
+ @Override
+ public void onUpdateWindowGeometry(Window window, boolean resized) {
+ updatePointWindow();
+ }
+
+ @Override
+ public void onCreateResource(XResource resource) {
+ updatePointWindow();
+ }
+
+ @Override
+ public void onFreeResource(XResource resource) {
+ updatePointWindow();
+ }
+
+ private void updatePointWindow() {
+ Window pointWindow = xServer.windowManager.findPointWindow(xServer);
+ sendEnterLeaveNotify(this.pointWindow, pointWindow, PointerWindowEvent.Mode.NORMAL);
+ this.pointWindow = pointWindow;
+ }
+
+ public Window getPointWindow() {
+ return pointWindow;
+ }
+
+ private void sendEvent(Window window, int eventId, Event event) {
+ Window grabWindow = xServer.grabManager.getWindow();
+ if (grabWindow != null && grabWindow.attributes.isEnabled()) {
+ EventListener eventListener = xServer.grabManager.getEventListener();
+ if (xServer.grabManager.isOwnerEvents() && window != null) {
+ window.sendEvent(eventId, event, xServer.grabManager.getClient());
+ }
+ else if (eventListener.isInterestedIn(eventId)) {
+ eventListener.sendEvent(event);
+ }
+ }
+ else if (window != null && window.attributes.isEnabled()) {
+ window.sendEvent(eventId, event);
+ }
+ }
+
+ private void sendEvent(Window window, Bitmask eventMask, Event event) {
+ Window grabWindow = xServer.grabManager.getWindow();
+ if (grabWindow != null && grabWindow.attributes.isEnabled()) {
+ EventListener eventListener = xServer.grabManager.getEventListener();
+ if (xServer.grabManager.isOwnerEvents() && window != null) {
+ window.sendEvent(eventMask, event, eventListener.client);
+ }
+ else if (eventListener.isInterestedIn(eventMask)) {
+ eventListener.sendEvent(event);
+ }
+ }
+ else if (window != null && window.attributes.isEnabled()) {
+ window.sendEvent(eventMask, event);
+ }
+ }
+
+ public void sendEnterLeaveNotify(Window windowA, Window windowB, PointerWindowEvent.Mode mode) {
+ if (windowA == windowB) return;
+ short x = xServer.pointer.getX();
+ short y = xServer.pointer.getY();
+
+ short[] localPointA = windowA.rootPointToLocal(x, y);
+ short[] localPointB = windowB.rootPointToLocal(x, y);
+
+ boolean sameScreenAndFocus = windowB.isAncestorOf(xServer.windowManager.getFocusedWindow());
+ PointerWindowEvent.Detail detailA = PointerWindowEvent.Detail.NONLINEAR;
+ PointerWindowEvent.Detail detailB = PointerWindowEvent.Detail.NONLINEAR;
+
+ if (windowA.isAncestorOf(windowB)) {
+ detailA = PointerWindowEvent.Detail.ANCESTOR;
+ detailB = PointerWindowEvent.Detail.INFERIOR;
+ }
+ else if (windowB.isAncestorOf(windowA)) {
+ detailB = PointerWindowEvent.Detail.ANCESTOR;
+ detailA = PointerWindowEvent.Detail.INFERIOR;
+ }
+
+ Bitmask keyButMask = getKeyButMask();
+ sendEvent(windowA, Event.LEAVE_WINDOW, new LeaveNotify(detailA, xServer.windowManager.rootWindow, windowA, null, x, y, localPointA[0], localPointA[1], keyButMask, mode, sameScreenAndFocus));
+ sendEvent(windowB, Event.ENTER_WINDOW, new EnterNotify(detailB, xServer.windowManager.rootWindow, windowB, null, x, y, localPointB[0], localPointB[1], keyButMask, mode, sameScreenAndFocus));
+ }
+
+ @Override
+ public void onPointerButtonPress(Pointer.Button button) {
+ if (xServer.cursorLocker.getState() == CursorLocker.State.LOCKED) {
+ WinHandler winHandler = xServer.getWinHandler();
+ int wheelDelta = button == Pointer.Button.BUTTON_SCROLL_UP ? MOUSE_WHEEL_DELTA : (button == Pointer.Button.BUTTON_SCROLL_DOWN ? -MOUSE_WHEEL_DELTA : 0);
+ winHandler.mouseEvent(MouseEventFlags.getFlagFor(button, true), 0, 0, wheelDelta);
+ }
+ else {
+ Window grabWindow = xServer.grabManager.getWindow();
+ if (grabWindow == null) {
+ grabWindow = pointWindow.getAncestorWithEventId(Event.BUTTON_PRESS);
+ if (grabWindow != null) xServer.grabManager.activatePointerGrab(grabWindow);
+ }
+
+ if (grabWindow != null && grabWindow.attributes.isEnabled()) {
+ Bitmask eventMask = createPointerEventMask();
+ eventMask.unset(button.flag());
+
+ short x = xServer.pointer.getX();
+ short y = xServer.pointer.getY();
+ short[] localPoint = grabWindow.rootPointToLocal(x, y);
+
+ Window child = grabWindow.isAncestorOf(pointWindow) ? pointWindow : null;
+ grabWindow.sendEvent(Event.BUTTON_PRESS, new ButtonPress(button.code(), xServer.windowManager.rootWindow, grabWindow, child, x, y, localPoint[0], localPoint[1], eventMask));
+ }
+ }
+ }
+
+ @Override
+ public void onPointerButtonRelease(Pointer.Button button) {
+ if (xServer.cursorLocker.getState() == CursorLocker.State.LOCKED) {
+ WinHandler winHandler = xServer.getWinHandler();
+ winHandler.mouseEvent(MouseEventFlags.getFlagFor(button, false), 0, 0, 0);
+ }
+ else {
+ Bitmask eventMask = createPointerEventMask();
+ Window grabWindow = xServer.grabManager.getWindow();
+ Window window = grabWindow == null || xServer.grabManager.isOwnerEvents() ? pointWindow.getAncestorWithEventMask(eventMask) : null;
+
+ if (grabWindow != null || window != null) {
+ Window eventWindow = window != null ? window : grabWindow;
+
+ short x = xServer.pointer.getX();
+ short y = xServer.pointer.getY();
+ short[] localPoint = eventWindow.rootPointToLocal(x, y);
+
+ Window child = eventWindow.isAncestorOf(pointWindow) ? pointWindow : null;
+ ButtonRelease buttonRelease = new ButtonRelease(button.code(), xServer.windowManager.rootWindow, eventWindow, child, x, y, localPoint[0], localPoint[1], eventMask);
+ sendEvent(window, eventMask, buttonRelease);
+ }
+
+ if (xServer.pointer.getButtonMask().isEmpty() && xServer.grabManager.isReleaseWithButtons()) {
+ xServer.grabManager.deactivatePointerGrab();
+ }
+ }
+ }
+
+ @Override
+ public void onPointerMove(short x, short y) {
+ updatePointWindow();
+ Bitmask eventMask = createPointerEventMask();
+ Window grabWindow = xServer.grabManager.getWindow();
+ Window window = grabWindow == null || xServer.grabManager.isOwnerEvents() ? pointWindow.getAncestorWithEventMask(eventMask) : null;
+
+ if (grabWindow != null || window != null) {
+ Window eventWindow = window != null ? window : grabWindow;
+ short[] localPoint = eventWindow.rootPointToLocal(x, y);
+
+ Window child = eventWindow.isAncestorOf(pointWindow) ? pointWindow : null;
+ sendEvent(window, eventMask, new MotionNotify(false, xServer.windowManager.rootWindow, eventWindow, child, x, y, localPoint[0], localPoint[1], getKeyButMask()));
+ }
+ }
+
+ @Override
+ public void onKeyPress(byte keycode, int keysym) {
+ Window focusedWindow = xServer.windowManager.getFocusedWindow();
+ if (focusedWindow == null) return;
+ updatePointWindow();
+
+ Window eventWindow = null;
+ Window child = null;
+ if (focusedWindow.isAncestorOf(pointWindow)) {
+ eventWindow = pointWindow.getAncestorWithEventId(Event.KEY_PRESS, focusedWindow);
+ child = eventWindow.isAncestorOf(pointWindow) ? pointWindow : null;
+ }
+ if (eventWindow == null) {
+ if (!focusedWindow.hasEventListenerFor(Event.KEY_PRESS)) return;
+ eventWindow = focusedWindow;
+ }
+
+ if (!eventWindow.attributes.isEnabled()) return;
+
+ Bitmask keyButMask = getKeyButMask();
+ short x = xServer.pointer.getX();
+ short y = xServer.pointer.getY();
+ short[] localPoint = eventWindow.rootPointToLocal(x, y);
+
+ if (keysym != 0 && !xServer.keyboard.hasKeysym(keycode, keysym)) {
+ xServer.keyboard.setKeysyms(keycode, keysym, keysym);
+ eventWindow.sendEvent(new MappingNotify(MappingNotify.Request.KEYBOARD, keycode, 1));
+ }
+
+ eventWindow.sendEvent(Event.KEY_PRESS, new KeyPress(keycode, xServer.windowManager.rootWindow, eventWindow, child, x, y, localPoint[0], localPoint[1], keyButMask));
+ }
+
+ @Override
+ public void onKeyRelease(byte keycode) {
+ Window focusedWindow = xServer.windowManager.getFocusedWindow();
+ if (focusedWindow == null) return;
+ updatePointWindow();
+
+ Window eventWindow = null;
+ Window child = null;
+ if (focusedWindow.isAncestorOf(pointWindow)) {
+ eventWindow = pointWindow.getAncestorWithEventId(Event.KEY_RELEASE, focusedWindow);
+ child = eventWindow.isAncestorOf(pointWindow) ? pointWindow : null;
+ }
+ if (eventWindow == null) {
+ if (!focusedWindow.hasEventListenerFor(Event.KEY_RELEASE)) return;
+ eventWindow = focusedWindow;
+ }
+
+ if (!eventWindow.attributes.isEnabled()) return;
+
+ Bitmask keyButMask = getKeyButMask();
+ short x = xServer.pointer.getX();
+ short y = xServer.pointer.getY();
+ short[] localPoint = eventWindow.rootPointToLocal(x, y);
+
+ eventWindow.sendEvent(Event.KEY_RELEASE, new KeyRelease(keycode, xServer.windowManager.rootWindow, eventWindow, child, x, y, localPoint[0], localPoint[1], keyButMask));
+ }
+
+ private Bitmask createPointerEventMask() {
+ Bitmask eventMask = new Bitmask();
+ eventMask.set(Event.POINTER_MOTION);
+
+ Bitmask buttonMask = xServer.pointer.getButtonMask();
+ if (!buttonMask.isEmpty()) {
+ eventMask.set(Event.BUTTON_MOTION);
+
+ if (buttonMask.isSet(Pointer.Button.BUTTON_LEFT.flag())) {
+ eventMask.set(Event.BUTTON1_MOTION);
+ }
+ if (buttonMask.isSet(Pointer.Button.BUTTON_MIDDLE.flag())) {
+ eventMask.set(Event.BUTTON2_MOTION);
+ }
+ if (buttonMask.isSet(Pointer.Button.BUTTON_RIGHT.flag())) {
+ eventMask.set(Event.BUTTON3_MOTION);
+ }
+ if (buttonMask.isSet(Pointer.Button.BUTTON_SCROLL_UP.flag())) {
+ eventMask.set(Event.BUTTON4_MOTION);
+ }
+ if (buttonMask.isSet(Pointer.Button.BUTTON_SCROLL_DOWN.flag())) {
+ eventMask.set(Event.BUTTON5_MOTION);
+ }
+ }
+ return eventMask;
+ }
+
+ public Bitmask getKeyButMask() {
+ Bitmask keyButMask = new Bitmask();
+ keyButMask.join(xServer.pointer.getButtonMask());
+ keyButMask.join(xServer.keyboard.getModifiersMask());
+ return keyButMask;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/Keyboard.java b/app/src/main/java/com/winlator/xserver/Keyboard.java
new file mode 100644
index 0000000..5c0b2df
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/Keyboard.java
@@ -0,0 +1,354 @@
+package com.winlator.xserver;
+
+import android.view.KeyEvent;
+
+import androidx.collection.ArraySet;
+
+import java.util.ArrayList;
+
+public class Keyboard {
+ public static final byte KEYSYMS_PER_KEYCODE = 2;
+ public static final short KEYS_COUNT = 248;
+ public static final short MAX_KEYCODE = 255;
+ public static final short MIN_KEYCODE = 8;
+ public final int[] keysyms = new int[KEYS_COUNT];
+ private final Bitmask modifiersMask = new Bitmask();
+ private final XKeycode[] keycodeMap = createKeycodeMap();
+ private final ArraySet pressedKeys = new ArraySet<>();
+ private final ArrayList onKeyboardListeners = new ArrayList<>();
+ private final XServer xServer;
+
+ public interface OnKeyboardListener {
+ void onKeyPress(byte keycode, int keysym);
+
+ void onKeyRelease(byte keycode);
+ }
+
+ public Keyboard(XServer xServer) {
+ this.xServer = xServer;
+ }
+
+ public Bitmask getModifiersMask() {
+ return modifiersMask;
+ }
+
+ public void setKeysyms(byte keycode, int minKeysym, int majKeysym) {
+ int index = keycode - 8;
+ keysyms[index*KEYSYMS_PER_KEYCODE+0] = minKeysym;
+ keysyms[index*KEYSYMS_PER_KEYCODE+1] = majKeysym;
+ }
+
+ public boolean hasKeysym(byte keycode, int keysym) {
+ int index = keycode - 8;
+ return keysyms[index*KEYSYMS_PER_KEYCODE+0] == keysym || keysyms[index*KEYSYMS_PER_KEYCODE+1] == keysym;
+ }
+
+ public void setKeyPress(byte keycode, int keysym) {
+ if (isModifierSticky(keycode)) {
+ if (pressedKeys.contains(keycode)) {
+ pressedKeys.remove(keycode);
+ modifiersMask.unset(getModifierFlag(keycode));
+ triggerOnKeyRelease(keycode);
+ }
+ else {
+ pressedKeys.add(keycode);
+ modifiersMask.set(getModifierFlag(keycode));
+ triggerOnKeyPress(keycode, keysym);
+ }
+ }
+ else if (!pressedKeys.contains(keycode)) {
+ pressedKeys.add(keycode);
+ if (isModifier(keycode)) modifiersMask.set(getModifierFlag(keycode));
+ triggerOnKeyPress(keycode, keysym);
+ }
+ }
+
+ public void setKeyRelease(byte keycode) {
+ if (!isModifierSticky(keycode) && pressedKeys.contains(keycode)) {
+ pressedKeys.remove(keycode);
+ if (isModifier(keycode)) modifiersMask.unset(getModifierFlag(keycode));
+ triggerOnKeyRelease(keycode);
+ }
+ }
+
+ public void addOnKeyboardListener(OnKeyboardListener onKeyboardListener) {
+ onKeyboardListeners.add(onKeyboardListener);
+ }
+
+ public void removeOnKeyboardListener(OnKeyboardListener onKeyboardListener) {
+ onKeyboardListeners.remove(onKeyboardListener);
+ }
+
+ private void triggerOnKeyPress(byte keycode, int keysym) {
+ for (int i = onKeyboardListeners.size()-1; i >= 0; i--) {
+ onKeyboardListeners.get(i).onKeyPress(keycode, keysym);
+ }
+ }
+
+ private void triggerOnKeyRelease(byte keycode) {
+ for (int i = onKeyboardListeners.size()-1; i >= 0; i--) {
+ onKeyboardListeners.get(i).onKeyRelease(keycode);
+ }
+ }
+
+ public boolean onKeyEvent(KeyEvent event) {
+ int action = event.getAction();
+ if (action == KeyEvent.ACTION_DOWN || action == KeyEvent.ACTION_UP) {
+ int keyCode = event.getKeyCode();
+ XKeycode xKeycode = keycodeMap[keyCode];
+ if (xKeycode == null) return false;
+
+ if (action == KeyEvent.ACTION_DOWN) {
+ boolean shiftPressed = event.isShiftPressed() || keyCode == KeyEvent.KEYCODE_AT || keyCode == KeyEvent.KEYCODE_STAR || keyCode == KeyEvent.KEYCODE_POUND || keyCode == KeyEvent.KEYCODE_PLUS;
+ if (shiftPressed) xServer.injectKeyPress(XKeycode.KEY_SHIFT_L);
+ xServer.injectKeyPress(xKeycode, event.getUnicodeChar());
+ }
+ else if (action == KeyEvent.ACTION_UP) {
+ xServer.injectKeyRelease(XKeycode.KEY_SHIFT_L);
+ xServer.injectKeyRelease(xKeycode);
+ }
+ }
+ return true;
+ }
+
+ private static XKeycode[] createKeycodeMap() {
+ XKeycode[] keycodeMap = new XKeycode[(KeyEvent.getMaxKeyCode() + 1)];
+ keycodeMap[KeyEvent.KEYCODE_ENTER] = XKeycode.KEY_ENTER;
+ keycodeMap[KeyEvent.KEYCODE_DPAD_LEFT] = XKeycode.KEY_LEFT;
+ keycodeMap[KeyEvent.KEYCODE_DPAD_RIGHT] = XKeycode.KEY_RIGHT;
+ keycodeMap[KeyEvent.KEYCODE_DPAD_UP] = XKeycode.KEY_UP;
+ keycodeMap[KeyEvent.KEYCODE_DPAD_DOWN] = XKeycode.KEY_DOWN;
+ keycodeMap[KeyEvent.KEYCODE_DEL] = XKeycode.KEY_BKSP;
+ keycodeMap[KeyEvent.KEYCODE_INSERT] = XKeycode.KEY_INSERT;
+ keycodeMap[KeyEvent.KEYCODE_FORWARD_DEL] = XKeycode.KEY_DEL;
+ keycodeMap[KeyEvent.KEYCODE_MOVE_HOME] = XKeycode.KEY_HOME;
+ keycodeMap[KeyEvent.KEYCODE_MOVE_END] = XKeycode.KEY_END;
+ keycodeMap[KeyEvent.KEYCODE_PAGE_UP] = XKeycode.KEY_PRIOR;
+ keycodeMap[KeyEvent.KEYCODE_PAGE_DOWN] = XKeycode.KEY_NEXT;
+ keycodeMap[KeyEvent.KEYCODE_SHIFT_LEFT] = XKeycode.KEY_SHIFT_L;
+ keycodeMap[KeyEvent.KEYCODE_SHIFT_RIGHT] = XKeycode.KEY_SHIFT_R;
+ keycodeMap[KeyEvent.KEYCODE_CTRL_LEFT] = XKeycode.KEY_CTRL_L;
+ keycodeMap[KeyEvent.KEYCODE_CTRL_RIGHT] = XKeycode.KEY_CTRL_R;
+ keycodeMap[KeyEvent.KEYCODE_ALT_LEFT] = XKeycode.KEY_ALT_L;
+ keycodeMap[KeyEvent.KEYCODE_ALT_RIGHT] = XKeycode.KEY_ALT_R;
+ keycodeMap[KeyEvent.KEYCODE_TAB] = XKeycode.KEY_TAB;
+ keycodeMap[KeyEvent.KEYCODE_SPACE] = XKeycode.KEY_SPACE;
+ keycodeMap[KeyEvent.KEYCODE_A] = XKeycode.KEY_A;
+ keycodeMap[KeyEvent.KEYCODE_B] = XKeycode.KEY_B;
+ keycodeMap[KeyEvent.KEYCODE_C] = XKeycode.KEY_C;
+ keycodeMap[KeyEvent.KEYCODE_D] = XKeycode.KEY_D;
+ keycodeMap[KeyEvent.KEYCODE_E] = XKeycode.KEY_E;
+ keycodeMap[KeyEvent.KEYCODE_F] = XKeycode.KEY_F;
+ keycodeMap[KeyEvent.KEYCODE_G] = XKeycode.KEY_G;
+ keycodeMap[KeyEvent.KEYCODE_H] = XKeycode.KEY_H;
+ keycodeMap[KeyEvent.KEYCODE_I] = XKeycode.KEY_I;
+ keycodeMap[KeyEvent.KEYCODE_J] = XKeycode.KEY_J;
+ keycodeMap[KeyEvent.KEYCODE_K] = XKeycode.KEY_K;
+ keycodeMap[KeyEvent.KEYCODE_L] = XKeycode.KEY_L;
+ keycodeMap[KeyEvent.KEYCODE_M] = XKeycode.KEY_M;
+ keycodeMap[KeyEvent.KEYCODE_N] = XKeycode.KEY_N;
+ keycodeMap[KeyEvent.KEYCODE_O] = XKeycode.KEY_O;
+ keycodeMap[KeyEvent.KEYCODE_P] = XKeycode.KEY_P;
+ keycodeMap[KeyEvent.KEYCODE_Q] = XKeycode.KEY_Q;
+ keycodeMap[KeyEvent.KEYCODE_R] = XKeycode.KEY_R;
+ keycodeMap[KeyEvent.KEYCODE_S] = XKeycode.KEY_S;
+ keycodeMap[KeyEvent.KEYCODE_T] = XKeycode.KEY_T;
+ keycodeMap[KeyEvent.KEYCODE_U] = XKeycode.KEY_U;
+ keycodeMap[KeyEvent.KEYCODE_V] = XKeycode.KEY_V;
+ keycodeMap[KeyEvent.KEYCODE_W] = XKeycode.KEY_W;
+ keycodeMap[KeyEvent.KEYCODE_X] = XKeycode.KEY_X;
+ keycodeMap[KeyEvent.KEYCODE_Y] = XKeycode.KEY_Y;
+ keycodeMap[KeyEvent.KEYCODE_Z] = XKeycode.KEY_Z;
+ keycodeMap[KeyEvent.KEYCODE_0] = XKeycode.KEY_0;
+ keycodeMap[KeyEvent.KEYCODE_1] = XKeycode.KEY_1;
+ keycodeMap[KeyEvent.KEYCODE_2] = XKeycode.KEY_2;
+ keycodeMap[KeyEvent.KEYCODE_3] = XKeycode.KEY_3;
+ keycodeMap[KeyEvent.KEYCODE_4] = XKeycode.KEY_4;
+ keycodeMap[KeyEvent.KEYCODE_5] = XKeycode.KEY_5;
+ keycodeMap[KeyEvent.KEYCODE_6] = XKeycode.KEY_6;
+ keycodeMap[KeyEvent.KEYCODE_7] = XKeycode.KEY_7;
+ keycodeMap[KeyEvent.KEYCODE_8] = XKeycode.KEY_8;
+ keycodeMap[KeyEvent.KEYCODE_9] = XKeycode.KEY_9;
+ keycodeMap[KeyEvent.KEYCODE_STAR] = XKeycode.KEY_8;
+ keycodeMap[KeyEvent.KEYCODE_POUND] = XKeycode.KEY_3;
+ keycodeMap[KeyEvent.KEYCODE_COMMA] = XKeycode.KEY_COMMA;
+ keycodeMap[KeyEvent.KEYCODE_PERIOD] = XKeycode.KEY_PERIOD;
+ keycodeMap[KeyEvent.KEYCODE_SEMICOLON] = XKeycode.KEY_SEMICOLON;
+ keycodeMap[KeyEvent.KEYCODE_APOSTROPHE] = XKeycode.KEY_APOSTROPHE;
+ keycodeMap[KeyEvent.KEYCODE_LEFT_BRACKET] = XKeycode.KEY_BRACKET_LEFT;
+ keycodeMap[KeyEvent.KEYCODE_RIGHT_BRACKET] = XKeycode.KEY_BRACKET_RIGHT;
+ keycodeMap[KeyEvent.KEYCODE_GRAVE] = XKeycode.KEY_GRAVE;
+ keycodeMap[KeyEvent.KEYCODE_MINUS] = XKeycode.KEY_MINUS;
+ keycodeMap[KeyEvent.KEYCODE_PLUS] = XKeycode.KEY_EQUAL;
+ keycodeMap[KeyEvent.KEYCODE_EQUALS] = XKeycode.KEY_EQUAL;
+ keycodeMap[KeyEvent.KEYCODE_SLASH] = XKeycode.KEY_SLASH;
+ keycodeMap[KeyEvent.KEYCODE_AT] = XKeycode.KEY_2;
+ keycodeMap[KeyEvent.KEYCODE_BACKSLASH] = XKeycode.KEY_BACKSLASH;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_DIVIDE] = XKeycode.KEY_KP_DIVIDE;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_MULTIPLY] = XKeycode.KEY_KP_MULTIPLY;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_SUBTRACT] = XKeycode.KEY_KP_SUBTRACT;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_ADD] = XKeycode.KEY_KP_ADD;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_DOT] = XKeycode.KEY_KP_DEL;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_0] = XKeycode.KEY_KP_0;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_1] = XKeycode.KEY_KP_1;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_2] = XKeycode.KEY_KP_2;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_3] = XKeycode.KEY_KP_3;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_4] = XKeycode.KEY_KP_4;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_5] = XKeycode.KEY_KP_5;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_6] = XKeycode.KEY_KP_6;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_7] = XKeycode.KEY_KP_7;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_8] = XKeycode.KEY_KP_8;
+ keycodeMap[KeyEvent.KEYCODE_NUMPAD_9] = XKeycode.KEY_KP_9;
+ keycodeMap[KeyEvent.KEYCODE_F1] = XKeycode.KEY_F1;
+ keycodeMap[KeyEvent.KEYCODE_F2] = XKeycode.KEY_F2;
+ keycodeMap[KeyEvent.KEYCODE_F3] = XKeycode.KEY_F3;
+ keycodeMap[KeyEvent.KEYCODE_F4] = XKeycode.KEY_F4;
+ keycodeMap[KeyEvent.KEYCODE_F5] = XKeycode.KEY_F5;
+ keycodeMap[KeyEvent.KEYCODE_F6] = XKeycode.KEY_F6;
+ keycodeMap[KeyEvent.KEYCODE_F7] = XKeycode.KEY_F7;
+ keycodeMap[KeyEvent.KEYCODE_F8] = XKeycode.KEY_F8;
+ keycodeMap[KeyEvent.KEYCODE_F9] = XKeycode.KEY_F9;
+ keycodeMap[KeyEvent.KEYCODE_F10] = XKeycode.KEY_F10;
+ keycodeMap[KeyEvent.KEYCODE_F11] = XKeycode.KEY_F11;
+ keycodeMap[KeyEvent.KEYCODE_F12] = XKeycode.KEY_F12;
+ keycodeMap[KeyEvent.KEYCODE_NUM_LOCK] = XKeycode.KEY_NUM_LOCK;
+ keycodeMap[KeyEvent.KEYCODE_CAPS_LOCK] = XKeycode.KEY_CAPS_LOCK;
+ return keycodeMap;
+ }
+
+ public static Keyboard createKeyboard(XServer xServer) {
+ Keyboard keyboard = new Keyboard(xServer);
+ keyboard.setKeysyms(XKeycode.KEY_ESC.id, 65307, 0);
+ keyboard.setKeysyms(XKeycode.KEY_ENTER.id, 65293, 0);
+ keyboard.setKeysyms(XKeycode.KEY_RIGHT.id, 65363, 0);
+ keyboard.setKeysyms(XKeycode.KEY_UP.id, 65362, 0);
+ keyboard.setKeysyms(XKeycode.KEY_LEFT.id, 65361, 0);
+ keyboard.setKeysyms(XKeycode.KEY_DOWN.id, 65364, 0);
+ keyboard.setKeysyms(XKeycode.KEY_DEL.id, 65535, 0);
+ keyboard.setKeysyms(XKeycode.KEY_BKSP.id, 65288, 0);
+ keyboard.setKeysyms(XKeycode.KEY_INSERT.id, 65379, 0);
+ keyboard.setKeysyms(XKeycode.KEY_PRIOR.id, 65365, 0);
+ keyboard.setKeysyms(XKeycode.KEY_NEXT.id, 65366, 0);
+ keyboard.setKeysyms(XKeycode.KEY_HOME.id, 65360, 0);
+ keyboard.setKeysyms(XKeycode.KEY_END.id, 65367, 0);
+ keyboard.setKeysyms(XKeycode.KEY_SHIFT_L.id, 65505, 0);
+ keyboard.setKeysyms(XKeycode.KEY_SHIFT_R.id, 65506, 0);
+ keyboard.setKeysyms(XKeycode.KEY_CTRL_L.id, 65507, 0);
+ keyboard.setKeysyms(XKeycode.KEY_CTRL_R.id, 65508, 0);
+ keyboard.setKeysyms(XKeycode.KEY_ALT_L.id, 65511, 0);
+ keyboard.setKeysyms(XKeycode.KEY_ALT_R.id, 65512, 0);
+ keyboard.setKeysyms(XKeycode.KEY_TAB.id, 65289, 0);
+ keyboard.setKeysyms(XKeycode.KEY_SPACE.id, 32, 32);
+ keyboard.setKeysyms(XKeycode.KEY_A.id, 97, 65);
+ keyboard.setKeysyms(XKeycode.KEY_B.id, 98, 66);
+ keyboard.setKeysyms(XKeycode.KEY_C.id, 99, 67);
+ keyboard.setKeysyms(XKeycode.KEY_D.id, 100, 68);
+ keyboard.setKeysyms(XKeycode.KEY_E.id, 101, 69);
+ keyboard.setKeysyms(XKeycode.KEY_F.id, 102, 70);
+ keyboard.setKeysyms(XKeycode.KEY_G.id, 103, 71);
+ keyboard.setKeysyms(XKeycode.KEY_H.id, 104, 72);
+ keyboard.setKeysyms(XKeycode.KEY_I.id, 105, 73);
+ keyboard.setKeysyms(XKeycode.KEY_J.id, 106, 74);
+ keyboard.setKeysyms(XKeycode.KEY_K.id, 107, 75);
+ keyboard.setKeysyms(XKeycode.KEY_L.id, 108, 76);
+ keyboard.setKeysyms(XKeycode.KEY_M.id, 109, 77);
+ keyboard.setKeysyms(XKeycode.KEY_N.id, 110, 78);
+ keyboard.setKeysyms(XKeycode.KEY_O.id, 111, 79);
+ keyboard.setKeysyms(XKeycode.KEY_P.id, 112, 80);
+ keyboard.setKeysyms(XKeycode.KEY_Q.id, 113, 81);
+ keyboard.setKeysyms(XKeycode.KEY_R.id, 114, 82);
+ keyboard.setKeysyms(XKeycode.KEY_S.id, 115, 83);
+ keyboard.setKeysyms(XKeycode.KEY_T.id, 116, 84);
+ keyboard.setKeysyms(XKeycode.KEY_U.id, 117, 85);
+ keyboard.setKeysyms(XKeycode.KEY_V.id, 118, 86);
+ keyboard.setKeysyms(XKeycode.KEY_W.id, 119, 87);
+ keyboard.setKeysyms(XKeycode.KEY_X.id, 120, 88);
+ keyboard.setKeysyms(XKeycode.KEY_Y.id, 121, 89);
+ keyboard.setKeysyms(XKeycode.KEY_Z.id, 122, 90);
+ keyboard.setKeysyms(XKeycode.KEY_1.id, 49, 33);
+ keyboard.setKeysyms(XKeycode.KEY_2.id, 50, 64);
+ keyboard.setKeysyms(XKeycode.KEY_3.id, 51, 35);
+ keyboard.setKeysyms(XKeycode.KEY_4.id, 52, 36);
+ keyboard.setKeysyms(XKeycode.KEY_5.id, 53, 37);
+ keyboard.setKeysyms(XKeycode.KEY_6.id, 54, 94);
+ keyboard.setKeysyms(XKeycode.KEY_7.id, 55, 38);
+ keyboard.setKeysyms(XKeycode.KEY_8.id, 56, 42);
+ keyboard.setKeysyms(XKeycode.KEY_9.id, 57, 40);
+ keyboard.setKeysyms(XKeycode.KEY_0.id, 48, 41);
+ keyboard.setKeysyms(XKeycode.KEY_COMMA.id, 44, 60);
+ keyboard.setKeysyms(XKeycode.KEY_PERIOD.id, 46, 62);
+ keyboard.setKeysyms(XKeycode.KEY_SEMICOLON.id, 59, 58);
+ keyboard.setKeysyms(XKeycode.KEY_APOSTROPHE.id, 39, 34);
+ keyboard.setKeysyms(XKeycode.KEY_BRACKET_LEFT.id, 91, 123);
+ keyboard.setKeysyms(XKeycode.KEY_BRACKET_RIGHT.id, 93, 125);
+ keyboard.setKeysyms(XKeycode.KEY_GRAVE.id, 96, 126);
+ keyboard.setKeysyms(XKeycode.KEY_MINUS.id, 45, 95);
+ keyboard.setKeysyms(XKeycode.KEY_EQUAL.id, 61, 43);
+ keyboard.setKeysyms(XKeycode.KEY_SLASH.id, 47, 63);
+ keyboard.setKeysyms(XKeycode.KEY_BACKSLASH.id, 92, 124);
+ keyboard.setKeysyms(XKeycode.KEY_KP_DIVIDE.id, 65455, 65455);
+ keyboard.setKeysyms(XKeycode.KEY_KP_MULTIPLY.id, 65450, 65450);
+ keyboard.setKeysyms(XKeycode.KEY_KP_SUBTRACT.id, 65453, 65453);
+ keyboard.setKeysyms(XKeycode.KEY_KP_ADD.id, 65451, 65451);
+ keyboard.setKeysyms(XKeycode.KEY_KP_0.id, 65456, 65438);
+ keyboard.setKeysyms(XKeycode.KEY_KP_1.id, 65457, 65436);
+ keyboard.setKeysyms(XKeycode.KEY_KP_2.id, 65458, 65433);
+ keyboard.setKeysyms(XKeycode.KEY_KP_3.id, 65459, 65459);
+ keyboard.setKeysyms(XKeycode.KEY_KP_4.id, 65460, 65430);
+ keyboard.setKeysyms(XKeycode.KEY_KP_5.id, 65461, 65461);
+ keyboard.setKeysyms(XKeycode.KEY_KP_6.id, 65462, 65432);
+ keyboard.setKeysyms(XKeycode.KEY_KP_7.id, 65463, 65429);
+ keyboard.setKeysyms(XKeycode.KEY_KP_8.id, 65464, 65431);
+ keyboard.setKeysyms(XKeycode.KEY_KP_9.id, 65465, 65465);
+ keyboard.setKeysyms(XKeycode.KEY_KP_DEL.id, 65439, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F1.id, 65470, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F2.id, 65471, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F3.id, 65472, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F4.id, 65473, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F5.id, 65474, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F6.id, 65475, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F7.id, 65476, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F8.id, 65477, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F9.id, 65478, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F10.id, 65479, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F11.id, 65480, 0);
+ keyboard.setKeysyms(XKeycode.KEY_F12.id, 65481, 0);
+ return keyboard;
+ }
+
+ public static boolean isModifier(byte keycode) {
+ return
+ keycode == XKeycode.KEY_SHIFT_L.id ||
+ keycode == XKeycode.KEY_SHIFT_R.id ||
+ keycode == XKeycode.KEY_CTRL_L.id ||
+ keycode == XKeycode.KEY_CTRL_R.id ||
+ keycode == XKeycode.KEY_ALT_L.id ||
+ keycode == XKeycode.KEY_ALT_R.id ||
+ keycode == XKeycode.KEY_CAPS_LOCK.id ||
+ keycode == XKeycode.KEY_NUM_LOCK.id
+ ;
+ }
+
+ public static int getModifierFlag(byte keycode) {
+ if (keycode == XKeycode.KEY_SHIFT_L.id || keycode == XKeycode.KEY_SHIFT_R.id) {
+ return 1;
+ }
+ else if (keycode == XKeycode.KEY_CAPS_LOCK.id) {
+ return 2;
+ }
+ else if (keycode == XKeycode.KEY_CTRL_L.id || keycode == XKeycode.KEY_CTRL_R.id) {
+ return 4;
+ }
+ else if (keycode == XKeycode.KEY_ALT_L.id || keycode == XKeycode.KEY_ALT_R.id) {
+ return 8;
+ }
+ else if (keycode == XKeycode.KEY_NUM_LOCK.id) {
+ return 16;
+ }
+ return 0;
+ }
+
+ public static boolean isModifierSticky(byte keycode) {
+ return keycode == XKeycode.KEY_CAPS_LOCK.id || keycode == XKeycode.KEY_NUM_LOCK.id;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/Pixmap.java b/app/src/main/java/com/winlator/xserver/Pixmap.java
new file mode 100644
index 0000000..ec7b0c6
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/Pixmap.java
@@ -0,0 +1,10 @@
+package com.winlator.xserver;
+
+public class Pixmap extends XResource {
+ public final Drawable drawable;
+
+ public Pixmap(Drawable drawable) {
+ super(drawable.id);
+ this.drawable = drawable;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/PixmapFormat.java b/app/src/main/java/com/winlator/xserver/PixmapFormat.java
new file mode 100644
index 0000000..b29fb3f
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/PixmapFormat.java
@@ -0,0 +1,13 @@
+package com.winlator.xserver;
+
+public class PixmapFormat {
+ public final byte depth;
+ public final byte bitsPerPixel;
+ public final byte scanlinePad;
+
+ public PixmapFormat(int depth, int bitsPerPixel, int scanlinePad) {
+ this.depth = (byte)depth;
+ this.bitsPerPixel = (byte)bitsPerPixel;
+ this.scanlinePad = (byte)scanlinePad;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/PixmapManager.java b/app/src/main/java/com/winlator/xserver/PixmapManager.java
new file mode 100644
index 0000000..b3a0509
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/PixmapManager.java
@@ -0,0 +1,54 @@
+package com.winlator.xserver;
+
+import android.util.SparseArray;
+
+public class PixmapManager extends XResourceManager {
+ public final Visual visual;
+ public final Visual[] supportedVisuals;
+ public final PixmapFormat[] supportedPixmapFormats;
+ private final SparseArray pixmaps = new SparseArray<>();
+
+ public PixmapManager() {
+ visual = new Visual(IDGenerator.generate(), true, 32, 24, 0xff0000, 0x00ff00, 0x0000ff);
+ supportedVisuals = new Visual[]{visual, new Visual(IDGenerator.generate(), false, 1, 1, 0, 0, 0)};
+
+ supportedPixmapFormats = new PixmapFormat[] {
+ new PixmapFormat(1, 1, 32),
+ new PixmapFormat(24, 32, 32),
+ new PixmapFormat(32, 32, 32)
+ };
+ }
+
+ public Pixmap getPixmap(int id) {
+ return pixmaps.get(id);
+ }
+
+ public Pixmap createPixmap(Drawable drawable) {
+ if (pixmaps.indexOfKey(drawable.id) >= 0) return null;
+ Pixmap pixmap = new Pixmap(drawable);
+ pixmaps.put(drawable.id, pixmap);
+ triggerOnCreateResourceListener(pixmap);
+ return pixmap;
+ }
+
+ public void freePixmap(int id) {
+ triggerOnFreeResourceListener(pixmaps.get(id));
+ pixmaps.remove(id);
+ }
+
+ public Visual getVisualForDepth(byte depth) {
+ if (depth == visual.depth) return visual;
+ for (Visual visual : supportedVisuals) {
+ if (depth == visual.depth) return visual;
+ }
+ return null;
+ }
+
+ public Visual getVisual(int id) {
+ if (id == visual.id) return visual;
+ for (Visual visual : supportedVisuals) {
+ if (id == visual.id && visual.displayable) return visual;
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/Pointer.java b/app/src/main/java/com/winlator/xserver/Pointer.java
new file mode 100644
index 0000000..7bf958d
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/Pointer.java
@@ -0,0 +1,110 @@
+package com.winlator.xserver;
+
+import com.winlator.math.Mathf;
+
+import java.util.ArrayList;
+
+public class Pointer {
+ public enum Button {
+ BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT, BUTTON_SCROLL_UP, BUTTON_SCROLL_DOWN, BUTTON_SCROLL_CLICK_LEFT, BUTTON_SCROLL_CLICK_RIGHT;
+
+ public byte code() {
+ return (byte)(ordinal() + 1);
+ }
+
+ public int flag() {
+ return 1<<(code() + MAX_BUTTONS);
+ }
+ }
+ public static final byte MAX_BUTTONS = 7;
+ private final ArrayList onPointerMotionListeners = new ArrayList<>();
+ private final Bitmask buttonMask = new Bitmask();
+ private final XServer xServer;
+ private short x;
+ private short y;
+
+ public interface OnPointerMotionListener {
+ default void onPointerButtonPress(Button button) {}
+ default void onPointerButtonRelease(Button button) {}
+ default void onPointerMove(short x, short y) {}
+ }
+
+ public Pointer(XServer xServer) {
+ this.xServer = xServer;
+ }
+
+ public void setX(int x) {
+ this.x = (short)x;
+ }
+
+ public void setY(int y) {
+ this.y = (short)y;
+ }
+
+ public short getX() {
+ return x;
+ }
+
+ public short getY() {
+ return y;
+ }
+
+ public short getClampedX() {
+ return (short)Mathf.clamp(x, 0, xServer.screenInfo.width -1);
+ }
+
+ public short getClampedY() {
+ return (short)Mathf.clamp(y, 0, xServer.screenInfo.height -1);
+ }
+
+ public void moveTo(int x, int y) {
+ setX(x);
+ setY(y);
+ triggerOnPointerMove(this.x, this.y);
+ }
+
+ public Bitmask getButtonMask() {
+ return buttonMask;
+ }
+
+ public void setButton(Button button, boolean pressed) {
+ boolean oldPressed = isButtonPressed(button);
+ buttonMask.set(button.flag(), pressed);
+ if (oldPressed != pressed) {
+ if (pressed) {
+ triggerOnPointerButtonPress(button);
+ }
+ else triggerOnPointerButtonRelease(button);
+ }
+ }
+
+ public boolean isButtonPressed(Button button) {
+ return buttonMask.isSet(button.flag());
+ }
+
+ public void addOnPointerMotionListener(OnPointerMotionListener onPointerMotionListener) {
+ onPointerMotionListeners.add(onPointerMotionListener);
+ }
+
+ public void removeOnPointerMotionListener(OnPointerMotionListener onPointerMotionListener) {
+ onPointerMotionListeners.remove(onPointerMotionListener);
+ }
+
+ private void triggerOnPointerButtonPress(Button button) {
+ for (int i = onPointerMotionListeners.size()-1; i >= 0; i--) {
+ onPointerMotionListeners.get(i).onPointerButtonPress(button);
+ }
+ }
+
+ private void triggerOnPointerButtonRelease(Button button) {
+ for (int i = onPointerMotionListeners.size()-1; i >= 0; i--) {
+ onPointerMotionListeners.get(i).onPointerButtonRelease(button);
+ }
+ }
+
+ private void triggerOnPointerMove(short x, short y) {
+ for (int i = onPointerMotionListeners.size()-1; i >= 0; i--) {
+ onPointerMotionListeners.get(i).onPointerMove(x, y);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/Property.java b/app/src/main/java/com/winlator/xserver/Property.java
new file mode 100644
index 0000000..f14107b
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/Property.java
@@ -0,0 +1,90 @@
+package com.winlator.xserver;
+
+import androidx.annotation.NonNull;
+
+import com.winlator.core.ArrayUtils;
+import com.winlator.core.StringUtils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+
+public class Property {
+ public enum Mode {REPLACE, PREPEND, APPEND}
+ public enum Format {
+ BYTE_ARRAY(8), SHORT_ARRAY(16), INT_ARRAY(32);
+ public final byte value;
+
+ Format(int value) {
+ this.value = (byte)value;
+ }
+
+ public static Format valueOf(int format) {
+ switch (format) {
+ case 8: return BYTE_ARRAY;
+ case 16: return SHORT_ARRAY;
+ case 32: return INT_ARRAY;
+ }
+ return null;
+ }
+ }
+ public final int name;
+ public final int type;
+ public final Format format;
+ private byte[] data;
+
+ public Property(int name, int type, Format format, byte[] data) {
+ this.name = name;
+ this.type = type;
+ this.format = format;
+ replace(data);
+ }
+
+ public byte[] data() {
+ return data;
+ }
+
+ public void replace(byte[] data) {
+ this.data = data != null ? data : new byte[0];
+ }
+
+ public void prepend(byte[] values) {
+ this.data = ArrayUtils.concat(values, this.data);
+ }
+
+ public void append(byte[] values) {
+ this.data = ArrayUtils.concat(this.data, values);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ String type = Atom.getName(this.type);
+ switch (type) {
+ case "UTF8_STRING":
+ return StringUtils.fromANSIString(data, StandardCharsets.UTF_8);
+ case "STRING":
+ return StringUtils.fromANSIString(data, XServer.LATIN1_CHARSET);
+ case "ATOM":
+ return Atom.getName(ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN).getInt());
+ default:
+ StringBuilder sb = new StringBuilder();
+ ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
+ for (int i = 0, size = data.length / (format.value >> 3); i < size; i++) {
+ if (i > 0) sb.append(",");
+ switch (format) {
+ case BYTE_ARRAY:
+ sb.append(buffer.get());
+ break;
+ case SHORT_ARRAY:
+ sb.append(buffer.getShort());
+ break;
+ case INT_ARRAY:
+ sb.append(buffer.getInt());
+ break;
+ }
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/ResourceIDs.java b/app/src/main/java/com/winlator/xserver/ResourceIDs.java
new file mode 100644
index 0000000..17c3f6a
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/ResourceIDs.java
@@ -0,0 +1,34 @@
+package com.winlator.xserver;
+
+import androidx.collection.ArraySet;
+
+import java.util.Iterator;
+
+public class ResourceIDs {
+ private final ArraySet idBases = new ArraySet<>();
+ public final int idMask;
+
+ public ResourceIDs(int maxClients) {
+ int clientsBits = 32 - Integer.numberOfLeadingZeros(maxClients);
+ clientsBits = Integer.bitCount(maxClients) == 1 ? clientsBits - 1 : clientsBits;
+ int base = 29 - clientsBits;
+ idMask = (1 << base) - 1;
+ for (int i = 1; i < maxClients; i++) idBases.add(i << base);
+ }
+
+ public synchronized Integer get() {
+ if (idBases.isEmpty()) return -1;
+ Iterator iter = idBases.iterator();
+ int idBase = iter.next();
+ iter.remove();
+ return idBase;
+ }
+
+ public boolean isInInterval(int value, int idBase) {
+ return (value | idMask) == (idBase | idMask);
+ }
+
+ public synchronized void free(Integer idBase) {
+ idBases.add(idBase);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/SHMSegmentManager.java b/app/src/main/java/com/winlator/xserver/SHMSegmentManager.java
new file mode 100644
index 0000000..377288b
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/SHMSegmentManager.java
@@ -0,0 +1,36 @@
+package com.winlator.xserver;
+
+import android.util.SparseArray;
+
+import com.winlator.sysvshm.SysVSharedMemory;
+import com.winlator.xserver.errors.BadAccess;
+import com.winlator.xserver.errors.BadSHMSegment;
+
+import java.nio.ByteBuffer;
+
+public class SHMSegmentManager {
+ private final SysVSharedMemory sysVSharedMemory;
+ private final SparseArray shmSegments = new SparseArray<>();
+
+ public SHMSegmentManager(SysVSharedMemory sysVSharedMemory) {
+ this.sysVSharedMemory = sysVSharedMemory;
+ }
+
+ public void attach(int xid, int shmid) throws BadAccess, BadSHMSegment {
+ if (shmSegments.indexOfKey(xid) >= 0) detach(xid);
+ ByteBuffer data = sysVSharedMemory.attach(shmid);
+ if (data == null) throw new BadAccess();
+ shmSegments.put(xid, data);
+ }
+
+ public void detach(int xid) throws BadSHMSegment {
+ ByteBuffer data = shmSegments.get(xid);
+ if (data == null) throw new BadSHMSegment(xid);
+ sysVSharedMemory.detach(data);
+ shmSegments.remove(xid);
+ }
+
+ public ByteBuffer getData(int xid) {
+ return shmSegments.get(xid);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/ScreenInfo.java b/app/src/main/java/com/winlator/xserver/ScreenInfo.java
new file mode 100644
index 0000000..17a7a13
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/ScreenInfo.java
@@ -0,0 +1,30 @@
+package com.winlator.xserver;
+
+public class ScreenInfo {
+ public final short width;
+ public final short height;
+
+ public ScreenInfo(String value) {
+ String[] parts = value.split("x");
+ width = Short.parseShort(parts[0]);
+ height = Short.parseShort(parts[1]);
+ }
+
+ public ScreenInfo(int width, int height) {
+ this.width = (short)width;
+ this.height = (short)height;
+ }
+
+ public short getWidthInMillimeters() {
+ return (short)(width / 10);
+ }
+
+ public short getHeightInMillimeters() {
+ return (short)(height / 10);
+ }
+
+ @Override
+ public String toString() {
+ return width+"x"+height;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/SelectionManager.java b/app/src/main/java/com/winlator/xserver/SelectionManager.java
new file mode 100644
index 0000000..f148dcd
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/SelectionManager.java
@@ -0,0 +1,43 @@
+package com.winlator.xserver;
+
+import android.util.SparseArray;
+
+import com.winlator.xserver.events.SelectionClear;
+
+public class SelectionManager implements XResourceManager.OnResourceLifecycleListener {
+ private final SparseArray selections = new SparseArray<>();
+
+ public SelectionManager(WindowManager windowManager) {
+ windowManager.addOnResourceLifecycleListener(this);
+ }
+
+ public static class Selection {
+ public Window owner;
+ private XClient client;
+ }
+
+ public void setSelection(int atom, Window owner, XClient client, int timestamp) {
+ Selection selection = getSelection(atom);
+ if (selection.owner != null && (owner == null || selection.client != client)) {
+ selection.client.sendEvent(new SelectionClear(timestamp, owner, atom));
+ }
+ selection.owner = owner;
+ selection.client = client;
+ }
+
+ public Selection getSelection(int atom) {
+ Selection selection = selections.get(atom);
+ if (selection != null) return selection;
+ selection = new Selection();
+ selections.put(atom, selection);
+ return selection;
+ }
+
+ @Override
+ public void onFreeResource(XResource resource) {
+ for (int i = 0; i < selections.size(); i++) {
+ Selection selection = selections.valueAt(i);
+ if (selection.owner == resource) selection.owner = null;
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/Visual.java b/app/src/main/java/com/winlator/xserver/Visual.java
new file mode 100644
index 0000000..c5ee7ef
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/Visual.java
@@ -0,0 +1,23 @@
+package com.winlator.xserver;
+
+public class Visual {
+ public final int id;
+ public final byte visualClass = 4;
+ public final boolean displayable;
+ public final byte depth;
+ public final byte bitsPerRGBValue;
+ public final short colormapEntries = 256;
+ public final int blueMask;
+ public final int greenMask;
+ public final int redMask;
+
+ public Visual(int id, boolean displayable, int depth, int bitsPerRGBValue, int redMask, int greenMask, int blueMask) {
+ this.id = id;
+ this.displayable = displayable;
+ this.depth = (byte)depth;
+ this.bitsPerRGBValue = (byte)bitsPerRGBValue;
+ this.redMask = redMask;
+ this.greenMask = greenMask;
+ this.blueMask = blueMask;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/Window.java b/app/src/main/java/com/winlator/xserver/Window.java
new file mode 100644
index 0000000..9e492a1
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/Window.java
@@ -0,0 +1,377 @@
+package com.winlator.xserver;
+
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+
+import com.winlator.xserver.events.Event;
+import com.winlator.xserver.events.PropertyNotify;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Stack;
+
+public class Window extends XResource {
+ public static final int FLAG_X = 1;
+ public static final int FLAG_Y = 1<<1;
+ public static final int FLAG_WIDTH = 1<<2;
+ public static final int FLAG_HEIGHT = 1<<3;
+ public static final int FLAG_BORDER_WIDTH = 1<<4;
+ public static final int FLAG_SIBLING = 1<<5;
+ public static final int FLAG_STACK_MODE = 1<<6;
+ public enum StackMode {ABOVE, BELOW, TOP_IF, BOTTOM_IF, OPPOSITE}
+ public enum MapState {UNMAPPED, UNVIEWABLE, VIEWABLE}
+ private Drawable content;
+ private short x;
+ private short y;
+ private short width;
+ private short height;
+ private short borderWidth;
+ private Window parent;
+ public final XClient originClient;
+ public final WindowAttributes attributes = new WindowAttributes(this);
+ private final SparseArray properties = new SparseArray<>();
+ private final ArrayList children = new ArrayList<>();
+ private final List immutableChildren = Collections.unmodifiableList(children);
+ private final ArrayList eventListeners = new ArrayList<>();
+
+ public Window(int id, Drawable content, int x, int y, int width, int height, XClient originClient) {
+ super(id);
+ this.content = content;
+ this.x = (short)x;
+ this.y = (short)y;
+ this.width = (short)width;
+ this.height = (short)height;
+ this.originClient = originClient;
+ }
+
+ public short getX() {
+ return x;
+ }
+
+ public void setX(short x) {
+ this.x = x;
+ }
+
+ public short getY() {
+ return y;
+ }
+
+ public void setY(short y) {
+ this.y = y;
+ }
+
+ public short getWidth() {
+ return width;
+ }
+
+ public void setWidth(short width) {
+ this.width = width;
+ }
+
+ public short getHeight() {
+ return height;
+ }
+
+ public void setHeight(short height) {
+ this.height = height;
+ }
+
+ public short getBorderWidth() {
+ return borderWidth;
+ }
+
+ public void setBorderWidth(short borderWidth) {
+ this.borderWidth = borderWidth;
+ }
+
+ public Drawable getContent() {
+ return content;
+ }
+
+ public void setContent(Drawable content) {
+ this.content = content;
+ }
+
+ public Window getParent() {
+ return parent;
+ }
+
+ public void setParent(Window parent) {
+ this.parent = parent;
+ }
+
+ public Property getProperty(int id) {
+ return properties.get(id);
+ }
+
+ @NonNull
+ public String getPropertyValue(String name) {
+ Property property = getProperty(Atom.getId(name));
+ return property != null ? property.toString() : "";
+ }
+
+ public void addProperty(Property property) {
+ properties.put(property.name, property);
+ }
+
+ public void removeProperty(int id) {
+ properties.remove(id);
+ sendEvent(Event.PROPERTY_CHANGE, new PropertyNotify(this, id, true));
+ }
+
+ public boolean modifyProperty(int atom, int type, Property.Format format, Property.Mode mode, byte[] data) {
+ Property property = getProperty(atom);
+ boolean modified = false;
+ if (property == null) {
+ addProperty(new Property(atom, type, format, data));
+ modified = true;
+ }
+ else if (mode == Property.Mode.REPLACE) {
+ if (property.format == format) {
+ property.replace(data);
+ }
+ else properties.put(atom, new Property(atom, type, format, data));
+ modified = true;
+ }
+ else if (property.format == format && property.type == type) {
+ if (mode == Property.Mode.PREPEND) {
+ property.prepend(data);
+ }
+ else if (mode == Property.Mode.APPEND) {
+ property.append(data);
+ }
+ modified = true;
+ }
+
+ if (modified) sendEvent(Event.PROPERTY_CHANGE, new PropertyNotify(this, atom, false));
+ return modified;
+ }
+
+ public boolean isInputOutput() {
+ return content != null;
+ }
+
+ public void addChild(Window child) {
+ if (child == null || child.parent == this) return;
+ child.parent = this;
+ children.add(child);
+ }
+
+ public void removeChild(Window child) {
+ if (child == null || child.parent != this) return;
+ child.parent = null;
+ children.remove(child);
+ }
+
+ public Window previousSibling() {
+ if (parent == null) return null;
+ int index = parent.children.indexOf(this);
+ return index > 0 ? parent.children.get(index - 1) : null;
+ }
+
+ public void moveChildAbove(Window child, Window sibling) {
+ children.remove(child);
+ if (sibling != null && children.contains(sibling)) {
+ children.add(children.indexOf(sibling) + 1, child);
+ return;
+ }
+ children.add(child);
+ }
+
+ public void moveChildBelow(Window child, Window sibling) {
+ children.remove(child);
+ if (sibling != null && children.contains(sibling)) {
+ children.add(children.indexOf(sibling), child);
+ return;
+ }
+ children.add(0, child);
+ }
+
+ public List getChildren() {
+ return immutableChildren;
+ }
+
+ public int getChildCount() {
+ return children.size();
+ }
+
+ public void addEventListener(EventListener eventListener) {
+ eventListeners.add(eventListener);
+ }
+
+ public void removeEventListener(EventListener eventListener) {
+ eventListeners.remove(eventListener);
+ }
+
+ public boolean hasEventListenerFor(int eventId) {
+ for (EventListener eventListener : eventListeners) {
+ if (eventListener.isInterestedIn(eventId)) return true;
+ }
+ return false;
+ }
+
+ public boolean hasEventListenerFor(Bitmask mask) {
+ for (EventListener eventListener : eventListeners) {
+ if (eventListener.isInterestedIn(mask)) return true;
+ }
+ return false;
+ }
+
+ public void sendEvent(int eventId, Event event) {
+ for (EventListener eventListener : eventListeners) {
+ if (eventListener.isInterestedIn(eventId)) {
+ eventListener.sendEvent(event);
+ }
+ }
+ }
+
+ public void sendEvent(Bitmask eventMask, Event event) {
+ for (EventListener eventListener : eventListeners) {
+ if (eventListener.isInterestedIn(eventMask)) {
+ eventListener.sendEvent(event);
+ }
+ }
+ }
+
+ public void sendEvent(int eventId, Event event, XClient client) {
+ for (EventListener eventListener : eventListeners) {
+ if (eventListener.isInterestedIn(eventId) && eventListener.client == client) {
+ eventListener.sendEvent(event);
+ }
+ }
+ }
+
+ public void sendEvent(Bitmask eventMask, Event event, XClient client) {
+ for (EventListener eventListener : eventListeners) {
+ if (eventListener.isInterestedIn(eventMask) && eventListener.client == client) {
+ eventListener.sendEvent(event);
+ }
+ }
+ }
+
+ public void sendEvent(Event event) {
+ for (EventListener eventListener : eventListeners) eventListener.sendEvent(event);
+ }
+
+ public boolean containsPoint(short rootX, short rootY) {
+ short[] localPoint = rootPointToLocal(rootX, rootY);
+ return localPoint[0] >= 0 && localPoint[1] >= 0 && localPoint[0] < width && localPoint[1] < height;
+ }
+
+ public short[] rootPointToLocal(short x, short y) {
+ Window window = this;
+ while (window != null) {
+ x -= window.x;
+ y -= window.y;
+ window = window.parent;
+ }
+ return new short[]{x, y};
+ }
+
+ public short[] localPointToRoot(short x, short y) {
+ Window window = this;
+ while (window != null) {
+ x += window.x;
+ y += window.y;
+ window = window.parent;
+ }
+ return new short[]{x, y};
+ }
+
+ public short getRootX() {
+ short rootX = x;
+ Window window = parent;
+ while (window != null) {
+ rootX += window.x;
+ window = window.parent;
+ }
+ return rootX;
+ }
+
+ public short getRootY() {
+ short rootY = y;
+ Window window = parent;
+ while (window != null) {
+ rootY += window.y;
+ window = window.parent;
+ }
+ return rootY;
+ }
+
+ public Window getAncestorWithEventMask(Bitmask eventMask) {
+ Window window = this;
+ while (window != null) {
+ if (window.hasEventListenerFor(eventMask)) return window;
+ if (window.attributes.getDoNotPropagateMask().intersects(eventMask)) return null;
+ window = window.parent;
+ }
+ return null;
+ }
+
+ public Window getAncestorWithEventId(int eventId) {
+ return getAncestorWithEventId(eventId, null);
+ }
+
+ public Window getAncestorWithEventId(int eventId, Window endWindow) {
+ Window window = this;
+ while (window != null) {
+ if (window.hasEventListenerFor(eventId)) return window;
+ if (window == endWindow || window.attributes.getDoNotPropagateMask().isSet(eventId)) return null;
+ window = window.parent;
+ }
+ return null;
+ }
+
+ public boolean isAncestorOf(Window window) {
+ if (window == this) return false;
+ while (window != null) {
+ if (window == this) return true;
+ window = window.parent;
+ }
+ return false;
+ }
+
+ public Window getChildByCoords(short x, short y) {
+ for (int i = children.size()-1; i >= 0; i--) {
+ Window child = children.get(i);
+ if (child.attributes.isMapped() && child.containsPoint(x, y)) return child;
+ }
+ return null;
+ }
+
+ public MapState getMapState() {
+ if (!attributes.isMapped()) return MapState.UNMAPPED;
+ Window window = this;
+ do {
+ window = window.parent;
+ if (window == null) return MapState.VIEWABLE;
+ }
+ while (window.attributes.isMapped());
+ return MapState.UNVIEWABLE;
+ }
+
+ public Bitmask getAllEventMasks() {
+ Bitmask eventMask = new Bitmask();
+ for (EventListener eventListener : eventListeners) eventMask.join(eventListener.eventMask);
+ return eventMask;
+ }
+
+ public EventListener getButtonPressListener() {
+ for (EventListener eventListener : eventListeners) {
+ if (eventListener.isInterestedIn(Event.BUTTON_PRESS)) return eventListener;
+ }
+ return null;
+ }
+
+ public void disableAllDescendants() {
+ Stack stack = new Stack<>();
+ stack.push(this);
+ while (!stack.isEmpty()) {
+ Window window = stack.pop();
+ window.attributes.setEnabled(false);
+ stack.addAll(window.children);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/WindowAttributes.java b/app/src/main/java/com/winlator/xserver/WindowAttributes.java
new file mode 100644
index 0000000..52c9608
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/WindowAttributes.java
@@ -0,0 +1,160 @@
+package com.winlator.xserver;
+
+import com.winlator.xconnector.XInputStream;
+
+public class WindowAttributes {
+ public static final int FLAG_BACKGROUND_PIXMAP = 1<<0;
+ public static final int FLAG_BACKGROUND_PIXEL = 1<<1;
+ public static final int FLAG_BORDER_PIXMAP = 1<<2;
+ public static final int FLAG_BORDER_PIXEL = 1<<3;
+ public static final int FLAG_BIT_GRAVITY = 1<<4;
+ public static final int FLAG_WIN_GRAVITY = 1<<5;
+ public static final int FLAG_BACKING_STORE = 1<<6;
+ public static final int FLAG_BACKING_PLANES = 1<<7;
+ public static final int FLAG_BACKING_PIXEL = 1<<8;
+ public static final int FLAG_OVERRIDE_REDIRECT = 1<<9;
+ public static final int FLAG_SAVE_UNDER = 1<<10;
+ public static final int FLAG_EVENT_MASK = 1<<11;
+ public static final int FLAG_DO_NOT_PROPAGATE_MASK = 1<<12;
+ public static final int FLAG_COLORMAP = 1<<13;
+ public static final int FLAG_CURSOR = 1<<14;
+ public enum BackingStore {NOT_USEFUL, WHEN_MAPPED, ALWAYS}
+ public enum WindowClass {COPY_FROM_PARENT, INPUT_OUTPUT, INPUT_ONLY}
+ public enum BitGravity {FORGET, NORTH_WEST, NORTH, NORTH_EAST, WEST, CENTER, EAST, SOUTH_WEST, SOUTH, SOUTH_EAST, STATIC}
+ public enum WinGravity {UNMAP, NORTH_WEST, NORTH, NORTH_EAST, WEST, CENTER, EAST, SOUTH_WEST, SOUTH, SOUTH_EAST, STATIC}
+ private int backingPixel = 0;
+ private int backingPlanes = 1;
+ private BackingStore backingStore = BackingStore.NOT_USEFUL;
+ private BitGravity bitGravity = BitGravity.CENTER;
+ private Cursor cursor;
+ private Bitmask doNotPropagateMask = new Bitmask(0);
+ private Bitmask eventMask = new Bitmask(0);
+ private boolean mapped = false;
+ private boolean overrideRedirect = false;
+ private boolean saveUnder = false;
+ private boolean enabled = true;
+ private WinGravity winGravity = WinGravity.CENTER;
+ private WindowClass windowClass = WindowClass.INPUT_OUTPUT;
+ public final Window window;
+
+ public WindowAttributes(Window window) {
+ this.window = window;
+ }
+
+ public int getBackingPixel() {
+ return backingPixel;
+ }
+
+ public int getBackingPlanes() {
+ return backingPlanes;
+ }
+
+ public BackingStore getBackingStore() {
+ return backingStore;
+ }
+
+ public BitGravity getBitGravity() {
+ return bitGravity;
+ }
+
+ public Cursor getCursor() {
+ Window parent = window.getParent();
+ return cursor == null && parent != null ? parent.attributes.getCursor() : cursor;
+ }
+
+ public Bitmask getEventMask() {
+ return eventMask;
+ }
+
+ public Bitmask getDoNotPropagateMask() {
+ return doNotPropagateMask;
+ }
+
+ public boolean isMapped() {
+ return mapped;
+ }
+
+ public void setMapped(boolean mapped) {
+ this.mapped = mapped;
+ }
+
+ public boolean isOverrideRedirect() {
+ return overrideRedirect;
+ }
+
+ public boolean isSaveUnder() {
+ return saveUnder;
+ }
+
+ public WinGravity getWinGravity() {
+ return winGravity;
+ }
+
+ public WindowClass getWindowClass() {
+ return windowClass;
+ }
+
+ public void setWindowClass(WindowClass windowClass) {
+ this.windowClass = windowClass;
+ }
+
+ public Window getWindow() {
+ return window;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public void update(Bitmask valueMask, XInputStream inputStream, XClient client) {
+ for (int index : valueMask) {
+ switch (index) {
+ case FLAG_BACKGROUND_PIXEL:
+ window.getContent().fillColor(inputStream.readInt());
+ break;
+ case FLAG_BACKING_PIXEL:
+ backingPixel = inputStream.readInt();
+ break;
+ case FLAG_BACKING_PLANES:
+ backingPlanes = inputStream.readInt();
+ break;
+ case FLAG_BIT_GRAVITY:
+ bitGravity = BitGravity.values()[inputStream.readInt()];
+ break;
+ case FLAG_WIN_GRAVITY:
+ winGravity = WinGravity.values()[inputStream.readInt()];
+ break;
+ case FLAG_BACKING_STORE:
+ backingStore = BackingStore.values()[inputStream.readInt()];
+ break;
+ case FLAG_SAVE_UNDER:
+ saveUnder = inputStream.readInt() == 1;
+ break;
+ case FLAG_OVERRIDE_REDIRECT:
+ overrideRedirect = inputStream.readInt() == 1;
+ break;
+ case FLAG_EVENT_MASK:
+ eventMask = new Bitmask(inputStream.readInt());
+ break;
+ case FLAG_DO_NOT_PROPAGATE_MASK:
+ doNotPropagateMask = new Bitmask(inputStream.readInt());
+ break;
+ case FLAG_CURSOR:
+ cursor = client.xServer.cursorManager.getCursor(inputStream.readInt());
+ break;
+ case FLAG_BACKGROUND_PIXMAP:
+ case FLAG_BORDER_PIXMAP:
+ case FLAG_BORDER_PIXEL:
+ case FLAG_COLORMAP:
+ inputStream.skip(4);
+ break;
+ }
+ }
+
+ client.xServer.windowManager.triggerOnUpdateWindowAttributes(window, valueMask);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/WindowManager.java b/app/src/main/java/com/winlator/xserver/WindowManager.java
new file mode 100644
index 0000000..9c5effa
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/WindowManager.java
@@ -0,0 +1,325 @@
+package com.winlator.xserver;
+
+import android.util.SparseArray;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xserver.errors.BadIdChoice;
+import com.winlator.xserver.errors.BadMatch;
+import com.winlator.xserver.errors.XRequestError;
+import com.winlator.xserver.events.ConfigureNotify;
+import com.winlator.xserver.events.ConfigureRequest;
+import com.winlator.xserver.events.DestroyNotify;
+import com.winlator.xserver.events.Event;
+import com.winlator.xserver.events.Expose;
+import com.winlator.xserver.events.MapNotify;
+import com.winlator.xserver.events.MapRequest;
+import com.winlator.xserver.events.ResizeRequest;
+import com.winlator.xserver.events.UnmapNotify;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class WindowManager extends XResourceManager {
+ public enum FocusRevertTo {NONE, POINTER_ROOT, PARENT}
+ public final Window rootWindow;
+ private final SparseArray windows = new SparseArray<>();
+ public final DrawableManager drawableManager;
+ private Window focusedWindow;
+ private FocusRevertTo focusRevertTo = FocusRevertTo.NONE;
+ private final ArrayList onWindowModificationListeners = new ArrayList<>();
+
+ public interface OnWindowModificationListener {
+ default void onMapWindow(Window window) {}
+
+ default void onUnmapWindow(Window window) {}
+
+ default void onChangeWindowZOrder(Window window) {}
+
+ default void onUpdateWindowContent(Window window) {}
+
+ default void onUpdateWindowGeometry(Window window, boolean resized) {}
+
+ default void onUpdateWindowAttributes(Window window, Bitmask mask) {}
+ }
+
+ public WindowManager(ScreenInfo screenInfo, DrawableManager drawableManager) {
+ this.drawableManager = drawableManager;
+ int id = IDGenerator.generate();
+ Drawable drawable = drawableManager.createDrawable(id, screenInfo.width, screenInfo.height, drawableManager.getVisual());
+ rootWindow = new Window(id, drawable, 0, 0, screenInfo.width, screenInfo.height, null);
+ rootWindow.attributes.setMapped(true);
+ windows.put(id, rootWindow);
+ }
+
+ public Window getWindow(int id) {
+ return windows.get(id);
+ }
+
+ public void destroyWindow(int id) {
+ Window window = getWindow(id);
+ if (window != null && rootWindow.id != id) {
+ unmapWindow(window);
+ removeAllSubwindowsAndWindow(window);
+ }
+ }
+
+ private void removeAllSubwindowsAndWindow(Window window) {
+ List children = new ArrayList<>(window.getChildren());
+ for (Window child : children) removeAllSubwindowsAndWindow(child);
+
+ Window parent = window.getParent();
+ window.sendEvent(Event.STRUCTURE_NOTIFY, new DestroyNotify(window, window));
+ parent.sendEvent(Event.SUBSTRUCTURE_NOTIFY, new DestroyNotify(parent, window));
+ windows.remove(window.id);
+ if (window.isInputOutput()) drawableManager.removeDrawable(window.getContent().id);
+ triggerOnFreeResourceListener(window);
+ if (window == focusedWindow) revertFocus();
+ parent.removeChild(window);
+ }
+
+ public void mapWindow(Window window) {
+ if (!window.attributes.isMapped()) {
+ Window parent = window.getParent();
+ if (!parent.hasEventListenerFor(Event.SUBSTRUCTURE_REDIRECT) || window.attributes.isOverrideRedirect()) {
+ window.attributes.setMapped(true);
+ window.sendEvent(Event.STRUCTURE_NOTIFY, new MapNotify(window, window));
+ parent.sendEvent(Event.SUBSTRUCTURE_NOTIFY, new MapNotify(parent, window));
+ window.sendEvent(Event.EXPOSURE, new Expose(window));
+ triggerOnMapWindow(window);
+ }
+ else parent.sendEvent(Event.SUBSTRUCTURE_REDIRECT, new MapRequest(parent, window));
+ }
+ }
+
+ public void unmapWindow(Window window) {
+ if (rootWindow.id != window.id && window.attributes.isMapped()) {
+ window.attributes.setMapped(false);
+ Window parent = window.getParent();
+ window.sendEvent(Event.STRUCTURE_NOTIFY, new UnmapNotify(window, window));
+ parent.sendEvent(Event.SUBSTRUCTURE_NOTIFY, new UnmapNotify(parent, window));
+ if (window == focusedWindow) revertFocus();
+ triggerOnUnmapWindow(window);
+ }
+ }
+
+ public Window getFocusedWindow() {
+ return focusedWindow;
+ }
+
+ public void revertFocus() {
+ switch (focusRevertTo) {
+ case NONE:
+ focusedWindow = null;
+ break;
+ case POINTER_ROOT:
+ focusedWindow = rootWindow;
+ break;
+ case PARENT:
+ if (focusedWindow.getParent() != null) focusedWindow = focusedWindow.getParent();
+ break;
+ }
+ }
+
+ public void setFocus(Window focusedWindow, FocusRevertTo focusRevertTo) {
+ this.focusedWindow = focusedWindow;
+ this.focusRevertTo = focusRevertTo;
+ }
+
+ public FocusRevertTo getFocusRevertTo() {
+ return focusRevertTo;
+ }
+
+ public Window createWindow(int id, Window parent, short x, short y, short width, short height, WindowAttributes.WindowClass windowClass, Visual visual, byte depth, XClient client) throws XRequestError {
+ if (windows.indexOfKey(id) >= 0) throw new BadIdChoice(id);
+
+ boolean isInputOutput = false;
+ switch (windowClass) {
+ case COPY_FROM_PARENT:
+ depth = (depth != 0 || !parent.isInputOutput()) ? depth : parent.getContent().visual.depth;
+ isInputOutput = parent.isInputOutput();
+ break;
+ case INPUT_OUTPUT:
+ if (parent.isInputOutput()) {
+ depth = depth == 0 ? parent.getContent().visual.depth : depth;
+ isInputOutput = true;
+ } else throw new BadMatch();
+ break;
+ case INPUT_ONLY:
+ isInputOutput = false;
+ break;
+ }
+
+ if (isInputOutput) {
+ visual = visual == null ? parent.getContent().visual : visual;
+ if (depth != visual.depth) throw new BadMatch();
+ }
+
+ Drawable drawable = null;
+ if (isInputOutput) {
+ drawable = drawableManager.createDrawable(id, width, height, visual);
+ if (drawable == null) throw new BadIdChoice(id);
+ }
+
+ final Window window = new Window(id, drawable, x, y, width, height, client);
+ window.attributes.setWindowClass(windowClass);
+ if (drawable != null) drawable.setOnDrawListener(() -> triggerOnUpdateWindowContent(window));
+ windows.put(id, window);
+ parent.addChild(window);
+ triggerOnCreateResourceListener(window);
+ return window;
+ }
+
+ private void changeWindowGeometry(Window window, short x, short y, short width, short height) {
+ boolean resized = window.getWidth() != width || window.getHeight() != height;
+ if (resized && window.hasEventListenerFor(Event.RESIZE_REDIRECT)) {
+ window.sendEvent(Event.SUBSTRUCTURE_REDIRECT, new ResizeRequest(window, width, height));
+ width = window.getWidth();
+ height = window.getHeight();
+ resized = false;
+ }
+
+ if (resized && window.isInputOutput()) {
+ Drawable oldContent = window.getContent();
+ drawableManager.removeDrawable(oldContent.id);
+ Drawable newContent = drawableManager.createDrawable(oldContent.id, width, height, oldContent.visual);
+ newContent.setOnDrawListener(() -> triggerOnUpdateWindowContent(window));
+ window.setContent(newContent);
+ }
+
+ if (resized || window.getX() != x || window.getY() != y) {
+ window.setX(x);
+ window.setY(y);
+ window.setWidth(width);
+ window.setHeight(height);
+ triggerOnUpdateWindowGeometry(window, resized);
+ }
+
+ if (resized && window.isInputOutput() && window.attributes.isMapped()) {
+ window.sendEvent(new Expose(window));
+ }
+ }
+
+ private void changeWindowZOrder(Window.StackMode stackMode, Window window, Window sibling) {
+ Window parent = window.getParent();
+ switch (stackMode) {
+ case ABOVE:
+ parent.moveChildAbove(window, sibling);
+ break;
+ case BELOW:
+ parent.moveChildBelow(window, sibling);
+ break;
+ }
+ triggerOnChangeWindowZOrder(window);
+ }
+
+ public void configureWindow(Window window, Bitmask valueMask, XInputStream inputStream) {
+ short x = window.getX();
+ short y = window.getY();
+ short width = window.getWidth();
+ short height = window.getHeight();
+ short borderWidth = window.getBorderWidth();
+ Window sibling = null;
+ Window.StackMode stackMode = null;
+
+ for (int index : valueMask) {
+ switch (index) {
+ case Window.FLAG_X:
+ x = (short)inputStream.readInt();
+ break;
+ case Window.FLAG_Y:
+ y = (short)inputStream.readInt();
+ break;
+ case Window.FLAG_WIDTH:
+ width = (short)inputStream.readInt();
+ break;
+ case Window.FLAG_HEIGHT:
+ height = (short)inputStream.readInt();
+ break;
+ case Window.FLAG_BORDER_WIDTH:
+ borderWidth = (short)inputStream.readInt();
+ break;
+ case Window.FLAG_SIBLING:
+ sibling = getWindow(inputStream.readInt());
+ break;
+ case Window.FLAG_STACK_MODE:
+ stackMode = Window.StackMode.values()[inputStream.readInt()];
+ break;
+ }
+ }
+
+ Window parent = window.getParent();
+ boolean overrideRedirect = window.attributes.isOverrideRedirect();
+ if (!parent.hasEventListenerFor(Event.SUBSTRUCTURE_REDIRECT) || overrideRedirect) {
+ changeWindowGeometry(window, x, y, width, height);
+
+ window.setBorderWidth(borderWidth);
+ if (stackMode != null) changeWindowZOrder(stackMode, window, sibling);
+
+ Window previousSibling = window.previousSibling();
+ window.sendEvent(Event.STRUCTURE_NOTIFY, new ConfigureNotify(window, window, previousSibling, x, y, width, height, borderWidth, overrideRedirect));
+ parent.sendEvent(Event.SUBSTRUCTURE_NOTIFY, new ConfigureNotify(parent, window, previousSibling, x, y, width, height, borderWidth, overrideRedirect));
+ }
+ else parent.sendEvent(Event.SUBSTRUCTURE_REDIRECT, new ConfigureRequest(parent, window, window.previousSibling(), x, y, width, height, borderWidth, stackMode, valueMask));
+ }
+
+ public void reparentWindow(Window window, Window newParent) {
+ Window oldParent = window.getParent();
+ if (oldParent != null) oldParent.removeChild(window);
+ newParent.addChild(window);
+ }
+
+ public Window findPointWindow(XServer xServer) {
+ Window pointWindow = findPointWindow(rootWindow, xServer.pointer.getClampedX(), xServer.pointer.getClampedY());
+ return pointWindow != null ? pointWindow : rootWindow;
+ }
+
+ private Window findPointWindow(Window window, short rootX, short rootY) {
+ if (!(window.attributes.isMapped() && window.containsPoint(rootX, rootY))) return null;
+ Window child = window.getChildByCoords(rootX, rootY);
+ return child != null ? findPointWindow(child, rootX, rootY) : window;
+ }
+
+ public void addOnWindowModificationListener(OnWindowModificationListener onWindowModificationListener) {
+ onWindowModificationListeners.add(onWindowModificationListener);
+ }
+
+ public void removeOnWindowModificationListener(OnWindowModificationListener onWindowModificationListener) {
+ onWindowModificationListeners.remove(onWindowModificationListener);
+ }
+
+ private void triggerOnMapWindow(Window window) {
+ for (int i = onWindowModificationListeners.size()-1; i >= 0; i--) {
+ onWindowModificationListeners.get(i).onMapWindow(window);
+ }
+ }
+
+ private void triggerOnUnmapWindow(Window window) {
+ for (int i = onWindowModificationListeners.size()-1; i >= 0; i--) {
+ onWindowModificationListeners.get(i).onUnmapWindow(window);
+ }
+ }
+
+ private void triggerOnChangeWindowZOrder(Window window) {
+ for (int i = onWindowModificationListeners.size()-1; i >= 0; i--) {
+ onWindowModificationListeners.get(i).onChangeWindowZOrder(window);
+ }
+ }
+
+ protected void triggerOnUpdateWindowContent(Window window) {
+ for (int i = onWindowModificationListeners.size()-1; i >= 0; i--) {
+ onWindowModificationListeners.get(i).onUpdateWindowContent(window);
+ }
+ }
+
+ protected void triggerOnUpdateWindowGeometry(Window window, boolean resized) {
+ for (int i = onWindowModificationListeners.size()-1; i >= 0; i--) {
+ onWindowModificationListeners.get(i).onUpdateWindowGeometry(window, resized);
+ }
+ }
+
+ public void triggerOnUpdateWindowAttributes(Window window, Bitmask mask) {
+ for (int i = onWindowModificationListeners.size()-1; i >= 0; i--) {
+ onWindowModificationListeners.get(i).onUpdateWindowAttributes(window, mask);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/XClient.java b/app/src/main/java/com/winlator/xserver/XClient.java
new file mode 100644
index 0000000..2e5a33c
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/XClient.java
@@ -0,0 +1,161 @@
+package com.winlator.xserver;
+
+import androidx.collection.ArrayMap;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xserver.events.Event;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class XClient implements XResourceManager.OnResourceLifecycleListener {
+ public final XServer xServer;
+ private boolean authenticated = false;
+ public final Integer resourceIDBase;
+ private short sequenceNumber = 0;
+ private int requestLength;
+ private byte requestData;
+ private int initialLength;
+ private final XInputStream inputStream;
+ private final XOutputStream outputStream;
+ private final ArrayMap eventListeners = new ArrayMap<>();
+ private final ArrayList resources = new ArrayList<>();
+
+ public XClient(XServer xServer, XInputStream inputStream, XOutputStream outputStream) {
+ this.xServer = xServer;
+ this.inputStream = inputStream;
+ this.outputStream = outputStream;
+
+ try (XLock lock = xServer.lockAll()) {
+ resourceIDBase = xServer.resourceIDs.get();
+ xServer.windowManager.addOnResourceLifecycleListener(this);
+ xServer.pixmapManager.addOnResourceLifecycleListener(this);
+ xServer.graphicsContextManager.addOnResourceLifecycleListener(this);
+ xServer.cursorManager.addOnResourceLifecycleListener(this);
+ }
+ }
+
+ public void registerAsOwnerOfResource(XResource resource) {
+ resources.add(resource);
+ }
+
+ public void setEventListenerForWindow(Window window, Bitmask eventMask) {
+ EventListener eventListener = eventListeners.get(window);
+ if (eventListener != null) window.removeEventListener(eventListener);
+ if (eventMask.isEmpty()) return;
+ eventListener = new EventListener(this, eventMask);
+ eventListeners.put(window, eventListener);
+ window.addEventListener(eventListener);
+ }
+
+ public void sendEvent(Event event) {
+ try {
+ event.send(sequenceNumber, outputStream);
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public boolean isInterestedIn(int eventId, Window window) {
+ EventListener eventListener = eventListeners.get(window);
+ return eventListener != null && eventListener.isInterestedIn(eventId);
+ }
+
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ public void setAuthenticated(boolean authenticated) {
+ this.authenticated = authenticated;
+ }
+
+ public void freeResources() {
+ try (XLock lock = xServer.lockAll()) {
+ while (!resources.isEmpty()) {
+ XResource resource = resources.remove(resources.size()-1);
+ if (resource instanceof Window) {
+ xServer.windowManager.destroyWindow(resource.id);
+ }
+ else if (resource instanceof Pixmap) {
+ xServer.pixmapManager.freePixmap(resource.id);
+ }
+ else if (resource instanceof GraphicsContext) {
+ xServer.graphicsContextManager.freeGraphicsContext(resource.id);
+ }
+ else if (resource instanceof Cursor) {
+ xServer.cursorManager.freeCursor(resource.id);
+ }
+ }
+
+ while (!eventListeners.isEmpty()) {
+ int i = eventListeners.size()-1;
+ eventListeners.keyAt(i).removeEventListener(eventListeners.removeAt(i));
+ }
+
+ xServer.windowManager.removeOnResourceLifecycleListener(this);
+ xServer.pixmapManager.removeOnResourceLifecycleListener(this);
+ xServer.graphicsContextManager.removeOnResourceLifecycleListener(this);
+ xServer.cursorManager.removeOnResourceLifecycleListener(this);
+ xServer.resourceIDs.free(resourceIDBase);
+ }
+ }
+
+ public void generateSequenceNumber() {
+ sequenceNumber++;
+ }
+
+ public short getSequenceNumber() {
+ return sequenceNumber;
+ }
+
+ public int getRequestLength() {
+ return requestLength;
+ }
+
+ public void setRequestLength(int requestLength) {
+ this.requestLength = requestLength;
+ initialLength = inputStream.available();
+ }
+
+ public byte getRequestData() {
+ return requestData;
+ }
+
+ public void setRequestData(byte requestData) {
+ this.requestData = requestData;
+ }
+
+ public int getRemainingRequestLength() {
+ int actualLength = initialLength - inputStream.available();
+ return requestLength - actualLength;
+ }
+
+ public void skipRequest() {
+ inputStream.skip(getRemainingRequestLength());
+ }
+
+ public XInputStream getInputStream() {
+ return inputStream;
+ }
+
+ public XOutputStream getOutputStream() {
+ return outputStream;
+ }
+
+ public Bitmask getEventMaskForWindow(Window window) {
+ EventListener eventListener = eventListeners.get(window);
+ return eventListener != null ? eventListener.eventMask : new Bitmask();
+ }
+
+ @Override
+ public void onFreeResource(XResource resource) {
+ if (resource instanceof Window) eventListeners.remove(resource);
+ resources.remove(resource);
+ }
+
+ public boolean isValidResourceId(int id) {
+ return xServer.resourceIDs.isInInterval(id, resourceIDBase);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/XClientConnectionHandler.java b/app/src/main/java/com/winlator/xserver/XClientConnectionHandler.java
new file mode 100644
index 0000000..61331aa
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/XClientConnectionHandler.java
@@ -0,0 +1,23 @@
+package com.winlator.xserver;
+
+import com.winlator.xconnector.Client;
+import com.winlator.xconnector.ConnectionHandler;
+
+public class XClientConnectionHandler implements ConnectionHandler {
+ private final XServer xServer;
+
+ public XClientConnectionHandler(XServer xServer) {
+ this.xServer = xServer;
+ }
+
+ @Override
+ public void handleNewConnection(Client client) {
+ client.createIOStreams();
+ client.setTag(new XClient(xServer, client.getInputStream(), client.getOutputStream()));
+ }
+
+ @Override
+ public void handleConnectionShutdown(Client client) {
+ ((XClient)client.getTag()).freeResources();
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/XClientRequestHandler.java b/app/src/main/java/com/winlator/xserver/XClientRequestHandler.java
new file mode 100644
index 0000000..a9aa50d
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/XClientRequestHandler.java
@@ -0,0 +1,415 @@
+package com.winlator.xserver;
+
+import com.winlator.xconnector.Client;
+import com.winlator.xconnector.RequestHandler;
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.errors.XRequestError;
+import com.winlator.xserver.extensions.Extension;
+import com.winlator.xserver.requests.AtomRequests;
+import com.winlator.xserver.requests.CursorRequests;
+import com.winlator.xserver.requests.DrawRequests;
+import com.winlator.xserver.requests.ExtensionRequests;
+import com.winlator.xserver.requests.FontRequests;
+import com.winlator.xserver.requests.GrabRequests;
+import com.winlator.xserver.requests.GraphicsContextRequests;
+import com.winlator.xserver.requests.KeyboardRequests;
+import com.winlator.xserver.requests.PixmapRequests;
+import com.winlator.xserver.requests.SelectionRequests;
+import com.winlator.xserver.requests.WindowRequests;
+
+import java.io.IOException;
+import java.nio.ByteOrder;
+
+public class XClientRequestHandler implements RequestHandler {
+ public static final byte RESPONSE_CODE_ERROR = 0;
+ public static final byte RESPONSE_CODE_SUCCESS = 1;
+ public static final int MAX_REQUEST_LENGTH = 65535;
+
+ @Override
+ public boolean handleRequest(Client client) throws IOException {
+ XClient xClient = (XClient)client.getTag();
+ XInputStream inputStream = client.getInputStream();
+ XOutputStream outputStream = client.getOutputStream();
+
+ if (xClient.isAuthenticated()) {
+ return handleNormalRequest(xClient, inputStream, outputStream);
+ }
+ else return handleAuthRequest(xClient, inputStream, outputStream);
+ }
+
+ private void sendServerInformation(XClient client, XOutputStream outputStream) throws IOException {
+ short vendorNameLength = (short)XServer.VENDOR_NAME.length();
+ byte pixmapFormatCount = (byte)client.xServer.pixmapManager.supportedPixmapFormats.length;
+ short additionalDataLength = (short)(8 + (2 * pixmapFormatCount) + ((vendorNameLength + 3) / 4) + ((40 + 8 * client.xServer.pixmapManager.supportedVisuals.length + 24) + 3) / 4);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(XServer.VERSION);
+ outputStream.writeShort((short)0);
+ outputStream.writeShort(additionalDataLength);
+ outputStream.writeInt(1);
+ outputStream.writeInt(client.resourceIDBase);
+ outputStream.writeInt(client.xServer.resourceIDs.idMask);
+ outputStream.writeInt(256);
+ outputStream.writeShort(vendorNameLength);
+ outputStream.writeShort((short)MAX_REQUEST_LENGTH);
+ outputStream.writeByte((byte)1);
+ outputStream.writeByte(pixmapFormatCount);
+ outputStream.writeByte((byte)0);
+ outputStream.writeByte((byte)0);
+ outputStream.writeByte((byte)32);
+ outputStream.writeByte((byte)32);
+ outputStream.writeByte((byte)Keyboard.MIN_KEYCODE);
+ outputStream.writeByte((byte)Keyboard.MAX_KEYCODE);
+ outputStream.writeInt(0);
+ outputStream.writeString8(XServer.VENDOR_NAME);
+
+ for (PixmapFormat pixmapFormat : client.xServer.pixmapManager.supportedPixmapFormats) {
+ outputStream.writeByte(pixmapFormat.depth);
+ outputStream.writeByte(pixmapFormat.bitsPerPixel);
+ outputStream.writeByte(pixmapFormat.scanlinePad);
+ outputStream.writePad(5);
+ }
+
+ Visual rootVisual = client.xServer.windowManager.rootWindow.getContent().visual;
+
+ outputStream.writeInt(client.xServer.windowManager.rootWindow.id);
+ outputStream.writeInt(0);
+ outputStream.writeInt(0xffffff);
+ outputStream.writeInt(0x000000);
+ outputStream.writeInt(client.xServer.windowManager.rootWindow.getAllEventMasks().getBits());
+ outputStream.writeShort(client.xServer.screenInfo.width);
+ outputStream.writeShort(client.xServer.screenInfo.height);
+ outputStream.writeShort(client.xServer.screenInfo.getWidthInMillimeters());
+ outputStream.writeShort(client.xServer.screenInfo.getHeightInMillimeters());
+ outputStream.writeShort((short)1);
+ outputStream.writeShort((short)1);
+ outputStream.writeInt(rootVisual.id);
+ outputStream.writeByte((byte)0);
+ outputStream.writeByte((byte)0);
+ outputStream.writeByte(rootVisual.depth);
+ outputStream.writeByte((byte)client.xServer.pixmapManager.supportedVisuals.length);
+
+ for (Visual visual : client.xServer.pixmapManager.supportedVisuals) {
+ outputStream.writeByte(visual.depth);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort((short)(visual.displayable ? 1 : 0));
+ outputStream.writeInt(0);
+
+ if (visual.displayable) {
+ outputStream.writeInt(visual.id);
+ outputStream.writeByte(visual.visualClass);
+ outputStream.writeByte(visual.bitsPerRGBValue);
+ outputStream.writeShort(visual.colormapEntries);
+ outputStream.writeInt(visual.redMask);
+ outputStream.writeInt(visual.greenMask);
+ outputStream.writeInt(visual.blueMask);
+ outputStream.writeInt(0);
+ }
+ }
+ }
+ }
+
+ private boolean handleAuthRequest(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException {
+ if (inputStream.available() < 12) return false;
+
+ byte byteOrder = inputStream.readByte();
+ if (byteOrder == 66) {
+ inputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ outputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ }
+ else if (byteOrder == 108) {
+ inputStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ outputStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ inputStream.skip(1);
+
+ short majorVersion = inputStream.readShort();
+ if (majorVersion != 11) throw new UnsupportedOperationException("Unsupported major X protocol version "+majorVersion+".");
+
+ inputStream.skip(2);
+ int nameLength = inputStream.readShort();
+ int dataLength = inputStream.readShort();
+ inputStream.skip(2);
+
+ if (nameLength > 0) inputStream.readString8(nameLength);
+ if (dataLength > 0) inputStream.readString8(dataLength);
+
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ sendServerInformation(client, outputStream);
+ }
+
+ client.setAuthenticated(true);
+ return true;
+ }
+
+ private boolean handleNormalRequest(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException {
+ if (inputStream.available() < 4) return false;
+ byte opcode = inputStream.readByte();
+ byte requestData = inputStream.readByte();
+
+ int requestLength = inputStream.readUnsignedShort();
+ if (requestLength != 0) {
+ requestLength = requestLength * 4 - 4;
+ }
+ else if (inputStream.available() < 4) {
+ return false;
+ }
+ else requestLength = inputStream.readInt() * 4 - 8;
+ if (inputStream.available() < requestLength) return false;
+
+ client.generateSequenceNumber();
+ client.setRequestData(requestData);
+ client.setRequestLength(requestLength);
+
+ try {
+ switch (opcode) {
+ case ClientOpcodes.CREATE_WINDOW:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.INPUT_DEVICE, XServer.Lockable.CURSOR_MANAGER)) {
+ WindowRequests.createWindow(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.CHANGE_WINDOW_ATTRIBUTES:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.CURSOR_MANAGER)) {
+ WindowRequests.changeWindowAttributes(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.GET_WINDOW_ATTRIBUTES:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ WindowRequests.getWindowAttributes(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.DESTROY_WINDOW:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.INPUT_DEVICE)) {
+ WindowRequests.destroyWindow(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.REPARENT_WINDOW:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ WindowRequests.reparentWindow(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.MAP_WINDOW:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.INPUT_DEVICE)) {
+ WindowRequests.mapWindow(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.UNMAP_WINDOW:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.INPUT_DEVICE)) {
+ WindowRequests.unmapWindow(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.CONFIGURE_WINDOW:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.INPUT_DEVICE)) {
+ WindowRequests.configureWindow(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.GET_GEOMETRY:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.DRAWABLE_MANAGER)) {
+ WindowRequests.getGeometry(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.QUERY_TREE:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ WindowRequests.queryTree(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.INTERN_ATOM:
+ AtomRequests.internAtom(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.CHANGE_PROPERTY:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ WindowRequests.changeProperty(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.DELETE_PROPERTY:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ WindowRequests.deleteProperty(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.GET_PROPERTY:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ WindowRequests.getProperty(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.SET_SELECTION_OWNER:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ SelectionRequests.setSelectionOwner(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.GET_SELECTION_OWNER:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ SelectionRequests.getSelectionOwner(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.SEND_EVENT:
+ try (XLock lock = client.xServer.lockAll()) {
+ WindowRequests.sendEvent(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.GRAB_POINTER:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.INPUT_DEVICE, XServer.Lockable.CURSOR_MANAGER)) {
+ GrabRequests.grabPointer(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.UNGRAB_POINTER:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.INPUT_DEVICE)) {
+ GrabRequests.ungrabPointer(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.QUERY_POINTER:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.INPUT_DEVICE)) {
+ WindowRequests.queryPointer(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.TRANSLATE_COORDINATES:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ WindowRequests.translateCoordinates(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.WARP_POINTER:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.INPUT_DEVICE)) {
+ WindowRequests.warpPointer(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.SET_INPUT_FOCUS:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ WindowRequests.setInputFocus(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.GET_INPUT_FOCUS:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ WindowRequests.getInputFocus(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.OPEN_FONT:
+ FontRequests.openFont(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.LIST_FONTS:
+ FontRequests.listFonts(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.CREATE_PIXMAP:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.PIXMAP_MANAGER, XServer.Lockable.DRAWABLE_MANAGER)) {
+ PixmapRequests.createPixmap(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.FREE_PIXMAP:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.PIXMAP_MANAGER, XServer.Lockable.DRAWABLE_MANAGER)) {
+ PixmapRequests.freePixmap(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.CREATE_GC:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.PIXMAP_MANAGER, XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.GRAPHIC_CONTEXT_MANAGER)) {
+ GraphicsContextRequests.createGC(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.CHANGE_GC:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.PIXMAP_MANAGER, XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.GRAPHIC_CONTEXT_MANAGER)) {
+ GraphicsContextRequests.changeGC(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.SET_CLIP_RECTANGLES:
+ client.skipRequest();
+ break;
+ case ClientOpcodes.FREE_GC:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.GRAPHIC_CONTEXT_MANAGER)) {
+ GraphicsContextRequests.freeGC(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.COPY_AREA:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.GRAPHIC_CONTEXT_MANAGER)) {
+ DrawRequests.copyArea(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.POLY_LINE:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.GRAPHIC_CONTEXT_MANAGER)) {
+ DrawRequests.polyLine(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.POLY_SEGMENT:
+ client.skipRequest();
+ break;
+ case ClientOpcodes.POLY_RECTANGLE:
+ client.skipRequest();
+ break;
+ case ClientOpcodes.POLY_FILL_RECTANGLE:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.GRAPHIC_CONTEXT_MANAGER)) {
+ DrawRequests.polyFillRectangle(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.PUT_IMAGE:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.GRAPHIC_CONTEXT_MANAGER)) {
+ DrawRequests.putImage(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.GET_IMAGE:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.PIXMAP_MANAGER, XServer.Lockable.DRAWABLE_MANAGER)) {
+ DrawRequests.getImage(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.CREATE_COLORMAP:
+ client.skipRequest();
+ break;
+ case ClientOpcodes.FREE_COLORMAP:
+ client.skipRequest();
+ break;
+ case ClientOpcodes.CREATE_CURSOR:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.PIXMAP_MANAGER, XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.CURSOR_MANAGER)) {
+ CursorRequests.createCursor(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.CREATE_GLYPH_CURSOR:
+ client.skipRequest();
+ break;
+ case ClientOpcodes.FREE_CURSOR:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.PIXMAP_MANAGER, XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.CURSOR_MANAGER)) {
+ CursorRequests.freeCursor(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.QUERY_EXTENSION:
+ ExtensionRequests.queryExtension(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.GET_KEYBOARD_MAPPING:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.INPUT_DEVICE)) {
+ KeyboardRequests.getKeyboardMapping(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.BELL:
+ client.skipRequest();
+ break;
+ case ClientOpcodes.SET_SCREEN_SAVER:
+ client.skipRequest();
+ break;
+ case ClientOpcodes.GET_SCREEN_SAVER:
+ WindowRequests.getScreenSaver(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.FORCE_SCREEN_SAVER:
+ client.skipRequest();
+ break;
+ case ClientOpcodes.GET_MODIFIER_MAPPING:
+ KeyboardRequests.getModifierMapping(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.NO_OPERATION:
+ client.skipRequest();
+ break;
+ default:
+ if (opcode < 0) {
+ Extension extension = client.xServer.extensions.get(opcode);
+ if (extension != null) extension.handleRequest(client, inputStream, outputStream);
+ }
+ else throw new UnsupportedOperationException("Unsupported opcode "+opcode+".");
+ break;
+ }
+ }
+ catch (XRequestError e) {
+ client.skipRequest();
+ e.sendError(client, opcode);
+ }
+
+ return true;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/XKeycode.java b/app/src/main/java/com/winlator/xserver/XKeycode.java
new file mode 100644
index 0000000..2bb1179
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/XKeycode.java
@@ -0,0 +1,111 @@
+package com.winlator.xserver;
+
+public enum XKeycode {
+ KEY_NONE(0),
+ KEY_ESC(9),
+ KEY_1(10),
+ KEY_2(11),
+ KEY_3(12),
+ KEY_4(13),
+ KEY_5(14),
+ KEY_6(15),
+ KEY_7(16),
+ KEY_8(17),
+ KEY_9(18),
+ KEY_0(19),
+ KEY_MINUS(20),
+ KEY_EQUAL(21),
+ KEY_BKSP(22),
+ KEY_TAB(23),
+ KEY_Q(24),
+ KEY_W(25),
+ KEY_E(26),
+ KEY_R(27),
+ KEY_T(28),
+ KEY_Y(29),
+ KEY_U(30),
+ KEY_I(31),
+ KEY_O(32),
+ KEY_P(33),
+ KEY_BRACKET_LEFT(34),
+ KEY_BRACKET_RIGHT(35),
+ KEY_ENTER(36),
+ KEY_CTRL_L(37),
+ KEY_A(38),
+ KEY_S(39),
+ KEY_D(40),
+ KEY_F(41),
+ KEY_G(42),
+ KEY_H(43),
+ KEY_J(44),
+ KEY_K(45),
+ KEY_L(46),
+ KEY_SEMICOLON(47),
+ KEY_APOSTROPHE(48),
+ KEY_GRAVE(49),
+ KEY_SHIFT_L(50),
+ KEY_BACKSLASH(51),
+ KEY_Z(52),
+ KEY_X(53),
+ KEY_C(54),
+ KEY_V(55),
+ KEY_B(56),
+ KEY_N(57),
+ KEY_M(58),
+ KEY_COMMA(59),
+ KEY_PERIOD(60),
+ KEY_SLASH(61),
+ KEY_SHIFT_R(62),
+ KEY_KP_MULTIPLY(63),
+ KEY_ALT_L(64),
+ KEY_SPACE(65),
+ KEY_CAPS_LOCK(66),
+ KEY_F1(67),
+ KEY_F2(68),
+ KEY_F3(69),
+ KEY_F4(70),
+ KEY_F5(71),
+ KEY_F6(72),
+ KEY_F7(73),
+ KEY_F8(74),
+ KEY_F9(75),
+ KEY_F10(76),
+ KEY_NUM_LOCK(77),
+ KEY_SCROLL_LOCK(78),
+ KEY_KP_7(79),
+ KEY_KP_8(80),
+ KEY_KP_9(81),
+ KEY_KP_SUBTRACT(82),
+ KEY_KP_4(83),
+ KEY_KP_5(84),
+ KEY_KP_6(85),
+ KEY_KP_ADD(86),
+ KEY_KP_1(87),
+ KEY_KP_2(88),
+ KEY_KP_3(89),
+ KEY_KP_0(90),
+ KEY_KP_DEL(91),
+ KEY_F11(95),
+ KEY_F12(96),
+ KEY_KP_ENTER(104),
+ KEY_CTRL_R(105),
+ KEY_KP_DIVIDE(106),
+ KEY_PRTSCN(107),
+ KEY_ALT_R(108),
+ KEY_HOME(110),
+ KEY_UP(111),
+ KEY_PRIOR(112),
+ KEY_LEFT(113),
+ KEY_RIGHT(114),
+ KEY_END(115),
+ KEY_DOWN(116),
+ KEY_NEXT(117),
+ KEY_INSERT(118),
+ KEY_DEL(119),
+ KEY_MAX(KEY_DEL.id);
+ public final byte id;
+
+ XKeycode(int id) {
+ this.id = (byte)id;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/XLock.java b/app/src/main/java/com/winlator/xserver/XLock.java
new file mode 100644
index 0000000..0f7d15d
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/XLock.java
@@ -0,0 +1,6 @@
+package com.winlator.xserver;
+
+public interface XLock extends AutoCloseable {
+ @Override
+ void close();
+}
diff --git a/app/src/main/java/com/winlator/xserver/XResource.java b/app/src/main/java/com/winlator/xserver/XResource.java
new file mode 100644
index 0000000..487b808
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/XResource.java
@@ -0,0 +1,14 @@
+package com.winlator.xserver;
+
+public abstract class XResource {
+ public final int id;
+
+ public XResource(int id) {
+ this.id = id;
+ }
+
+ @Override
+ public int hashCode() {
+ return id;
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/XResourceManager.java b/app/src/main/java/com/winlator/xserver/XResourceManager.java
new file mode 100644
index 0000000..e8a9888
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/XResourceManager.java
@@ -0,0 +1,33 @@
+package com.winlator.xserver;
+
+import java.util.ArrayList;
+
+public abstract class XResourceManager {
+ private final ArrayList onResourceLifecycleListeners = new ArrayList<>();
+
+ public interface OnResourceLifecycleListener {
+ default void onCreateResource(XResource resource) {}
+
+ default void onFreeResource(XResource resource) {}
+ }
+
+ public void addOnResourceLifecycleListener(OnResourceLifecycleListener OnResourceLifecycleListener) {
+ onResourceLifecycleListeners.add(OnResourceLifecycleListener);
+ }
+
+ public void removeOnResourceLifecycleListener(OnResourceLifecycleListener OnResourceLifecycleListener) {
+ onResourceLifecycleListeners.remove(OnResourceLifecycleListener);
+ }
+
+ public void triggerOnCreateResourceListener(XResource resource) {
+ for (int i = onResourceLifecycleListeners.size()-1; i >= 0; i--) {
+ onResourceLifecycleListeners.get(i).onCreateResource(resource);
+ }
+ }
+
+ public void triggerOnFreeResourceListener(XResource resource) {
+ for (int i = onResourceLifecycleListeners.size()-1; i >= 0; i--) {
+ onResourceLifecycleListeners.get(i).onFreeResource(resource);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/XServer.java b/app/src/main/java/com/winlator/xserver/XServer.java
new file mode 100644
index 0000000..13490e0
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/XServer.java
@@ -0,0 +1,185 @@
+package com.winlator.xserver;
+
+import android.util.SparseArray;
+
+import com.winlator.core.CursorLocker;
+import com.winlator.renderer.GLRenderer;
+import com.winlator.winhandler.WinHandler;
+import com.winlator.xserver.extensions.BigReqExtension;
+import com.winlator.xserver.extensions.DRI3Extension;
+import com.winlator.xserver.extensions.Extension;
+import com.winlator.xserver.extensions.MITSHMExtension;
+import com.winlator.xserver.extensions.PresentExtension;
+import com.winlator.xserver.extensions.SyncExtension;
+
+import java.nio.charset.Charset;
+import java.util.EnumMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class XServer {
+ public enum Lockable {WINDOW_MANAGER, PIXMAP_MANAGER, DRAWABLE_MANAGER, GRAPHIC_CONTEXT_MANAGER, INPUT_DEVICE, CURSOR_MANAGER, SHMSEGMENT_MANAGER}
+ public static final short VERSION = 11;
+ public static final String VENDOR_NAME = "Elbrus Technologies, LLC";
+ public static final Charset LATIN1_CHARSET = Charset.forName("latin1");
+ public final SparseArray extensions = new SparseArray<>();
+ public final ScreenInfo screenInfo;
+ public final PixmapManager pixmapManager;
+ public final ResourceIDs resourceIDs = new ResourceIDs(128);
+ public final GraphicsContextManager graphicsContextManager = new GraphicsContextManager();
+ public final SelectionManager selectionManager;
+ public final DrawableManager drawableManager;
+ public final WindowManager windowManager;
+ public final CursorManager cursorManager;
+ public final Keyboard keyboard = Keyboard.createKeyboard(this);
+ public final Pointer pointer = new Pointer(this);
+ public final InputDeviceManager inputDeviceManager;
+ public final GrabManager grabManager;
+ public final CursorLocker cursorLocker;
+ private SHMSegmentManager shmSegmentManager;
+ private GLRenderer renderer;
+ private WinHandler winHandler;
+ private final EnumMap locks = new EnumMap<>(Lockable.class);
+
+ public XServer(ScreenInfo screenInfo) {
+ this.screenInfo = screenInfo;
+ cursorLocker = new CursorLocker(this);
+ for (Lockable lockable : Lockable.values()) locks.put(lockable, new ReentrantLock());
+
+ pixmapManager = new PixmapManager();
+ drawableManager = new DrawableManager(this);
+ cursorManager = new CursorManager(drawableManager);
+ windowManager = new WindowManager(screenInfo, drawableManager);
+ selectionManager = new SelectionManager(windowManager);
+ inputDeviceManager = new InputDeviceManager(this);
+ grabManager = new GrabManager(this);
+
+ DesktopHelper.attachTo(this);
+ setupExtensions();
+ }
+
+ public GLRenderer getRenderer() {
+ return renderer;
+ }
+
+ public void setRenderer(GLRenderer renderer) {
+ this.renderer = renderer;
+ }
+
+ public WinHandler getWinHandler() {
+ return winHandler;
+ }
+
+ public void setWinHandler(WinHandler winHandler) {
+ this.winHandler = winHandler;
+ }
+
+ public SHMSegmentManager getSHMSegmentManager() {
+ return shmSegmentManager;
+ }
+
+ public void setSHMSegmentManager(SHMSegmentManager shmSegmentManager) {
+ this.shmSegmentManager = shmSegmentManager;
+ }
+
+ private class SingleXLock implements XLock {
+ private final ReentrantLock lock;
+
+ private SingleXLock(Lockable lockable) {
+ this.lock = locks.get(lockable);
+ lock.lock();
+ }
+
+ @Override
+ public void close() {
+ lock.unlock();
+ }
+ }
+
+ private class MultiXLock implements XLock {
+ private final Lockable[] lockables;
+
+ private MultiXLock(Lockable[] lockables) {
+ this.lockables = lockables;
+ for (Lockable lockable : lockables) locks.get(lockable).lock();
+ }
+
+ @Override
+ public void close() {
+ for (int i = lockables.length - 1; i >= 0; i--) {
+ locks.get(lockables[i]).unlock();
+ }
+ }
+ }
+
+ public XLock lock(Lockable lockable) {
+ return new SingleXLock(lockable);
+ }
+
+ public XLock lock(Lockable... lockables) {
+ return new MultiXLock(lockables);
+ }
+
+ public XLock lockAll() {
+ return new MultiXLock(Lockable.values());
+ }
+
+ public Extension getExtensionByName(String name) {
+ for (int i = 0; i < extensions.size(); i++) {
+ Extension extension = extensions.valueAt(i);
+ if (extension.getName().equals(name)) return extension;
+ }
+ return null;
+ }
+
+ public void injectPointerMove(int x, int y) {
+ try (XLock lock = lock(Lockable.WINDOW_MANAGER, Lockable.INPUT_DEVICE)) {
+ pointer.moveTo(x, y);
+ }
+ }
+
+ public void injectPointerMoveDelta(int dx, int dy) {
+ try (XLock lock = lock(Lockable.WINDOW_MANAGER, Lockable.INPUT_DEVICE)) {
+ pointer.moveTo(pointer.getX() + dx, pointer.getY() + dy);
+ }
+ }
+
+ public void injectPointerButtonPress(Pointer.Button buttonCode) {
+ try (XLock lock = lock(Lockable.WINDOW_MANAGER, Lockable.INPUT_DEVICE)) {
+ pointer.setButton(buttonCode, true);
+ }
+ }
+
+ public void injectPointerButtonRelease(Pointer.Button buttonCode) {
+ try (XLock lock = lock(Lockable.WINDOW_MANAGER, Lockable.INPUT_DEVICE)) {
+ pointer.setButton(buttonCode, false);
+ }
+ }
+
+ public void injectKeyPress(XKeycode xKeycode) {
+ injectKeyPress(xKeycode, 0);
+ }
+
+ public void injectKeyPress(XKeycode xKeycode, int keysym) {
+ try (XLock lock = lock(Lockable.WINDOW_MANAGER, Lockable.INPUT_DEVICE)) {
+ keyboard.setKeyPress(xKeycode.id, keysym);
+ }
+ }
+
+ public void injectKeyRelease(XKeycode xKeycode) {
+ try (XLock lock = lock(Lockable.WINDOW_MANAGER, Lockable.INPUT_DEVICE)) {
+ keyboard.setKeyRelease(xKeycode.id);
+ }
+ }
+
+ private void setupExtensions() {
+ extensions.put(BigReqExtension.MAJOR_OPCODE, new BigReqExtension());
+ extensions.put(MITSHMExtension.MAJOR_OPCODE, new MITSHMExtension());
+ extensions.put(DRI3Extension.MAJOR_OPCODE, new DRI3Extension());
+ extensions.put(PresentExtension.MAJOR_OPCODE, new PresentExtension());
+ extensions.put(SyncExtension.MAJOR_OPCODE, new SyncExtension());
+ }
+
+ public T getExtension(int opcode) {
+ return (T)extensions.get(opcode);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadAccess.java b/app/src/main/java/com/winlator/xserver/errors/BadAccess.java
new file mode 100644
index 0000000..31bb960
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadAccess.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadAccess extends XRequestError {
+ public BadAccess() {
+ super(10, 0);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadAlloc.java b/app/src/main/java/com/winlator/xserver/errors/BadAlloc.java
new file mode 100644
index 0000000..6af0320
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadAlloc.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadAlloc extends XRequestError {
+ public BadAlloc() {
+ super(11, 0);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadAtom.java b/app/src/main/java/com/winlator/xserver/errors/BadAtom.java
new file mode 100644
index 0000000..bc74ada
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadAtom.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadAtom extends XRequestError {
+ public BadAtom(int id) {
+ super(5, id);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadCursor.java b/app/src/main/java/com/winlator/xserver/errors/BadCursor.java
new file mode 100644
index 0000000..99b99a4
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadCursor.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadCursor extends XRequestError {
+ public BadCursor(int data) {
+ super(6, data);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadDrawable.java b/app/src/main/java/com/winlator/xserver/errors/BadDrawable.java
new file mode 100644
index 0000000..c1fd78c
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadDrawable.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadDrawable extends XRequestError {
+ public BadDrawable(int id) {
+ super(9, id);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadFence.java b/app/src/main/java/com/winlator/xserver/errors/BadFence.java
new file mode 100644
index 0000000..3725321
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadFence.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadFence extends XRequestError {
+ public BadFence(int id) {
+ super(Byte.MIN_VALUE + 2, id);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadGraphicsContext.java b/app/src/main/java/com/winlator/xserver/errors/BadGraphicsContext.java
new file mode 100644
index 0000000..851a8aa
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadGraphicsContext.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadGraphicsContext extends XRequestError {
+ public BadGraphicsContext(int id) {
+ super(13, id);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadIdChoice.java b/app/src/main/java/com/winlator/xserver/errors/BadIdChoice.java
new file mode 100644
index 0000000..c75aefa
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadIdChoice.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadIdChoice extends XRequestError {
+ public BadIdChoice(int id) {
+ super(14, id);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadImplementation.java b/app/src/main/java/com/winlator/xserver/errors/BadImplementation.java
new file mode 100644
index 0000000..ed3e8b3
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadImplementation.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadImplementation extends XRequestError {
+ public BadImplementation() {
+ super(17, 0);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadMatch.java b/app/src/main/java/com/winlator/xserver/errors/BadMatch.java
new file mode 100644
index 0000000..6bd96ca
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadMatch.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadMatch extends XRequestError {
+ public BadMatch() {
+ super(8, 0);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadPixmap.java b/app/src/main/java/com/winlator/xserver/errors/BadPixmap.java
new file mode 100644
index 0000000..1e6e499
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadPixmap.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadPixmap extends XRequestError {
+ public BadPixmap(int id) {
+ super(4, id);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadSHMSegment.java b/app/src/main/java/com/winlator/xserver/errors/BadSHMSegment.java
new file mode 100644
index 0000000..4cd3969
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadSHMSegment.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadSHMSegment extends XRequestError {
+ public BadSHMSegment(int id) {
+ super(-128, id);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadValue.java b/app/src/main/java/com/winlator/xserver/errors/BadValue.java
new file mode 100644
index 0000000..9dfedec
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadValue.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadValue extends XRequestError {
+ public BadValue(int data) {
+ super(2, data);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/BadWindow.java b/app/src/main/java/com/winlator/xserver/errors/BadWindow.java
new file mode 100644
index 0000000..613bad1
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/BadWindow.java
@@ -0,0 +1,7 @@
+package com.winlator.xserver.errors;
+
+public class BadWindow extends XRequestError {
+ public BadWindow(int id) {
+ super(3, id);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/errors/XRequestError.java b/app/src/main/java/com/winlator/xserver/errors/XRequestError.java
new file mode 100644
index 0000000..7210a46
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/errors/XRequestError.java
@@ -0,0 +1,40 @@
+package com.winlator.xserver.errors;
+
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_ERROR;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.XClient;
+
+import java.io.IOException;
+
+public class XRequestError extends Exception {
+ private final byte code;
+ private final int data;
+
+ public XRequestError(int code, int data) {
+ this.code = (byte)code;
+ this.data = data;
+ }
+
+ public byte getCode() {
+ return code;
+ }
+
+ public int getData() {
+ return data;
+ }
+
+ public void sendError(XClient client, byte opcode) throws IOException {
+ XOutputStream outputStream = client.getOutputStream();
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_ERROR);
+ outputStream.writeByte(code);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(data);
+ outputStream.writeShort(client.getRequestData());
+ outputStream.writeByte(opcode);
+ outputStream.writePad(21);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/ButtonPress.java b/app/src/main/java/com/winlator/xserver/events/ButtonPress.java
new file mode 100644
index 0000000..53d7160
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/ButtonPress.java
@@ -0,0 +1,10 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+
+public class ButtonPress extends InputDeviceEvent {
+ public ButtonPress(byte detail, Window root, Window event, Window child, short rootX, short rootY, short eventX, short eventY, Bitmask state) {
+ super(4, detail, root, event, child, rootX, rootY, eventX, eventY, state);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/ButtonRelease.java b/app/src/main/java/com/winlator/xserver/events/ButtonRelease.java
new file mode 100644
index 0000000..331f6d6
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/ButtonRelease.java
@@ -0,0 +1,10 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+
+public class ButtonRelease extends InputDeviceEvent {
+ public ButtonRelease(byte detail, Window root, Window event, Window child, short rootX, short rootY, short eventX, short eventY, Bitmask state) {
+ super(5, detail, root, event, child, rootX, rootY, eventX, eventY, state);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/ConfigureNotify.java b/app/src/main/java/com/winlator/xserver/events/ConfigureNotify.java
new file mode 100644
index 0000000..26c7d91
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/ConfigureNotify.java
@@ -0,0 +1,51 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Window;
+
+import java.io.IOException;
+
+public class ConfigureNotify extends Event {
+ private final Window event;
+ private final Window window;
+ private final Window aboveSibling;
+ private final short x;
+ private final short y;
+ private final short height;
+ private final short width;
+ private final short borderWidth;
+ private final boolean overrideRedirect;
+
+ public ConfigureNotify(Window event, Window window, Window aboveSibling, int x, int y, int width, int height, int borderWidth, boolean overrideRedirect) {
+ super(22);
+ this.event = event;
+ this.window = window;
+ this.aboveSibling = aboveSibling;
+ this.x = (short)x;
+ this.y = (short)y;
+ this.width = (short)width;
+ this.height = (short)height;
+ this.borderWidth = (short)borderWidth;
+ this.overrideRedirect = overrideRedirect;
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(event.id);
+ outputStream.writeInt(window.id);
+ outputStream.writeInt(aboveSibling != null ? aboveSibling.id : 0);
+ outputStream.writeShort(x);
+ outputStream.writeShort(y);
+ outputStream.writeShort(width);
+ outputStream.writeShort(height);
+ outputStream.writeShort(borderWidth);
+ outputStream.writeByte((byte)(overrideRedirect ? 1 : 0));
+ outputStream.writePad(5);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/ConfigureRequest.java b/app/src/main/java/com/winlator/xserver/events/ConfigureRequest.java
new file mode 100644
index 0000000..d26a8ce
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/ConfigureRequest.java
@@ -0,0 +1,54 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+
+import java.io.IOException;
+
+public class ConfigureRequest extends Event {
+ private final Window parent;
+ private final Window window;
+ private final Window sibling;
+ private final short x;
+ private final short y;
+ private final short width;
+ private final short height;
+ private final short borderWidth;
+ private final Window.StackMode stackMode;
+ private final Bitmask valueMask;
+
+ public ConfigureRequest(Window parent, Window window, Window sibling, short x, short y, short width, short height, short borderWidth, Window.StackMode stackMode, Bitmask valueMask) {
+ super(23);
+ this.parent = parent;
+ this.window = window;
+ this.sibling = sibling;
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ this.borderWidth = borderWidth;
+ this.stackMode = stackMode != null ? stackMode : Window.StackMode.ABOVE;
+ this.valueMask = valueMask;
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte((byte)stackMode.ordinal());
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(parent.id);
+ outputStream.writeInt(window.id);
+ outputStream.writeInt(sibling != null ? sibling.id : 0);
+ outputStream.writeShort(x);
+ outputStream.writeShort(y);
+ outputStream.writeShort(width);
+ outputStream.writeShort(height);
+ outputStream.writeShort(borderWidth);
+ outputStream.writeShort((short)valueMask.getBits());
+ outputStream.writePad(4);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/CreateNotify.java b/app/src/main/java/com/winlator/xserver/events/CreateNotify.java
new file mode 100644
index 0000000..05b2436
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/CreateNotify.java
@@ -0,0 +1,36 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Window;
+
+import java.io.IOException;
+
+public class CreateNotify extends Event {
+ private final Window parent;
+ private final Window window;
+
+ public CreateNotify(Window parent, Window window) {
+ super(16);
+ this.parent = parent;
+ this.window = window;
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(parent.id);
+ outputStream.writeInt(window.id);
+ outputStream.writeShort(window.getX());
+ outputStream.writeShort(window.getY());
+ outputStream.writeShort(window.getWidth());
+ outputStream.writeShort(window.getHeight());
+ outputStream.writeShort(window.getBorderWidth());
+ outputStream.writeByte((byte)(window.attributes.isOverrideRedirect() ? 1 : 0));
+ outputStream.writePad(9);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/DestroyNotify.java b/app/src/main/java/com/winlator/xserver/events/DestroyNotify.java
new file mode 100644
index 0000000..1b61864
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/DestroyNotify.java
@@ -0,0 +1,30 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Window;
+
+import java.io.IOException;
+
+public class DestroyNotify extends Event {
+ private final Window event;
+ private final Window window;
+
+ public DestroyNotify(Window event, Window window) {
+ super(17);
+ this.event = event;
+ this.window = window;
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(event.id);
+ outputStream.writeInt(window.id);
+ outputStream.writePad(20);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/EnterNotify.java b/app/src/main/java/com/winlator/xserver/events/EnterNotify.java
new file mode 100644
index 0000000..a4b4578
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/EnterNotify.java
@@ -0,0 +1,10 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+
+public class EnterNotify extends PointerWindowEvent {
+ public EnterNotify(Detail detail, Window root, Window event, Window child, short rootX, short rootY, short eventX, short eventY, Bitmask state, Mode mode, boolean sameScreenAndFocus) {
+ super(7, detail, root, event, child, rootX, rootY, eventX, eventY, state, mode, sameScreenAndFocus);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/Event.java b/app/src/main/java/com/winlator/xserver/events/Event.java
new file mode 100644
index 0000000..1202a50
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/Event.java
@@ -0,0 +1,40 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+
+import java.io.IOException;
+
+public abstract class Event {
+ public static final int KEY_PRESS = 1<<0;
+ public static final int KEY_RELEASE = 1<<1;
+ public static final int BUTTON_PRESS = 1<<2;
+ public static final int BUTTON_RELEASE = 1<<3;
+ public static final int ENTER_WINDOW = 1<<4;
+ public static final int LEAVE_WINDOW = 1<<5;
+ public static final int POINTER_MOTION = 1<<6;
+ public static final int POINTER_MOTION_HINT = 1<<7;
+ public static final int BUTTON1_MOTION = 1<<8;
+ public static final int BUTTON2_MOTION = 1<<9;
+ public static final int BUTTON3_MOTION = 1<<10;
+ public static final int BUTTON4_MOTION = 1<<11;
+ public static final int BUTTON5_MOTION = 1<<12;
+ public static final int BUTTON_MOTION = 1<<13;
+ public static final int KEYMAP_STATE = 1<<14;
+ public static final int EXPOSURE = 1<<15;
+ public static final int VISIBILITY_CHANGE = 1<<16;
+ public static final int STRUCTURE_NOTIFY = 1<<17;
+ public static final int RESIZE_REDIRECT = 1<<18;
+ public static final int SUBSTRUCTURE_NOTIFY = 1<<19;
+ public static final int SUBSTRUCTURE_REDIRECT = 1<<20;
+ public static final int FOCUS_CHANGE = 1<<21;
+ public static final int PROPERTY_CHANGE = 1<<22;
+ public static final int COLORMAP_CHANGE = 1<<23;
+ public static final int OWNER_GRAB_BUTTON = 1<<24;
+ protected final byte code;
+
+ public Event(int code) {
+ this.code = (byte)code;
+ }
+
+ public abstract void send(short sequenceNumber, XOutputStream outputStream) throws IOException;
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/events/Expose.java b/app/src/main/java/com/winlator/xserver/events/Expose.java
new file mode 100644
index 0000000..2b86993
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/Expose.java
@@ -0,0 +1,40 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Window;
+
+import java.io.IOException;
+
+public class Expose extends Event {
+ private final Window window;
+ private final short width;
+ private final short height;
+ private final short x;
+ private final short y;
+
+ public Expose(Window window) {
+ super(12);
+ this.window = window;
+ this.y = 0;
+ this.x = 0;
+ this.width = window.getWidth();
+ this.height = window.getHeight();
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(window.id);
+ outputStream.writeShort(x);
+ outputStream.writeShort(y);
+ outputStream.writeShort(width);
+ outputStream.writeShort(height);
+ outputStream.writeShort((short)0);
+ outputStream.writePad(14);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/InputDeviceEvent.java b/app/src/main/java/com/winlator/xserver/events/InputDeviceEvent.java
new file mode 100644
index 0000000..ebef39c
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/InputDeviceEvent.java
@@ -0,0 +1,55 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+
+import java.io.IOException;
+
+public class InputDeviceEvent extends Event {
+ private final byte detail;
+ private final int timestamp;
+ private final Window root;
+ private final Window event;
+ private final Window child;
+ private final short eventX;
+ private final short eventY;
+ private final short rootX;
+ private final short rootY;
+ private final Bitmask state;
+
+ public InputDeviceEvent(int code, byte detail, Window root, Window event, Window child, short rootX, short rootY, short eventX, short eventY, Bitmask state) {
+ super(code);
+ this.detail = detail;
+ this.timestamp = (int)System.currentTimeMillis();
+ this.root = root;
+ this.event = event;
+ this.child = child;
+ this.rootX = rootX;
+ this.rootY = rootY;
+ this.eventX = eventX;
+ this.eventY = eventY;
+ this.state = state;
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte(detail);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(timestamp);
+ outputStream.writeInt(root.id);
+ outputStream.writeInt(event.id);
+ outputStream.writeInt(child != null ? child.id : 0);
+ outputStream.writeShort(rootX);
+ outputStream.writeShort(rootY);
+ outputStream.writeShort(eventX);
+ outputStream.writeShort(eventY);
+ outputStream.writeShort((short)state.getBits());
+ outputStream.writeByte((byte)1);
+ outputStream.writeByte((byte)0);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/KeyPress.java b/app/src/main/java/com/winlator/xserver/events/KeyPress.java
new file mode 100644
index 0000000..ca49ab6
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/KeyPress.java
@@ -0,0 +1,10 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+
+public class KeyPress extends InputDeviceEvent {
+ public KeyPress(byte keycode, Window root, Window event, Window child, short rootX, short rootY, short eventX, short eventY, Bitmask state) {
+ super(2, keycode, root, event, child, rootX, rootY, eventX, eventY, state);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/KeyRelease.java b/app/src/main/java/com/winlator/xserver/events/KeyRelease.java
new file mode 100644
index 0000000..4cd0637
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/KeyRelease.java
@@ -0,0 +1,10 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+
+public class KeyRelease extends InputDeviceEvent {
+ public KeyRelease(byte keycode, Window root, Window event, Window child, short rootX, short rootY, short eventX, short eventY, Bitmask state) {
+ super(3, keycode, root, event, child, rootX, rootY, eventX, eventY, state);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/LeaveNotify.java b/app/src/main/java/com/winlator/xserver/events/LeaveNotify.java
new file mode 100644
index 0000000..ac31b6c
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/LeaveNotify.java
@@ -0,0 +1,10 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+
+public class LeaveNotify extends PointerWindowEvent {
+ public LeaveNotify(Detail detail, Window root, Window event, Window child, short rootX, short rootY, short eventX, short eventY, Bitmask state, Mode mode, boolean sameScreenAndFocus) {
+ super(8, detail, root, event, child, rootX, rootY, eventX, eventY, state, mode, sameScreenAndFocus);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/MapNotify.java b/app/src/main/java/com/winlator/xserver/events/MapNotify.java
new file mode 100644
index 0000000..92caace
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/MapNotify.java
@@ -0,0 +1,31 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Window;
+
+import java.io.IOException;
+
+public class MapNotify extends Event {
+ private final Window event;
+ private final Window window;
+
+ public MapNotify(Window event, Window window) {
+ super(19);
+ this.event = event;
+ this.window = window;
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(event.id);
+ outputStream.writeInt(window.id);
+ outputStream.writeByte((byte)(window.attributes.isOverrideRedirect() ? 1 : 0));
+ outputStream.writePad(19);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/MapRequest.java b/app/src/main/java/com/winlator/xserver/events/MapRequest.java
new file mode 100644
index 0000000..9e39a87
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/MapRequest.java
@@ -0,0 +1,30 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Window;
+
+import java.io.IOException;
+
+public class MapRequest extends Event {
+ private final Window parent;
+ private final Window window;
+
+ public MapRequest(Window parent, Window window) {
+ super(20);
+ this.parent = parent;
+ this.window = window;
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(parent.id);
+ outputStream.writeInt(window.id);
+ outputStream.writePad(20);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/MappingNotify.java b/app/src/main/java/com/winlator/xserver/events/MappingNotify.java
new file mode 100644
index 0000000..78e2a20
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/MappingNotify.java
@@ -0,0 +1,33 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+
+import java.io.IOException;
+
+public class MappingNotify extends Event {
+ public enum Request {MODIFIER, KEYBOARD, POINTER}
+ private final Request request;
+ private final byte firstKeycode;
+ private final byte count;
+
+ public MappingNotify(Request request, byte firstKeycode, int count) {
+ super(34);
+ this.request = request;
+ this.firstKeycode = firstKeycode;
+ this.count = (byte)count;
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeByte((byte)request.ordinal());
+ outputStream.writeByte(firstKeycode);
+ outputStream.writeByte(count);
+ outputStream.writePad(25);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/MotionNotify.java b/app/src/main/java/com/winlator/xserver/events/MotionNotify.java
new file mode 100644
index 0000000..bb4b6db
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/MotionNotify.java
@@ -0,0 +1,10 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+
+public class MotionNotify extends InputDeviceEvent {
+ public MotionNotify(boolean detail, Window root, Window event, Window child, short rootX, short rootY, short eventX, short eventY, Bitmask state) {
+ super(6, (byte)(detail ? 1 : 0), root, event, child, rootX, rootY, eventX, eventY, state);
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/PointerWindowEvent.java b/app/src/main/java/com/winlator/xserver/events/PointerWindowEvent.java
new file mode 100644
index 0000000..9ef0ac2
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/PointerWindowEvent.java
@@ -0,0 +1,61 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+
+import java.io.IOException;
+
+public abstract class PointerWindowEvent extends Event {
+ public enum Detail {ANCESTOR, VIRTUAL, INFERIOR, NONLINEAR, NONLINEAR_VIRTUAL}
+ public enum Mode {NORMAL, GRAB, UNGRAB}
+ private final Detail detail;
+ private final int timestamp;
+ private final Window root;
+ private final Window event;
+ private final Window child;
+ private final short rootX;
+ private final short rootY;
+ private final short eventX;
+ private final short eventY;
+ private final Bitmask state;
+ private final Mode mode;
+ private final boolean sameScreenAndFocus;
+
+ public PointerWindowEvent(int code, Detail detail, Window root, Window event, Window child, short rootX, short rootY, short eventX, short eventY, Bitmask state, Mode mode, boolean sameScreenAndFocus) {
+ super(code);
+ this.detail = detail;
+ this.timestamp = (int)System.currentTimeMillis();
+ this.root = root;
+ this.event = event;
+ this.child = child;
+ this.rootX = rootX;
+ this.rootY = rootY;
+ this.eventX = eventX;
+ this.eventY = eventY;
+ this.state = state;
+ this.mode = mode;
+ this.sameScreenAndFocus = sameScreenAndFocus;
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte((byte)detail.ordinal());
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(timestamp);
+ outputStream.writeInt(root.id);
+ outputStream.writeInt(event.id);
+ outputStream.writeInt(child != null ? child.id : 0);
+ outputStream.writeShort(rootX);
+ outputStream.writeShort(rootY);
+ outputStream.writeShort(eventX);
+ outputStream.writeShort(eventY);
+ outputStream.writeShort((short)state.getBits());
+ outputStream.writeByte((byte)mode.ordinal());
+ outputStream.writeByte((byte)(sameScreenAndFocus ? 1 : 0));
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/events/PresentCompleteNotify.java b/app/src/main/java/com/winlator/xserver/events/PresentCompleteNotify.java
new file mode 100644
index 0000000..a67f8a9
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/events/PresentCompleteNotify.java
@@ -0,0 +1,55 @@
+package com.winlator.xserver.events;
+
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Window;
+import com.winlator.xserver.extensions.PresentExtension;
+
+import java.io.IOException;
+
+public class PresentCompleteNotify extends Event {
+ private final int eventId;
+ private final Window window;
+ private final int serial;
+ private final PresentExtension.Kind kind;
+ private final PresentExtension.Mode mode;
+ private final long ust;
+ private final long msc;
+
+ public PresentCompleteNotify(int eventId, Window window, int serial, PresentExtension.Kind kind, PresentExtension.Mode mode, long ust, long msc) {
+ super(35);
+ this.eventId = eventId;
+ this.window = window;
+ this.serial = serial;
+ this.kind = kind;
+ this.mode = mode;
+ this.ust = ust;
+ this.msc = msc;
+ }
+
+ @Override
+ public void send(short sequenceNumber, XOutputStream outputStream) throws IOException {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(code);
+ outputStream.writeByte(PresentExtension.MAJOR_OPCODE);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(2);
+ outputStream.writeShort(getEventType());
+ outputStream.writeByte((byte)kind.ordinal());
+ outputStream.writeByte((byte)mode.ordinal());
+ outputStream.writeInt(eventId);
+ outputStream.writeInt(window.id);
+ outputStream.writeInt(serial);
+ outputStream.writeLong(ust);
+ outputStream.writeLong(msc);
+ }
+ }
+
+ public static short getEventType() {
+ return 1;
+ }
+
+ public static int getEventMask() {
+ return 1< onDestroyDrawableListener = (drawable) -> {
+ ByteBuffer data = drawable.getData();
+ SysVSharedMemory.unmapSHMSegment(data, data.capacity());
+ };
+
+ private static abstract class ClientOpcodes {
+ private static final byte QUERY_VERSION = 0;
+ private static final byte OPEN = 1;
+ private static final byte PIXMAP_FROM_BUFFER = 2;
+ private static final byte PIXMAP_FROM_BUFFERS = 7;
+ }
+
+ @Override
+ public String getName() {
+ return "DRI3";
+ }
+
+ @Override
+ public byte getMajorOpcode() {
+ return MAJOR_OPCODE;
+ }
+
+ @Override
+ public byte getFirstErrorId() {
+ return 0;
+ }
+
+ @Override
+ public byte getFirstEventId() {
+ return 0;
+ }
+
+ private void queryVersion(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ inputStream.skip(8);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeInt(1);
+ outputStream.writeInt(0);
+ outputStream.writePad(16);
+ }
+ }
+
+ private void open(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int drawableId = inputStream.readInt();
+ inputStream.skip(4);
+
+ Drawable drawable = client.xServer.drawableManager.getDrawable(drawableId);
+ if (drawable == null) throw new BadDrawable(drawableId);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writePad(24);
+ }
+ }
+
+ private void pixmapFromBuffer(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int pixmapId = inputStream.readInt();
+ int windowId = inputStream.readInt();
+ int size = inputStream.readInt();
+ short width = inputStream.readShort();
+ short height = inputStream.readShort();
+ short stride = inputStream.readShort();
+ byte depth = inputStream.readByte();
+ inputStream.skip(1);
+
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+
+ Pixmap pixmap = client.xServer.pixmapManager.getPixmap(pixmapId);
+ if (pixmap != null) throw new BadIdChoice(pixmapId);
+
+ int fd = inputStream.getAncillaryFd();
+ pixmapFromFd(client, pixmapId, width, height, stride, 0, depth, fd, size);
+ }
+
+ private void pixmapFromBuffers(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int pixmapId = inputStream.readInt();
+ int windowId = inputStream.readInt();
+ inputStream.skip(4);
+ short width = inputStream.readShort();
+ short height = inputStream.readShort();
+ int stride = inputStream.readInt();
+ int offset = inputStream.readInt();
+ inputStream.skip(24);
+ byte depth = inputStream.readByte();
+ inputStream.skip(11);
+
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+
+ Pixmap pixmap = client.xServer.pixmapManager.getPixmap(pixmapId);
+ if (pixmap != null) throw new BadIdChoice(pixmapId);
+
+ int fd = inputStream.getAncillaryFd();
+ long size = (long)stride * height;
+ pixmapFromFd(client, pixmapId, width, height, stride, offset, depth, fd, size);
+ }
+
+ private void pixmapFromFd(XClient client, int pixmapId, short width, short height, int stride, int offset, byte depth, int fd, long size) throws IOException, XRequestError {
+ try {
+ ByteBuffer buffer = SysVSharedMemory.mapSHMSegment(fd, size, offset, true);
+ if (buffer == null) throw new BadAlloc();
+
+ short totalWidth = (short)(stride / 4);
+ Drawable drawable = client.xServer.drawableManager.createDrawable(pixmapId, totalWidth, height, depth);
+ drawable.setData(buffer);
+ drawable.setTexture(null);
+ drawable.setOnDestroyListener(onDestroyDrawableListener);
+ client.xServer.pixmapManager.createPixmap(drawable);
+ }
+ finally {
+ XConnectorEpoll.closeFd(fd);
+ }
+ }
+
+ @Override
+ public void handleRequest(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int opcode = client.getRequestData();
+ switch (opcode) {
+ case ClientOpcodes.QUERY_VERSION :
+ queryVersion(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.OPEN :
+ try (XLock lock = client.xServer.lock(XServer.Lockable.DRAWABLE_MANAGER)) {
+ open(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.PIXMAP_FROM_BUFFER:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.PIXMAP_MANAGER, XServer.Lockable.DRAWABLE_MANAGER)) {
+ pixmapFromBuffer(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.PIXMAP_FROM_BUFFERS:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.PIXMAP_MANAGER, XServer.Lockable.DRAWABLE_MANAGER)) {
+ pixmapFromBuffers(client, inputStream, outputStream);
+ }
+ break;
+ default:
+ throw new BadImplementation();
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/extensions/Extension.java b/app/src/main/java/com/winlator/xserver/extensions/Extension.java
new file mode 100644
index 0000000..abdcc77
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/extensions/Extension.java
@@ -0,0 +1,20 @@
+package com.winlator.xserver.extensions;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.XRequestError;
+
+import java.io.IOException;
+
+public interface Extension {
+ String getName();
+
+ byte getMajorOpcode();
+
+ byte getFirstErrorId();
+
+ byte getFirstEventId();
+
+ void handleRequest(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError;
+}
diff --git a/app/src/main/java/com/winlator/xserver/extensions/MITSHMExtension.java b/app/src/main/java/com/winlator/xserver/extensions/MITSHMExtension.java
new file mode 100644
index 0000000..25206d1
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/extensions/MITSHMExtension.java
@@ -0,0 +1,135 @@
+package com.winlator.xserver.extensions;
+
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_SUCCESS;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Drawable;
+import com.winlator.xserver.GraphicsContext;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.XLock;
+import com.winlator.xserver.XServer;
+import com.winlator.xserver.errors.BadDrawable;
+import com.winlator.xserver.errors.BadGraphicsContext;
+import com.winlator.xserver.errors.BadImplementation;
+import com.winlator.xserver.errors.BadSHMSegment;
+import com.winlator.xserver.errors.XRequestError;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class MITSHMExtension implements Extension {
+ public static final byte MAJOR_OPCODE = -101;
+
+ private static abstract class ClientOpcodes {
+ private static final byte QUERY_VERSION = 0;
+ private static final byte ATTACH = 1;
+ private static final byte DETACH = 2;
+ private static final byte PUT_IMAGE = 3;
+ }
+
+ @Override
+ public String getName() {
+ return "MIT-SHM";
+ }
+
+ @Override
+ public byte getMajorOpcode() {
+ return MAJOR_OPCODE;
+ }
+
+ @Override
+ public byte getFirstErrorId() {
+ return Byte.MIN_VALUE;
+ }
+
+ @Override
+ public byte getFirstEventId() {
+ return 64;
+ }
+
+ private static void queryVersion(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeShort((short)1);
+ outputStream.writeShort((short)1);
+ outputStream.writeShort((short)0);
+ outputStream.writeShort((short)0);
+ outputStream.writeByte((byte)0);
+ }
+ }
+
+ private static void attach(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int xid = inputStream.readInt();
+ int shmid = inputStream.readInt();
+ inputStream.skip(4);
+ client.xServer.getSHMSegmentManager().attach(xid, shmid);
+ }
+
+ private static void detach(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ client.xServer.getSHMSegmentManager().detach(inputStream.readInt());
+ }
+
+ private static void putImage(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int drawableId = inputStream.readInt();
+ int gcId = inputStream.readInt();
+ short totalWidth = inputStream.readShort();
+ short totalHeight = inputStream.readShort();
+ short srcX = inputStream.readShort();
+ short srcY = inputStream.readShort();
+ short srcWidth = inputStream.readShort();
+ short srcHeight = inputStream.readShort();
+ short dstX = inputStream.readShort();
+ short dstY = inputStream.readShort();
+ byte depth = inputStream.readByte();
+ inputStream.skip(3);
+ int shmseg = inputStream.readInt();
+ inputStream.skip(4);
+
+ Drawable drawable = client.xServer.drawableManager.getDrawable(drawableId);
+ if (drawable == null) throw new BadDrawable(drawableId);
+
+ GraphicsContext graphicsContext = client.xServer.graphicsContextManager.getGraphicsContext(gcId);
+ if (graphicsContext == null) throw new BadGraphicsContext(gcId);
+
+ ByteBuffer data = client.xServer.getSHMSegmentManager().getData(shmseg);
+ if (data == null) throw new BadSHMSegment(shmseg);
+
+ if (graphicsContext.getFunction() != GraphicsContext.Function.COPY) {
+ throw new UnsupportedOperationException("GC Function other than COPY is not supported.");
+ }
+
+ drawable.drawImage(srcX, srcY, dstX, dstY, srcWidth, srcHeight, depth, data, totalWidth, totalHeight);
+ }
+
+ @Override
+ public void handleRequest(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int opcode = client.getRequestData();
+ switch (opcode) {
+ case ClientOpcodes.QUERY_VERSION :
+ queryVersion(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.ATTACH :
+ try (XLock lock = client.xServer.lock(XServer.Lockable.SHMSEGMENT_MANAGER)) {
+ attach(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.DETACH :
+ try (XLock lock = client.xServer.lock(XServer.Lockable.SHMSEGMENT_MANAGER)) {
+ detach(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.PUT_IMAGE :
+ try (XLock lock = client.xServer.lock(XServer.Lockable.SHMSEGMENT_MANAGER, XServer.Lockable.DRAWABLE_MANAGER, XServer.Lockable.GRAPHIC_CONTEXT_MANAGER)) {
+ putImage(client, inputStream, outputStream);
+ }
+ break;
+ default:
+ throw new BadImplementation();
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/extensions/PresentExtension.java b/app/src/main/java/com/winlator/xserver/extensions/PresentExtension.java
new file mode 100644
index 0000000..46f6d11
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/extensions/PresentExtension.java
@@ -0,0 +1,197 @@
+package com.winlator.xserver.extensions;
+
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_SUCCESS;
+
+import android.util.SparseArray;
+
+import com.winlator.renderer.GPUImage;
+import com.winlator.renderer.Texture;
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Drawable;
+import com.winlator.xserver.Pixmap;
+import com.winlator.xserver.Window;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.XLock;
+import com.winlator.xserver.XServer;
+import com.winlator.xserver.errors.BadImplementation;
+import com.winlator.xserver.errors.BadMatch;
+import com.winlator.xserver.errors.BadPixmap;
+import com.winlator.xserver.errors.BadWindow;
+import com.winlator.xserver.errors.XRequestError;
+import com.winlator.xserver.events.PresentCompleteNotify;
+import com.winlator.xserver.events.PresentIdleNotify;
+
+import java.io.IOException;
+
+public class PresentExtension implements Extension {
+ public static final byte MAJOR_OPCODE = -103;
+ public enum Kind {PIXMAP, MSC_NOTIFY}
+ public enum Mode {COPY, FLIP, SKIP}
+ private final SparseArray events = new SparseArray<>();
+ private SyncExtension syncExtension;
+
+ private static abstract class ClientOpcodes {
+ private static final byte QUERY_VERSION = 0;
+ private static final byte PRESENT_PIXMAP = 1;
+ private static final byte SELECT_INPUT = 3;
+ }
+
+ private static class Event {
+ private Window window;
+ private XClient client;
+ private int id;
+ private Bitmask mask;
+ }
+
+ @Override
+ public String getName() {
+ return "Present";
+ }
+
+ @Override
+ public byte getMajorOpcode() {
+ return MAJOR_OPCODE;
+ }
+
+ @Override
+ public byte getFirstErrorId() {
+ return 0;
+ }
+
+ @Override
+ public byte getFirstEventId() {
+ return 0;
+ }
+
+ private void sendIdleNotify(Window window, Pixmap pixmap, int serial, int idleFence) {
+ if (idleFence != 0) syncExtension.setTriggered(idleFence);
+
+ synchronized (events) {
+ for (int i = 0; i < events.size(); i++) {
+ Event event = events.valueAt(i);
+ if (event.window == window && event.mask.isSet(PresentIdleNotify.getEventMask())) {
+ event.client.sendEvent(new PresentIdleNotify(event.id, window, pixmap, serial, idleFence));
+ }
+ }
+ }
+ }
+
+ private void sendCompleteNotify(Window window, int serial, Kind kind, Mode mode, long ust, long msc) {
+ synchronized (events) {
+ for (int i = 0; i < events.size(); i++) {
+ Event event = events.valueAt(i);
+ if (event.window == window && event.mask.isSet(PresentCompleteNotify.getEventMask())) {
+ event.client.sendEvent(new PresentCompleteNotify(event.id, window, serial, kind, mode, ust, msc));
+ }
+ }
+ }
+ }
+
+ private static void queryVersion(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ inputStream.skip(8);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeInt(1);
+ outputStream.writeInt(0);
+ outputStream.writePad(16);
+ }
+ }
+
+ private void presentPixmap(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int windowId = inputStream.readInt();
+ int pixmapId = inputStream.readInt();
+ int serial = inputStream.readInt();
+ inputStream.skip(8);
+ short xOff = inputStream.readShort();
+ short yOff = inputStream.readShort();
+ inputStream.skip(8);
+ int idleFence = inputStream.readInt();
+ inputStream.skip(client.getRemainingRequestLength());
+
+ final Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+
+ final Pixmap pixmap = client.xServer.pixmapManager.getPixmap(pixmapId);
+ if (pixmap == null) throw new BadPixmap(pixmapId);
+
+ Drawable content = window.getContent();
+ if (content.visual.depth != pixmap.drawable.visual.depth) throw new BadMatch();
+
+ final int fakeInterval = 1000000 / 60;
+ long ust = System.nanoTime() / 1000;
+ long msc = ust / fakeInterval;
+
+ synchronized (content.renderLock) {
+ content.copyArea((short)0, (short)0, xOff, yOff, pixmap.drawable.width, pixmap.drawable.height, pixmap.drawable);
+ sendIdleNotify(window, pixmap, serial, idleFence);
+ sendCompleteNotify(window, serial, Kind.PIXMAP, Mode.COPY, ust, msc);
+ }
+ }
+
+ private void selectInput(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int eventId = inputStream.readInt();
+ int windowId = inputStream.readInt();
+ Bitmask mask = new Bitmask(inputStream.readInt());
+
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+
+ if (GPUImage.isSupported() && !mask.isEmpty()) {
+ Drawable content = window.getContent();
+ final Texture oldTexture = content.getTexture();
+ client.xServer.getRenderer().xServerView.queueEvent(oldTexture::destroy);
+ content.setTexture(new GPUImage(content.width, content.height));
+ }
+
+ synchronized (events) {
+ Event event = events.get(eventId);
+ if (event != null) {
+ if (event.window != window || event.client != client) throw new BadMatch();
+
+ if (!mask.isEmpty()) {
+ event.mask = mask;
+ }
+ else events.remove(eventId);
+ }
+ else {
+ event = new Event();
+ event.id = eventId;
+ event.window = window;
+ event.client = client;
+ event.mask = mask;
+ events.put(eventId, event);
+ }
+ }
+ }
+
+ @Override
+ public void handleRequest(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int opcode = client.getRequestData();
+ if (syncExtension == null) syncExtension = client.xServer.getExtension(SyncExtension.MAJOR_OPCODE);
+
+ switch (opcode) {
+ case ClientOpcodes.QUERY_VERSION :
+ queryVersion(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.PRESENT_PIXMAP:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.PIXMAP_MANAGER)) {
+ presentPixmap(client, inputStream, outputStream);
+ }
+ break;
+ case ClientOpcodes.SELECT_INPUT:
+ try (XLock lock = client.xServer.lock(XServer.Lockable.WINDOW_MANAGER)) {
+ selectInput(client, inputStream, outputStream);
+ }
+ break;
+ default:
+ throw new BadImplementation();
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/extensions/SyncExtension.java b/app/src/main/java/com/winlator/xserver/extensions/SyncExtension.java
new file mode 100644
index 0000000..4f88f1c
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/extensions/SyncExtension.java
@@ -0,0 +1,142 @@
+package com.winlator.xserver.extensions;
+
+import android.util.SparseBooleanArray;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.BadFence;
+import com.winlator.xserver.errors.BadIdChoice;
+import com.winlator.xserver.errors.BadImplementation;
+import com.winlator.xserver.errors.BadMatch;
+import com.winlator.xserver.errors.XRequestError;
+
+import java.io.IOException;
+
+public class SyncExtension implements Extension {
+ public static final byte MAJOR_OPCODE = -104;
+ private final SparseBooleanArray fences = new SparseBooleanArray();
+
+ private static abstract class ClientOpcodes {
+ private static final byte CREATE_FENCE = 14;
+ private static final byte TRIGGER_FENCE = 15;
+ private static final byte RESET_FENCE = 16;
+ private static final byte DESTROY_FENCE = 17;
+ private static final byte AWAIT_FENCE = 19;
+ }
+
+ @Override
+ public String getName() {
+ return "SYNC";
+ }
+
+ @Override
+ public byte getMajorOpcode() {
+ return MAJOR_OPCODE;
+ }
+
+ @Override
+ public byte getFirstErrorId() {
+ return Byte.MIN_VALUE;
+ }
+
+ @Override
+ public byte getFirstEventId() {
+ return 0;
+ }
+
+ public void setTriggered(int id) {
+ synchronized (fences) {
+ if (fences.indexOfKey(id) >= 0) fences.put(id, true);
+ }
+ }
+
+ private void createFence(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ synchronized (fences) {
+ inputStream.skip(4);
+ int id = inputStream.readInt();
+
+ if (fences.indexOfKey(id) >= 0) throw new BadIdChoice(id);
+
+ boolean initiallyTriggered = inputStream.readByte() == 1;
+ inputStream.skip(3);
+
+ fences.put(id, initiallyTriggered);
+ }
+ }
+
+ private void triggerFence(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ synchronized (fences) {
+ int id = inputStream.readInt();
+ if (fences.indexOfKey(id) < 0) throw new BadFence(id);
+ fences.put(id, true);
+ }
+ }
+
+ private void resetFence(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ synchronized (fences) {
+ int id = inputStream.readInt();
+ if (fences.indexOfKey(id) < 0) throw new BadFence(id);
+
+ boolean triggered = fences.get(id);
+ if (!triggered) throw new BadMatch();
+
+ fences.put(id, false);
+ }
+ }
+
+ private void destroyFence(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ synchronized (fences) {
+ int id = inputStream.readInt();
+ if (fences.indexOfKey(id) < 0) throw new BadFence(id);
+ fences.delete(id);
+ }
+ }
+
+ private void awaitFence(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ synchronized (fences) {
+ int length = client.getRemainingRequestLength();
+ int[] ids = new int[length / 4];
+ int i = 0;
+
+ while (length != 0) {
+ ids[i++] = inputStream.readInt();
+ length -= 4;
+ }
+
+ boolean anyTriggered = false;
+ do {
+ for (int id : ids) {
+ if (fences.indexOfKey(id) < 0) throw new BadFence(id);
+ anyTriggered = fences.get(id);
+ if (anyTriggered) break;
+ }
+ }
+ while (!anyTriggered);
+ }
+ }
+
+ @Override
+ public void handleRequest(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int opcode = client.getRequestData();
+ switch (opcode) {
+ case ClientOpcodes.CREATE_FENCE :
+ createFence(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.TRIGGER_FENCE:
+ triggerFence(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.RESET_FENCE:
+ resetFence(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.DESTROY_FENCE:
+ destroyFence(client, inputStream, outputStream);
+ break;
+ case ClientOpcodes.AWAIT_FENCE:
+ awaitFence(client, inputStream, outputStream);
+ break;
+ default:
+ throw new BadImplementation();
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/requests/AtomRequests.java b/app/src/main/java/com/winlator/xserver/requests/AtomRequests.java
new file mode 100644
index 0000000..c28a556
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/AtomRequests.java
@@ -0,0 +1,33 @@
+package com.winlator.xserver.requests;
+
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_SUCCESS;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Atom;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.BadAtom;
+import com.winlator.xserver.errors.XRequestError;
+
+import java.io.IOException;
+
+public abstract class AtomRequests {
+ public static void internAtom(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ boolean onlyIfExists = client.getRequestData() == 1;
+ short length = inputStream.readShort();
+ inputStream.skip(2);
+ String name = inputStream.readString8(length);
+ int id = onlyIfExists ? Atom.getId(name) : Atom.internAtom(name);
+ if (id < 0) throw new BadAtom(id);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeInt(id);
+ outputStream.writePad(20);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/requests/CursorRequests.java b/app/src/main/java/com/winlator/xserver/requests/CursorRequests.java
new file mode 100644
index 0000000..2a6900c
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/CursorRequests.java
@@ -0,0 +1,50 @@
+package com.winlator.xserver.requests;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xserver.Cursor;
+import com.winlator.xserver.Pixmap;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.BadIdChoice;
+import com.winlator.xserver.errors.BadMatch;
+import com.winlator.xserver.errors.BadPixmap;
+import com.winlator.xserver.errors.XRequestError;
+
+public abstract class CursorRequests {
+ public static void createCursor(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int cursorId = inputStream.readInt();
+ int sourcePixmapId = inputStream.readInt();
+ int maskPixmapId = inputStream.readInt();
+
+ if (!client.isValidResourceId(cursorId)) throw new BadIdChoice(cursorId);
+
+ Pixmap sourcePixmap = client.xServer.pixmapManager.getPixmap(sourcePixmapId);
+ if (sourcePixmap == null) throw new BadPixmap(sourcePixmapId);
+
+ Pixmap maskPixmap = client.xServer.pixmapManager.getPixmap(maskPixmapId);
+ if (maskPixmap != null && (
+ maskPixmap.drawable.visual.depth != 1 ||
+ maskPixmap.drawable.width != sourcePixmap.drawable.width ||
+ maskPixmap.drawable.height != sourcePixmap.drawable.height)) {
+ throw new BadMatch();
+ }
+
+ byte foreRed = (byte)inputStream.readShort();
+ byte foreGreen = (byte)inputStream.readShort();
+ byte foreBlue = (byte)inputStream.readShort();
+ byte backRed = (byte)inputStream.readShort();
+ byte backGreen = (byte)inputStream.readShort();
+ byte backBlue = (byte)inputStream.readShort();
+ short x = inputStream.readShort();
+ short y = inputStream.readShort();
+
+ Cursor cursor = client.xServer.cursorManager.createCursor(cursorId, x, y, sourcePixmap, maskPixmap);
+ if (cursor == null) throw new BadIdChoice(cursorId);
+ client.xServer.cursorManager.recolorCursor(cursor, foreRed, foreGreen, foreBlue, backRed, backGreen, backBlue);
+ client.registerAsOwnerOfResource(cursor);
+ }
+
+ public static void freeCursor(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ client.xServer.cursorManager.freeCursor(inputStream.readInt());
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/requests/DrawRequests.java b/app/src/main/java/com/winlator/xserver/requests/DrawRequests.java
new file mode 100644
index 0000000..e75c6ef
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/DrawRequests.java
@@ -0,0 +1,164 @@
+package com.winlator.xserver.requests;
+
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_SUCCESS;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Drawable;
+import com.winlator.xserver.GraphicsContext;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.BadDrawable;
+import com.winlator.xserver.errors.BadGraphicsContext;
+import com.winlator.xserver.errors.BadMatch;
+import com.winlator.xserver.errors.XRequestError;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public abstract class DrawRequests {
+ public enum Format {BITMAP, XY_PIXMAP, Z_PIXMAP}
+ private enum CoordinateMode {ORIGIN, PREVIOUS}
+
+ public static void putImage(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ Format format = Format.values()[client.getRequestData()];
+ int drawableId = inputStream.readInt();
+ int gcId = inputStream.readInt();
+ short width = inputStream.readShort();
+ short height = inputStream.readShort();
+ short dstX = inputStream.readShort();
+ short dstY = inputStream.readShort();
+ byte leftPad = inputStream.readByte();
+ byte depth = inputStream.readByte();
+ inputStream.skip(2);
+ int length = client.getRemainingRequestLength();
+ ByteBuffer data = inputStream.readByteBuffer(length);
+
+ Drawable drawable = client.xServer.drawableManager.getDrawable(drawableId);
+ if (drawable == null) throw new BadDrawable(drawableId);
+
+ GraphicsContext graphicsContext = client.xServer.graphicsContextManager.getGraphicsContext(gcId);
+ if (graphicsContext == null) throw new BadGraphicsContext(gcId);
+
+ if (!(graphicsContext.getFunction() == GraphicsContext.Function.COPY || format == Format.Z_PIXMAP)) {
+ throw new UnsupportedOperationException("GC Function other than COPY is not supported.");
+ }
+
+ switch (format) {
+ case BITMAP:
+ if (leftPad != 0) throw new UnsupportedOperationException("PutImage.leftPad cannot be != 0.");
+ if (depth == 1) {
+ drawable.drawImage((short)0, (short)0, dstX, dstY, width, height, (byte)1, data, width, height);
+ }
+ else throw new BadMatch();
+ break;
+ case XY_PIXMAP:
+ if (drawable.visual.depth != depth) throw new BadMatch();
+ break;
+ case Z_PIXMAP:
+ if (leftPad == 0) {
+ drawable.drawImage((short)0, (short)0, dstX, dstY, width, height, depth, data, width, height);
+ }
+ else throw new BadMatch();
+ break;
+ }
+ }
+
+ public static void getImage(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ Format format = Format.values()[client.getRequestData()];
+ int drawableId = inputStream.readInt();
+ short x = inputStream.readShort();
+ short y = inputStream.readShort();
+ short width = inputStream.readShort();
+ short height = inputStream.readShort();
+ inputStream.skip(4);
+
+ if (format != Format.Z_PIXMAP) throw new UnsupportedOperationException("Only Z_PIXMAP is supported.");
+
+ Drawable drawable = client.xServer.drawableManager.getDrawable(drawableId);
+ if (drawable == null) throw new BadDrawable(drawableId);
+ int visualId = client.xServer.pixmapManager.getPixmap(drawableId) == null ? drawable.visual.id : 0;
+ ByteBuffer data = drawable.getImage(x, y, width, height);
+ int length = data.limit();
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte(drawable.visual.depth);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt((length + 3) / 4);
+ outputStream.writeInt(visualId);
+ outputStream.writePad(20);
+ outputStream.write(data);
+ if ((-length & 3) > 0) outputStream.writePad(-length & 3);
+ }
+ }
+
+ public static void copyArea(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int srcDrawableId = inputStream.readInt();
+ int dstDrawableId = inputStream.readInt();
+ int gcId = inputStream.readInt();
+ short srcX = inputStream.readShort();
+ short srcY = inputStream.readShort();
+ short dstX = inputStream.readShort();
+ short dstY = inputStream.readShort();
+ short width = inputStream.readShort();
+ short height = inputStream.readShort();
+
+ Drawable srcDrawable = client.xServer.drawableManager.getDrawable(srcDrawableId);
+ if (srcDrawable == null) throw new BadDrawable(srcDrawableId);
+
+ Drawable dstDrawable = client.xServer.drawableManager.getDrawable(dstDrawableId);
+ if (dstDrawable == null) throw new BadDrawable(dstDrawableId);
+
+ GraphicsContext graphicsContext = client.xServer.graphicsContextManager.getGraphicsContext(gcId);
+ if (graphicsContext == null) throw new BadGraphicsContext(gcId);
+
+ if (srcDrawable.visual.depth != dstDrawable.visual.depth) throw new BadMatch();
+
+ dstDrawable.copyArea(srcX, srcY, dstX, dstY, width, height, srcDrawable, graphicsContext.getFunction());
+ }
+
+ public static void polyLine(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ CoordinateMode coordinateMode = CoordinateMode.values()[client.getRequestData()];
+ int drawableId = inputStream.readInt();
+ int gcId = inputStream.readInt();
+
+ Drawable drawable = client.xServer.drawableManager.getDrawable(drawableId);
+ if (drawable == null) throw new BadDrawable(drawableId);
+ GraphicsContext graphicsContext = client.xServer.graphicsContextManager.getGraphicsContext(gcId);
+ if (graphicsContext == null) throw new BadGraphicsContext(gcId);
+ int length = client.getRemainingRequestLength();
+
+ short[] points = new short[length / 2];
+ int i = 0;
+ while (length != 0) {
+ points[i++] = inputStream.readShort();
+ points[i++] = inputStream.readShort();
+ length -= 4;
+ }
+
+ if (coordinateMode == CoordinateMode.ORIGIN && graphicsContext.getLineWidth() > 0) {
+ drawable.drawLines(graphicsContext.getForeground(), graphicsContext.getLineWidth(), points);
+ }
+ }
+
+ public static void polyFillRectangle(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int drawableId = inputStream.readInt();
+ int gcId = inputStream.readInt();
+
+ Drawable drawable = client.xServer.drawableManager.getDrawable(drawableId);
+ if (drawable == null) throw new BadDrawable(drawableId);
+ GraphicsContext graphicsContext = client.xServer.graphicsContextManager.getGraphicsContext(gcId);
+ if (graphicsContext == null) throw new BadGraphicsContext(gcId);
+ int length = client.getRemainingRequestLength();
+
+ while (length != 0) {
+ short x = inputStream.readShort();
+ short y = inputStream.readShort();
+ short width = inputStream.readShort();
+ short height = inputStream.readShort();
+ drawable.fillRect(x, y, width, height, graphicsContext.getBackground());
+ length -= 8;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/requests/ExtensionRequests.java b/app/src/main/java/com/winlator/xserver/requests/ExtensionRequests.java
new file mode 100644
index 0000000..0e3fc1c
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/ExtensionRequests.java
@@ -0,0 +1,39 @@
+package com.winlator.xserver.requests;
+
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_SUCCESS;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.XRequestError;
+import com.winlator.xserver.extensions.Extension;
+
+import java.io.IOException;
+
+public abstract class ExtensionRequests {
+ public static void queryExtension(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ short length = inputStream.readShort();
+ inputStream.skip(2);
+ String name = inputStream.readString8(length);
+ Extension extension = client.xServer.getExtensionByName(name);
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+
+ if (extension != null) {
+ outputStream.writeByte((byte)1);
+ outputStream.writeByte(extension.getMajorOpcode());
+ outputStream.writeByte(extension.getFirstEventId());
+ outputStream.writeByte(extension.getFirstErrorId());
+ outputStream.writePad(20);
+ }
+ else {
+ outputStream.writeByte((byte)0);
+ outputStream.writePad(23);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/requests/FontRequests.java b/app/src/main/java/com/winlator/xserver/requests/FontRequests.java
new file mode 100644
index 0000000..9eb3a48
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/FontRequests.java
@@ -0,0 +1,36 @@
+package com.winlator.xserver.requests;
+
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_SUCCESS;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.XRequestError;
+
+import java.io.IOException;
+
+public abstract class FontRequests {
+ public static void openFont(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ inputStream.skip(4);
+ int length = inputStream.readShort();
+ inputStream.skip(2);
+ String name = inputStream.readString8(length);
+ if (!name.equals("cursor")) throw new UnsupportedOperationException("OpenFont supports only name: cursor.");
+ }
+
+ public static void listFonts(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ inputStream.skip(2);
+ short patternLength = inputStream.readShort();
+ inputStream.readString8(patternLength);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeShort((short)0);
+ outputStream.writePad(22);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/requests/GrabRequests.java b/app/src/main/java/com/winlator/xserver/requests/GrabRequests.java
new file mode 100644
index 0000000..a0b834d
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/GrabRequests.java
@@ -0,0 +1,66 @@
+package com.winlator.xserver.requests;
+
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_SUCCESS;
+
+import com.winlator.core.CursorLocker;
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.Window;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.BadWindow;
+import com.winlator.xserver.errors.XRequestError;
+
+import java.io.IOException;
+
+public abstract class GrabRequests {
+ private enum Status {SUCCESS, ALREADY_GRABBED, INVALID_TIME, NOT_VIEWABLE, FROZEN}
+
+ public static void grabPointer(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ if (client.xServer.cursorLocker.getState() == CursorLocker.State.LOCKED) {
+ client.skipRequest();
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)Status.ALREADY_GRABBED.ordinal());
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writePad(24);
+ }
+ return;
+ }
+
+ boolean ownerEvents = client.getRequestData() == 1;
+ int windowId = inputStream.readInt();
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+
+ Bitmask eventMask = new Bitmask(inputStream.readShort());
+ inputStream.skip(14);
+
+ Status status;
+ if (client.xServer.grabManager.getWindow() != null && client.xServer.grabManager.getClient() != client) {
+ status = Status.ALREADY_GRABBED;
+ }
+ else if (window.getMapState() != Window.MapState.VIEWABLE) {
+ status = Status.NOT_VIEWABLE;
+ }
+ else {
+ status = Status.SUCCESS;
+ client.xServer.grabManager.activatePointerGrab(window, ownerEvents, eventMask, client);
+ }
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)status.ordinal());
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writePad(24);
+ }
+ }
+
+ public static void ungrabPointer(XClient client, XInputStream inputStream, XOutputStream outputStream) {
+ inputStream.skip(4);
+ client.xServer.grabManager.deactivatePointerGrab();
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/requests/GraphicsContextRequests.java b/app/src/main/java/com/winlator/xserver/requests/GraphicsContextRequests.java
new file mode 100644
index 0000000..33ff8a3
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/GraphicsContextRequests.java
@@ -0,0 +1,43 @@
+package com.winlator.xserver.requests;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xserver.Drawable;
+import com.winlator.xserver.GraphicsContext;
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.BadDrawable;
+import com.winlator.xserver.errors.BadGraphicsContext;
+import com.winlator.xserver.errors.BadIdChoice;
+import com.winlator.xserver.errors.XRequestError;
+
+public abstract class GraphicsContextRequests {
+ public static void createGC(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int gcId = inputStream.readInt();
+ int drawableId = inputStream.readInt();
+ Bitmask valueMask = new Bitmask(inputStream.readInt());
+
+ if (!client.isValidResourceId(gcId)) throw new BadIdChoice(gcId);
+
+ Drawable drawable = client.xServer.drawableManager.getDrawable(drawableId);
+ if (drawable == null) throw new BadDrawable(drawableId);
+ GraphicsContext graphicsContext = client.xServer.graphicsContextManager.createGraphicsContext(gcId, drawable);
+ if (graphicsContext == null) throw new BadIdChoice(gcId);
+
+ client.registerAsOwnerOfResource(graphicsContext);
+ if (!valueMask.isEmpty()) client.xServer.graphicsContextManager.updateGraphicsContext(graphicsContext, valueMask, inputStream);
+ }
+
+ public static void changeGC(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int gcId = inputStream.readInt();
+ Bitmask valueMask = new Bitmask(inputStream.readInt());
+ GraphicsContext graphicsContext = client.xServer.graphicsContextManager.getGraphicsContext(gcId);
+ if (graphicsContext == null) throw new BadGraphicsContext(gcId);
+
+ if (!valueMask.isEmpty()) client.xServer.graphicsContextManager.updateGraphicsContext(graphicsContext, valueMask, inputStream);
+ }
+
+ public static void freeGC(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ client.xServer.graphicsContextManager.freeGraphicsContext(inputStream.readInt());
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/requests/KeyboardRequests.java b/app/src/main/java/com/winlator/xserver/requests/KeyboardRequests.java
new file mode 100644
index 0000000..c516a43
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/KeyboardRequests.java
@@ -0,0 +1,47 @@
+package com.winlator.xserver.requests;
+
+import static com.winlator.xserver.Keyboard.KEYSYMS_PER_KEYCODE;
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_SUCCESS;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Keyboard;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.XRequestError;
+
+import java.io.IOException;
+
+public abstract class KeyboardRequests {
+ public static void getKeyboardMapping(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ byte firstKeycode = inputStream.readByte();
+ int count = inputStream.readUnsignedByte();
+ inputStream.skip(2);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte(KEYSYMS_PER_KEYCODE);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(count);
+ outputStream.writePad(24);
+
+ int i = firstKeycode - Keyboard.MIN_KEYCODE;
+ while (count != 0) {
+ outputStream.writeInt(client.xServer.keyboard.keysyms[i]);
+ count--;
+ i++;
+ }
+ }
+ }
+
+ public static void getModifierMapping(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)1);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(2);
+ outputStream.writePad(24);
+ outputStream.writePad(8);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/winlator/xserver/requests/PixmapRequests.java b/app/src/main/java/com/winlator/xserver/requests/PixmapRequests.java
new file mode 100644
index 0000000..642d239
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/PixmapRequests.java
@@ -0,0 +1,35 @@
+package com.winlator.xserver.requests;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xserver.Drawable;
+import com.winlator.xserver.Pixmap;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.BadDrawable;
+import com.winlator.xserver.errors.BadIdChoice;
+import com.winlator.xserver.errors.XRequestError;
+
+public abstract class PixmapRequests {
+ public static void createPixmap(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ byte depth = client.getRequestData();
+ int pixmapId = inputStream.readInt();
+ int drawableId = inputStream.readInt();
+ short width = inputStream.readShort();
+ short height = inputStream.readShort();
+
+ if (!client.isValidResourceId(pixmapId)) throw new BadIdChoice(pixmapId);
+
+ Drawable drawable = client.xServer.drawableManager.getDrawable(drawableId);
+ if (drawable == null) throw new BadDrawable(drawableId);
+
+ Drawable backingStore = client.xServer.drawableManager.createDrawable(pixmapId, width, height, depth);
+ if (backingStore == null) throw new BadIdChoice(pixmapId);
+ Pixmap pixmap = client.xServer.pixmapManager.createPixmap(backingStore);
+ if (pixmap == null) throw new BadIdChoice(pixmapId);
+ client.registerAsOwnerOfResource(pixmap);
+ }
+
+ public static void freePixmap(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ client.xServer.pixmapManager.freePixmap(inputStream.readInt());
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/requests/SelectionRequests.java b/app/src/main/java/com/winlator/xserver/requests/SelectionRequests.java
new file mode 100644
index 0000000..c93b57e
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/SelectionRequests.java
@@ -0,0 +1,44 @@
+package com.winlator.xserver.requests;
+
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_SUCCESS;
+
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Atom;
+import com.winlator.xserver.Window;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.BadAtom;
+import com.winlator.xserver.errors.BadWindow;
+import com.winlator.xserver.errors.XRequestError;
+
+import java.io.IOException;
+
+public abstract class SelectionRequests {
+ public static void setSelectionOwner(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int windowId = inputStream.readInt();
+ int atom = inputStream.readInt();
+ int timestamp = inputStream.readInt();
+
+ Window owner = client.xServer.windowManager.getWindow(windowId);
+ if (owner == null) throw new BadWindow(windowId);
+ if (!Atom.isValid(atom)) throw new BadAtom(atom);
+
+ client.xServer.selectionManager.setSelection(atom, owner, client, timestamp);
+ }
+
+ public static void getSelectionOwner(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int atom = inputStream.readInt();
+ if (!Atom.isValid(atom)) throw new BadAtom(atom);
+ Window owner = client.xServer.selectionManager.getSelection(atom).owner;
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeInt(owner != null ? owner.id : 0);
+ outputStream.writePad(20);
+ }
+ }
+}
diff --git a/app/src/main/java/com/winlator/xserver/requests/WindowRequests.java b/app/src/main/java/com/winlator/xserver/requests/WindowRequests.java
new file mode 100644
index 0000000..6c1d808
--- /dev/null
+++ b/app/src/main/java/com/winlator/xserver/requests/WindowRequests.java
@@ -0,0 +1,441 @@
+package com.winlator.xserver.requests;
+
+import static com.winlator.xserver.XClientRequestHandler.RESPONSE_CODE_SUCCESS;
+
+import com.winlator.core.CursorLocker;
+import com.winlator.xconnector.XInputStream;
+import com.winlator.xconnector.XOutputStream;
+import com.winlator.xconnector.XStreamLock;
+import com.winlator.xserver.Drawable;
+import com.winlator.xserver.Bitmask;
+import com.winlator.xserver.WindowAttributes;
+import com.winlator.xserver.errors.BadDrawable;
+import com.winlator.xserver.errors.BadIdChoice;
+import com.winlator.xserver.errors.BadValue;
+import com.winlator.xserver.events.CreateNotify;
+import com.winlator.xserver.events.Event;
+import com.winlator.xserver.Property;
+import com.winlator.xserver.Visual;
+import com.winlator.xserver.Window;
+import com.winlator.xserver.WindowManager;
+import com.winlator.xserver.XClient;
+import com.winlator.xserver.errors.BadAccess;
+import com.winlator.xserver.errors.BadMatch;
+import com.winlator.xserver.errors.BadWindow;
+import com.winlator.xserver.errors.XRequestError;
+import com.winlator.xserver.events.RawEvent;
+
+import java.io.IOException;
+import java.util.List;
+
+public abstract class WindowRequests {
+ public static void createWindow(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ byte depth = client.getRequestData();
+ int windowId = inputStream.readInt();
+ int parentId = inputStream.readInt();
+
+ if (!client.isValidResourceId(windowId)) throw new BadIdChoice(windowId);
+
+ Window parent = client.xServer.windowManager.getWindow(parentId);
+ if (parent == null) throw new BadWindow(parentId);
+
+ short x = inputStream.readShort();
+ short y = inputStream.readShort();
+ short width = inputStream.readShort();
+ short height = inputStream.readShort();
+ short borderWidth = inputStream.readShort();
+ WindowAttributes.WindowClass windowClass = WindowAttributes.WindowClass.values()[(byte)inputStream.readShort()];
+ Visual visual = client.xServer.pixmapManager.getVisual(inputStream.readInt());
+ Bitmask valueMask = new Bitmask(inputStream.readInt());
+
+ Window window = client.xServer.windowManager.createWindow(windowId, parent, x, y, width, height, windowClass, visual, depth, client);
+ window.setBorderWidth(borderWidth);
+ if (!valueMask.isEmpty()) window.attributes.update(valueMask, inputStream, client);
+ client.setEventListenerForWindow(window, window.attributes.getEventMask());
+ client.registerAsOwnerOfResource(window);
+ parent.sendEvent(Event.SUBSTRUCTURE_NOTIFY, new CreateNotify(parent, window));
+ }
+
+ public static void getWindowAttributes(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int windowId = inputStream.readInt();
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)window.attributes.getBackingStore().ordinal());
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(3);
+ outputStream.writeInt(window.isInputOutput() ? window.getContent().visual.id : 0);
+ outputStream.writeShort((short)window.attributes.getWindowClass().ordinal());
+ outputStream.writeByte((byte)window.attributes.getBitGravity().ordinal());
+ outputStream.writeByte((byte)window.attributes.getWinGravity().ordinal());
+ outputStream.writeInt(window.attributes.getBackingPlanes());
+ outputStream.writeInt(window.attributes.getBackingPixel());
+ outputStream.writeByte((byte)(window.attributes.isSaveUnder() ? 1 : 0));
+ outputStream.writeByte((byte)1);
+ outputStream.writeByte((byte)window.getMapState().ordinal());
+ outputStream.writeByte((byte)(window.attributes.isOverrideRedirect() ? 1 : 0));
+ outputStream.writeInt(0);
+ outputStream.writeInt(window.getAllEventMasks().getBits());
+ outputStream.writeInt(client.getEventMaskForWindow(window).getBits());
+ outputStream.writeShort((short)window.attributes.getDoNotPropagateMask().getBits());
+ outputStream.writeShort((short)0);
+ }
+ }
+
+ public static void changeWindowAttributes(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int windowId = inputStream.readInt();
+ Bitmask valueMask = new Bitmask(inputStream.readInt());
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+ if (!valueMask.isEmpty()) {
+ window.attributes.update(valueMask, inputStream, client);
+
+ if (valueMask.isSet(WindowAttributes.FLAG_EVENT_MASK)) {
+ if (isClientCanSelectFor(Event.SUBSTRUCTURE_REDIRECT, window, client) &&
+ isClientCanSelectFor(Event.RESIZE_REDIRECT, window, client) &&
+ isClientCanSelectFor(Event.BUTTON_PRESS, window, client)) {
+ client.setEventListenerForWindow(window, window.attributes.getEventMask());
+ }
+ else throw new BadAccess();
+ }
+ }
+ }
+
+ private static boolean isClientCanSelectFor(int eventId, Window window, XClient client) {
+ return !window.attributes.getEventMask().isSet(eventId) || !(window.hasEventListenerFor(eventId) && !client.isInterestedIn(eventId, window));
+ }
+
+ public static void destroyWindow(XClient client, XInputStream inputStream, XOutputStream outputStream) {
+ client.xServer.windowManager.destroyWindow(inputStream.readInt());
+ }
+
+ public static void reparentWindow(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int windowId = inputStream.readInt();
+ int parentId = inputStream.readInt();
+ inputStream.skip(4);
+
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+
+ Window parent = client.xServer.windowManager.getWindow(parentId);
+ if (parent == null) throw new BadWindow(parentId);
+
+ client.xServer.windowManager.reparentWindow(window, parent);
+ }
+
+ public static void mapWindow(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int windowId = inputStream.readInt();
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+ client.xServer.windowManager.mapWindow(window);
+ }
+
+ public static void unmapWindow(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int windowId = inputStream.readInt();
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+ client.xServer.windowManager.unmapWindow(window);
+ }
+
+ public static void changeProperty(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ Property.Mode mode = Property.Mode.values()[client.getRequestData()];
+ int windowId = inputStream.readInt();
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+
+ int atom = inputStream.readInt();
+ int type = inputStream.readInt();
+ byte format = inputStream.readByte();
+ inputStream.skip(3);
+ int length = inputStream.readInt();
+ int totalSize = length * (format >> 3);
+
+ byte[] data = null;
+ if (totalSize > 0) {
+ data = new byte[totalSize];
+ inputStream.read(data);
+ inputStream.skip(-totalSize & 3);
+ }
+
+ if (!window.modifyProperty(atom, type, Property.Format.valueOf(format), mode, data)) throw new BadMatch();
+ }
+
+ public static void deleteProperty(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int windowId = inputStream.readInt();
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+ window.removeProperty(inputStream.readInt());
+ }
+
+ public static void getProperty(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ boolean delete = client.getRequestData() == 1;
+ short sequenceNumber = client.getSequenceNumber();
+ int windowId = inputStream.readInt();
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+
+ int atom = inputStream.readInt();
+ int type = inputStream.readInt();
+ int longOffset = inputStream.readInt();
+ int longLength = inputStream.readInt();
+ Property property = window.getProperty(atom);
+
+ int bytesAfter = 0;
+ try (XStreamLock lock = outputStream.lock()) {
+ if (property == null) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(0);
+ outputStream.writeInt(0);
+ outputStream.writeInt(0);
+ outputStream.writeInt(0);
+ outputStream.writePad(12);
+ }
+ else if (property.type != type && type != 0) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte(property.format.value);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt(0);
+ outputStream.writeInt(property.type);
+ outputStream.writeInt(0);
+ outputStream.writeInt(0);
+ outputStream.writePad(12);
+ }
+ else {
+ byte[] data = property.data();
+ int offset = longOffset * 4;
+ int length = Math.min(data.length - offset, longLength * 4);
+ if (length < 0) throw new BadValue(longOffset);
+ bytesAfter = data.length - (offset + length);
+
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte(property.format.value);
+ outputStream.writeShort(sequenceNumber);
+ outputStream.writeInt((length + 3) / 4);
+ outputStream.writeInt(property.type);
+ outputStream.writeInt(bytesAfter);
+ outputStream.writeInt(length / (property.format.value / 8));
+ outputStream.writePad(12);
+ outputStream.write(data, offset, length);
+ if ((-length & 3) > 0) outputStream.writePad(-length & 3);
+ }
+ }
+
+ if (delete && property != null && bytesAfter == 0) {
+ window.removeProperty(atom);
+ }
+ }
+
+ public static void queryPointer(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int windowId = inputStream.readInt();
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+ short rootX = client.xServer.pointer.getClampedX();
+ short rootY = client.xServer.pointer.getClampedY();
+ Window child = window.getChildByCoords(rootX, rootY);
+ short[] localPoint = window.rootPointToLocal(rootX, rootY);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)(client.xServer.cursorLocker.getState() != CursorLocker.State.LOCKED ? 1 : 0));
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeInt(client.xServer.windowManager.rootWindow.id);
+ outputStream.writeInt(child != null ? child.id : 0);
+ outputStream.writeShort(rootX);
+ outputStream.writeShort(rootY);
+ outputStream.writeShort(localPoint[0]);
+ outputStream.writeShort(localPoint[1]);
+ outputStream.writeShort((short)client.xServer.inputDeviceManager.getKeyButMask().getBits());
+ outputStream.writePad(6);
+ }
+ }
+
+ public static void translateCoordinates(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int srcWindowId = inputStream.readInt();
+ int dstWindowId = inputStream.readInt();
+ short srcX = inputStream.readShort();
+ short srcY = inputStream.readShort();
+
+ Window srcWindow = client.xServer.windowManager.getWindow(srcWindowId);
+ Window dstWindow = client.xServer.windowManager.getWindow(dstWindowId);
+
+ if (srcWindow == null) throw new BadWindow(srcWindowId);
+ if (dstWindow == null) throw new BadWindow(dstWindowId);
+
+ short[] rootPoint = srcWindow.localPointToRoot(srcX, srcY);
+ short[] localPoint = dstWindow.rootPointToLocal(rootPoint[0], rootPoint[1]);
+ Window child = dstWindow.getChildByCoords(rootPoint[0], rootPoint[1]);
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)1);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeInt(child != null ? child.id : 0);
+ outputStream.writeShort(localPoint[0]);
+ outputStream.writeShort(localPoint[1]);
+ outputStream.writePad(16);
+ }
+ }
+
+ public static void warpPointer(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ if (client.xServer.cursorLocker.getState() == CursorLocker.State.LOCKED) {
+ client.skipRequest();
+ return;
+ }
+
+ Window srcWindow = client.xServer.windowManager.getWindow(inputStream.readInt());
+ Window dstWindow = client.xServer.windowManager.getWindow(inputStream.readInt());
+ short srcX = inputStream.readShort();
+ short srcY = inputStream.readShort();
+ short srcWidth = inputStream.readShort();
+ short srcHeight = inputStream.readShort();
+ short dstX = inputStream.readShort();
+ short dstY = inputStream.readShort();
+
+ if (srcWindow != null) {
+ if (srcWidth == 0) srcWidth = (short)(srcWindow.getWidth() - srcX);
+ if (srcHeight == 0) srcHeight = (short)(srcWindow.getHeight() - srcY);
+
+ short[] localPoint = srcWindow.rootPointToLocal(client.xServer.pointer.getX(), client.xServer.pointer.getY());
+ boolean isContained = localPoint[0] >= srcX && localPoint[1] >= srcY && localPoint[0] < (srcX + srcWidth) && localPoint[1] < (srcY + srcHeight);
+ if (!isContained) return;
+ }
+
+ if (dstWindow == null) {
+ client.xServer.pointer.moveTo(client.xServer.pointer.getX() + dstX, client.xServer.pointer.getY() + dstY);
+ }
+ else {
+ short[] localPoint = dstWindow.localPointToRoot(dstX, dstY);
+ client.xServer.pointer.moveTo(localPoint[0], localPoint[1]);
+ }
+ }
+
+ public static void setInputFocus(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ WindowManager.FocusRevertTo focusRevertTo = WindowManager.FocusRevertTo.values()[client.getRequestData()];
+ int windowId = inputStream.readInt();
+ inputStream.skip(4);
+
+ switch (focusRevertTo) {
+ case NONE:
+ client.xServer.windowManager.setFocus(null, focusRevertTo);
+ break;
+ case POINTER_ROOT:
+ client.xServer.windowManager.setFocus(client.xServer.windowManager.rootWindow, focusRevertTo);
+ break;
+ case PARENT:
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+ client.xServer.windowManager.setFocus(window, focusRevertTo);
+ break;
+ }
+ }
+
+ public static void getInputFocus(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ Window focusedWindow = client.xServer.windowManager.getFocusedWindow();
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)client.xServer.windowManager.getFocusRevertTo().ordinal());
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeInt(focusedWindow != null ? focusedWindow.id : 0);
+ outputStream.writePad(20);
+ }
+ }
+
+ public static void configureWindow(XClient client, XInputStream inputStream, XOutputStream outputStream) throws XRequestError {
+ int windowId = inputStream.readInt();
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+ Bitmask valueMask = new Bitmask(inputStream.readShort());
+ inputStream.skip(2);
+ if (!valueMask.isEmpty()) client.xServer.windowManager.configureWindow(window, valueMask, inputStream);
+ }
+
+ public static void getGeometry(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int drawableId = inputStream.readInt();
+ Drawable drawable = client.xServer.drawableManager.getDrawable(drawableId);
+ if (drawable == null) throw new BadDrawable(drawableId);
+ Window window = client.xServer.windowManager.getWindow(drawableId);
+ short x = window != null ? window.getX() : 0;
+ short y = window != null ? window.getY() : 0;
+ short borderWidth = window != null ? window.getBorderWidth() : 0;
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte(drawable.visual.depth);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeInt(client.xServer.windowManager.rootWindow.id);
+ outputStream.writeShort(x);
+ outputStream.writeShort(y);
+ outputStream.writeShort(drawable.width);
+ outputStream.writeShort(drawable.height);
+ outputStream.writeShort(borderWidth);
+ outputStream.writePad(10);
+ }
+ }
+
+ public static void queryTree(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int windowId = inputStream.readInt();
+ Window window = client.xServer.windowManager.getWindow(windowId);
+ if (window == null) throw new BadWindow(windowId);
+ Window parent = window.getParent();
+ List children = window.getChildren();
+
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(children.size());
+ outputStream.writeInt(client.xServer.windowManager.rootWindow.id);
+ outputStream.writeInt(parent != null ? parent.id : 0);
+ outputStream.writeShort((short)children.size());
+ outputStream.writePad(14);
+
+ for (int i = children.size()-1; i >= 0; i--) outputStream.writeInt(children.get(i).id);
+ }
+ }
+
+ public static void sendEvent(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ int windowId = inputStream.readInt();
+
+ if (windowId == 0 || windowId == 1) {
+ client.skipRequest();
+ return;
+ }
+
+ Window destination = client.xServer.windowManager.getWindow(windowId);
+ if (destination == null) throw new BadWindow(windowId);
+
+ Bitmask eventMask = new Bitmask(inputStream.readInt());
+
+ byte[] data = new byte[32];
+ inputStream.read(data);
+ Event event = new RawEvent(data);
+
+ if (eventMask.isEmpty()) {
+ destination.originClient.sendEvent(event);
+ }
+ else destination.sendEvent(eventMask, event);
+ }
+
+ public static void getScreenSaver(XClient client, XInputStream inputStream, XOutputStream outputStream) throws IOException, XRequestError {
+ try (XStreamLock lock = outputStream.lock()) {
+ outputStream.writeByte(RESPONSE_CODE_SUCCESS);
+ outputStream.writeByte((byte)0);
+ outputStream.writeShort(client.getSequenceNumber());
+ outputStream.writeInt(0);
+ outputStream.writeShort((short)600);
+ outputStream.writeShort((short)600);
+ outputStream.writeByte((byte)1);
+ outputStream.writeByte((byte)1);
+ outputStream.writePad(18);
+ }
+ }
+}
diff --git a/app/src/main/jniLibs/arm64-v8a/libepoxy.so b/app/src/main/jniLibs/arm64-v8a/libepoxy.so
new file mode 100644
index 0000000..a858ffa
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libepoxy.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libltdl.so b/app/src/main/jniLibs/arm64-v8a/libltdl.so
new file mode 100644
index 0000000..ddbce25
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libltdl.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libproot-loader.so b/app/src/main/jniLibs/arm64-v8a/libproot-loader.so
new file mode 100644
index 0000000..4b70f21
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libproot-loader.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libproot-loader32.so b/app/src/main/jniLibs/arm64-v8a/libproot-loader32.so
new file mode 100644
index 0000000..eb559c9
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libproot-loader32.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libproot.so b/app/src/main/jniLibs/arm64-v8a/libproot.so
new file mode 100644
index 0000000..0404dcb
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libproot.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libpulse.so b/app/src/main/jniLibs/arm64-v8a/libpulse.so
new file mode 100644
index 0000000..e6524e3
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libpulse.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libpulseaudio.so b/app/src/main/jniLibs/arm64-v8a/libpulseaudio.so
new file mode 100644
index 0000000..aa280f9
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libpulseaudio.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libpulsecommon-13.0.so b/app/src/main/jniLibs/arm64-v8a/libpulsecommon-13.0.so
new file mode 100644
index 0000000..78e8c9a
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libpulsecommon-13.0.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libpulsecore-13.0.so b/app/src/main/jniLibs/arm64-v8a/libpulsecore-13.0.so
new file mode 100644
index 0000000..e614f88
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libpulsecore-13.0.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libsndfile.so b/app/src/main/jniLibs/arm64-v8a/libsndfile.so
new file mode 100644
index 0000000..578dc87
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libsndfile.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libvirglrenderer.so b/app/src/main/jniLibs/arm64-v8a/libvirglrenderer.so
new file mode 100644
index 0000000..0a8678f
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libvirglrenderer.so differ
diff --git a/app/src/main/jniLibs/armeabi-v7a/libepoxy.so b/app/src/main/jniLibs/armeabi-v7a/libepoxy.so
new file mode 100644
index 0000000..33480b6
Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libepoxy.so differ
diff --git a/app/src/main/jniLibs/armeabi-v7a/libltdl.so b/app/src/main/jniLibs/armeabi-v7a/libltdl.so
new file mode 100644
index 0000000..f36b6be
Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libltdl.so differ
diff --git a/app/src/main/jniLibs/armeabi-v7a/libproot-loader.so b/app/src/main/jniLibs/armeabi-v7a/libproot-loader.so
new file mode 100644
index 0000000..3bf74fc
Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libproot-loader.so differ
diff --git a/app/src/main/jniLibs/armeabi-v7a/libproot.so b/app/src/main/jniLibs/armeabi-v7a/libproot.so
new file mode 100644
index 0000000..d2b4350
Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libproot.so differ
diff --git a/app/src/main/jniLibs/armeabi-v7a/libpulse.so b/app/src/main/jniLibs/armeabi-v7a/libpulse.so
new file mode 100644
index 0000000..bd1779e
Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libpulse.so differ
diff --git a/app/src/main/jniLibs/armeabi-v7a/libpulseaudio.so b/app/src/main/jniLibs/armeabi-v7a/libpulseaudio.so
new file mode 100644
index 0000000..3577273
Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libpulseaudio.so differ
diff --git a/app/src/main/jniLibs/armeabi-v7a/libpulsecommon-13.0.so b/app/src/main/jniLibs/armeabi-v7a/libpulsecommon-13.0.so
new file mode 100644
index 0000000..23d7d67
Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libpulsecommon-13.0.so differ
diff --git a/app/src/main/jniLibs/armeabi-v7a/libpulsecore-13.0.so b/app/src/main/jniLibs/armeabi-v7a/libpulsecore-13.0.so
new file mode 100644
index 0000000..d5280c5
Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libpulsecore-13.0.so differ
diff --git a/app/src/main/jniLibs/armeabi-v7a/libsndfile.so b/app/src/main/jniLibs/armeabi-v7a/libsndfile.so
new file mode 100644
index 0000000..11cf5bb
Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libsndfile.so differ
diff --git a/app/src/main/jniLibs/armeabi-v7a/libvirglrenderer.so b/app/src/main/jniLibs/armeabi-v7a/libvirglrenderer.so
new file mode 100644
index 0000000..321e26f
Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libvirglrenderer.so differ
diff --git a/app/src/main/res/drawable-hdpi/button_neutral_normal.9.png b/app/src/main/res/drawable-hdpi/button_neutral_normal.9.png
new file mode 100644
index 0000000..eee1b87
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/button_neutral_normal.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/button_neutral_pressed.9.png b/app/src/main/res/drawable-hdpi/button_neutral_pressed.9.png
new file mode 100644
index 0000000..d84269d
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/button_neutral_pressed.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/button_positive_normal.9.png b/app/src/main/res/drawable-hdpi/button_positive_normal.9.png
new file mode 100644
index 0000000..e1a7a70
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/button_positive_normal.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/button_positive_pressed.9.png b/app/src/main/res/drawable-hdpi/button_positive_pressed.9.png
new file mode 100644
index 0000000..ee26710
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/button_positive_pressed.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/combo_box_normal.9.png b/app/src/main/res/drawable-hdpi/combo_box_normal.9.png
new file mode 100644
index 0000000..1d0f0e0
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/combo_box_normal.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/combo_box_pressed.9.png b/app/src/main/res/drawable-hdpi/combo_box_pressed.9.png
new file mode 100644
index 0000000..9aa9392
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/combo_box_pressed.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/content_dialog_background.9.png b/app/src/main/res/drawable-hdpi/content_dialog_background.9.png
new file mode 100644
index 0000000..9865f83
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/content_dialog_background.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/cursor.png b/app/src/main/res/drawable-hdpi/cursor.png
new file mode 100644
index 0000000..d5a4210
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cursor.png differ
diff --git a/app/src/main/res/drawable-hdpi/edit_text_focused.9.png b/app/src/main/res/drawable-hdpi/edit_text_focused.9.png
new file mode 100644
index 0000000..e4c6431
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/edit_text_focused.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/edit_text_normal.9.png b/app/src/main/res/drawable-hdpi/edit_text_normal.9.png
new file mode 100644
index 0000000..8658b5a
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/edit_text_normal.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/edit_text_pressed.9.png b/app/src/main/res/drawable-hdpi/edit_text_pressed.9.png
new file mode 100644
index 0000000..b9be712
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/edit_text_pressed.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_about.png b/app/src/main/res/drawable-hdpi/icon_about.png
new file mode 100644
index 0000000..d97b65a
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_about.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_add.png b/app/src/main/res/drawable-hdpi/icon_add.png
new file mode 100644
index 0000000..6e7998a
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_add.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_confirm.png b/app/src/main/res/drawable-hdpi/icon_confirm.png
new file mode 100644
index 0000000..e3b278e
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_confirm.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_container.png b/app/src/main/res/drawable-hdpi/icon_container.png
new file mode 100644
index 0000000..b74f715
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_container.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_cpu.png b/app/src/main/res/drawable-hdpi/icon_cpu.png
new file mode 100644
index 0000000..b8a198c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_cpu.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_duplicate.png b/app/src/main/res/drawable-hdpi/icon_duplicate.png
new file mode 100644
index 0000000..5230cb7
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_duplicate.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_edit.png b/app/src/main/res/drawable-hdpi/icon_edit.png
new file mode 100644
index 0000000..1a8112c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_edit.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_env_var.png b/app/src/main/res/drawable-hdpi/icon_env_var.png
new file mode 100644
index 0000000..7718ddc
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_env_var.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_exit.png b/app/src/main/res/drawable-hdpi/icon_exit.png
new file mode 100644
index 0000000..7468a93
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_exit.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_fullscreen.png b/app/src/main/res/drawable-hdpi/icon_fullscreen.png
new file mode 100644
index 0000000..b39ca42
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_fullscreen.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_gamepad.png b/app/src/main/res/drawable-hdpi/icon_gamepad.png
new file mode 100644
index 0000000..fb19f1c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_gamepad.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_help.png b/app/src/main/res/drawable-hdpi/icon_help.png
new file mode 100644
index 0000000..733277c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_help.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_info.png b/app/src/main/res/drawable-hdpi/icon_info.png
new file mode 100644
index 0000000..d4b05b9
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_info.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_input_controls.png b/app/src/main/res/drawable-hdpi/icon_input_controls.png
new file mode 100644
index 0000000..cf937e4
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_input_controls.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_keyboard.png b/app/src/main/res/drawable-hdpi/icon_keyboard.png
new file mode 100644
index 0000000..9499f8a
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_keyboard.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_list_item_menu.png b/app/src/main/res/drawable-hdpi/icon_list_item_menu.png
new file mode 100644
index 0000000..18ca19c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_list_item_menu.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_menu.png b/app/src/main/res/drawable-hdpi/icon_menu.png
new file mode 100644
index 0000000..20e8c7f
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_menu.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_open.png b/app/src/main/res/drawable-hdpi/icon_open.png
new file mode 100644
index 0000000..95a65e6
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_open.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_remove.png b/app/src/main/res/drawable-hdpi/icon_remove.png
new file mode 100644
index 0000000..b57df94
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_remove.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_settings.png b/app/src/main/res/drawable-hdpi/icon_settings.png
new file mode 100644
index 0000000..bbe34cd
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_settings.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_shortcut.png b/app/src/main/res/drawable-hdpi/icon_shortcut.png
new file mode 100644
index 0000000..1c11388
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_shortcut.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_task_manager.png b/app/src/main/res/drawable-hdpi/icon_task_manager.png
new file mode 100644
index 0000000..157b8a6
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_task_manager.png differ
diff --git a/app/src/main/res/drawable-hdpi/icon_wine.png b/app/src/main/res/drawable-hdpi/icon_wine.png
new file mode 100644
index 0000000..3659482
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_wine.png differ
diff --git a/app/src/main/res/drawable-hdpi/logo.png b/app/src/main/res/drawable-hdpi/logo.png
new file mode 100644
index 0000000..ff4de2d
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/logo.png differ
diff --git a/app/src/main/res/drawable-hdpi/preloader_background.9.png b/app/src/main/res/drawable-hdpi/preloader_background.9.png
new file mode 100644
index 0000000..cc32bfb
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/preloader_background.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/preloader_spinner.png b/app/src/main/res/drawable-hdpi/preloader_spinner.png
new file mode 100644
index 0000000..d1a062b
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/preloader_spinner.png differ
diff --git a/app/src/main/res/drawable-hdpi/table_head_background.9.png b/app/src/main/res/drawable-hdpi/table_head_background.9.png
new file mode 100644
index 0000000..3a0eb7d
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/table_head_background.9.png differ
diff --git a/app/src/main/res/drawable-hdpi/toggle_button_off.png b/app/src/main/res/drawable-hdpi/toggle_button_off.png
new file mode 100644
index 0000000..c012201
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/toggle_button_off.png differ
diff --git a/app/src/main/res/drawable-hdpi/toggle_button_on.png b/app/src/main/res/drawable-hdpi/toggle_button_on.png
new file mode 100644
index 0000000..71fd82d
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/toggle_button_on.png differ
diff --git a/app/src/main/res/drawable-hdpi/touchpad_help_main_menu.png b/app/src/main/res/drawable-hdpi/touchpad_help_main_menu.png
new file mode 100644
index 0000000..0459e6c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/touchpad_help_main_menu.png differ
diff --git a/app/src/main/res/drawable-hdpi/touchpad_help_mouse_left_click.png b/app/src/main/res/drawable-hdpi/touchpad_help_mouse_left_click.png
new file mode 100644
index 0000000..01f29c2
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/touchpad_help_mouse_left_click.png differ
diff --git a/app/src/main/res/drawable-hdpi/touchpad_help_mouse_right_click.png b/app/src/main/res/drawable-hdpi/touchpad_help_mouse_right_click.png
new file mode 100644
index 0000000..efa80d8
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/touchpad_help_mouse_right_click.png differ
diff --git a/app/src/main/res/drawable-hdpi/touchpad_help_mouse_scroll_wheel.png b/app/src/main/res/drawable-hdpi/touchpad_help_mouse_scroll_wheel.png
new file mode 100644
index 0000000..16c0efc
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/touchpad_help_mouse_scroll_wheel.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_action_bar_add.png b/app/src/main/res/drawable-xxhdpi/icon_action_bar_add.png
new file mode 100644
index 0000000..9429bf0
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_action_bar_add.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_action_bar_back.png b/app/src/main/res/drawable-xxhdpi/icon_action_bar_back.png
new file mode 100644
index 0000000..8573a10
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_action_bar_back.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_action_bar_menu.png b/app/src/main/res/drawable-xxhdpi/icon_action_bar_menu.png
new file mode 100644
index 0000000..47a83b7
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_action_bar_menu.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_popup_menu_cpu.png b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_cpu.png
new file mode 100644
index 0000000..5f45af8
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_cpu.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_popup_menu_download.png b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_download.png
new file mode 100644
index 0000000..f2b50f3
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_download.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_popup_menu_duplicate.png b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_duplicate.png
new file mode 100644
index 0000000..cc1bf9c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_duplicate.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_popup_menu_edit.png b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_edit.png
new file mode 100644
index 0000000..8ef3772
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_edit.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_popup_menu_info.png b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_info.png
new file mode 100644
index 0000000..c93f4ed
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_info.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_popup_menu_open.png b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_open.png
new file mode 100644
index 0000000..6533c1e
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_open.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_popup_menu_remove.png b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_remove.png
new file mode 100644
index 0000000..116811d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_remove.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_popup_menu_run.png b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_run.png
new file mode 100644
index 0000000..b912e40
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_run.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_popup_menu_settings.png b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_settings.png
new file mode 100644
index 0000000..ee810fb
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_popup_menu_settings.png differ
diff --git a/app/src/main/res/drawable/bordered_panel.xml b/app/src/main/res/drawable/bordered_panel.xml
new file mode 100644
index 0000000..6bd23c9
--- /dev/null
+++ b/app/src/main/res/drawable/bordered_panel.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/button_neutral.xml b/app/src/main/res/drawable/button_neutral.xml
new file mode 100644
index 0000000..ef865e7
--- /dev/null
+++ b/app/src/main/res/drawable/button_neutral.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/button_positive.xml b/app/src/main/res/drawable/button_positive.xml
new file mode 100644
index 0000000..5b97599
--- /dev/null
+++ b/app/src/main/res/drawable/button_positive.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/combo_box.xml b/app/src/main/res/drawable/combo_box.xml
new file mode 100644
index 0000000..db24352
--- /dev/null
+++ b/app/src/main/res/drawable/combo_box.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/custom_toast_background.xml b/app/src/main/res/drawable/custom_toast_background.xml
new file mode 100644
index 0000000..4af267c
--- /dev/null
+++ b/app/src/main/res/drawable/custom_toast_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/edit_text.xml b/app/src/main/res/drawable/edit_text.xml
new file mode 100644
index 0000000..b0b7132
--- /dev/null
+++ b/app/src/main/res/drawable/edit_text.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/icon_background.xml b/app/src/main/res/drawable/icon_background.xml
new file mode 100644
index 0000000..4094e36
--- /dev/null
+++ b/app/src/main/res/drawable/icon_background.xml
@@ -0,0 +1,15 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/input_controls_toolbar.xml b/app/src/main/res/drawable/input_controls_toolbar.xml
new file mode 100644
index 0000000..5dd56c9
--- /dev/null
+++ b/app/src/main/res/drawable/input_controls_toolbar.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/progress_bar_indeterminate.xml b/app/src/main/res/drawable/progress_bar_indeterminate.xml
new file mode 100644
index 0000000..c1de882
--- /dev/null
+++ b/app/src/main/res/drawable/progress_bar_indeterminate.xml
@@ -0,0 +1,112 @@
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/tab_layout_background.xml b/app/src/main/res/drawable/tab_layout_background.xml
new file mode 100644
index 0000000..62d4aed
--- /dev/null
+++ b/app/src/main/res/drawable/tab_layout_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/toggle_button_selector.xml b/app/src/main/res/drawable/toggle_button_selector.xml
new file mode 100644
index 0000000..da1dbc2
--- /dev/null
+++ b/app/src/main/res/drawable/toggle_button_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/about_dialog.xml b/app/src/main/res/layout/about_dialog.xml
new file mode 100644
index 0000000..bd94a81
--- /dev/null
+++ b/app/src/main/res/layout/about_dialog.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/add_env_var_dialog.xml b/app/src/main/res/layout/add_env_var_dialog.xml
new file mode 100644
index 0000000..2318757
--- /dev/null
+++ b/app/src/main/res/layout/add_env_var_dialog.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/binding_field.xml b/app/src/main/res/layout/binding_field.xml
new file mode 100644
index 0000000..d6f6549
--- /dev/null
+++ b/app/src/main/res/layout/binding_field.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/box86_64_edit_preset_dialog.xml b/app/src/main/res/layout/box86_64_edit_preset_dialog.xml
new file mode 100644
index 0000000..0d06f23
--- /dev/null
+++ b/app/src/main/res/layout/box86_64_edit_preset_dialog.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/box86_64_env_var_list_item.xml b/app/src/main/res/layout/box86_64_env_var_list_item.xml
new file mode 100644
index 0000000..49e0594
--- /dev/null
+++ b/app/src/main/res/layout/box86_64_env_var_list_item.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/container_detail_fragment.xml b/app/src/main/res/layout/container_detail_fragment.xml
new file mode 100644
index 0000000..e530c76
--- /dev/null
+++ b/app/src/main/res/layout/container_detail_fragment.xml
@@ -0,0 +1,478 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/container_list_item.xml b/app/src/main/res/layout/container_list_item.xml
new file mode 100644
index 0000000..568ab00
--- /dev/null
+++ b/app/src/main/res/layout/container_list_item.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/container_storage_info_dialog.xml b/app/src/main/res/layout/container_storage_info_dialog.xml
new file mode 100644
index 0000000..d31117c
--- /dev/null
+++ b/app/src/main/res/layout/container_storage_info_dialog.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/containers_fragment.xml b/app/src/main/res/layout/containers_fragment.xml
new file mode 100644
index 0000000..a954448
--- /dev/null
+++ b/app/src/main/res/layout/containers_fragment.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/content_dialog.xml b/app/src/main/res/layout/content_dialog.xml
new file mode 100644
index 0000000..7627112
--- /dev/null
+++ b/app/src/main/res/layout/content_dialog.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/control_element_settings.xml b/app/src/main/res/layout/control_element_settings.xml
new file mode 100644
index 0000000..473caf7
--- /dev/null
+++ b/app/src/main/res/layout/control_element_settings.xml
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/controls_editor_activity.xml b/app/src/main/res/layout/controls_editor_activity.xml
new file mode 100644
index 0000000..558a1c9
--- /dev/null
+++ b/app/src/main/res/layout/controls_editor_activity.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/cpu_list_dialog.xml b/app/src/main/res/layout/cpu_list_dialog.xml
new file mode 100644
index 0000000..3e4af65
--- /dev/null
+++ b/app/src/main/res/layout/cpu_list_dialog.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/cpu_list_item.xml b/app/src/main/res/layout/cpu_list_item.xml
new file mode 100644
index 0000000..466084a
--- /dev/null
+++ b/app/src/main/res/layout/cpu_list_item.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/custom_toast.xml b/app/src/main/res/layout/custom_toast.xml
new file mode 100644
index 0000000..cf0565a
--- /dev/null
+++ b/app/src/main/res/layout/custom_toast.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/download_progress_dialog.xml b/app/src/main/res/layout/download_progress_dialog.xml
new file mode 100644
index 0000000..6b23df0
--- /dev/null
+++ b/app/src/main/res/layout/download_progress_dialog.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/drive_list_item.xml b/app/src/main/res/layout/drive_list_item.xml
new file mode 100644
index 0000000..1070fb5
--- /dev/null
+++ b/app/src/main/res/layout/drive_list_item.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dxcomponent_list_item.xml b/app/src/main/res/layout/dxcomponent_list_item.xml
new file mode 100644
index 0000000..f1819f1
--- /dev/null
+++ b/app/src/main/res/layout/dxcomponent_list_item.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/env_vars_list_item.xml b/app/src/main/res/layout/env_vars_list_item.xml
new file mode 100644
index 0000000..7f5c9fa
--- /dev/null
+++ b/app/src/main/res/layout/env_vars_list_item.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/external_controller_binding_list_item.xml b/app/src/main/res/layout/external_controller_binding_list_item.xml
new file mode 100644
index 0000000..797a3e9
--- /dev/null
+++ b/app/src/main/res/layout/external_controller_binding_list_item.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/external_controller_bindings_activity.xml b/app/src/main/res/layout/external_controller_bindings_activity.xml
new file mode 100644
index 0000000..ae86311
--- /dev/null
+++ b/app/src/main/res/layout/external_controller_bindings_activity.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/external_controller_list_item.xml b/app/src/main/res/layout/external_controller_list_item.xml
new file mode 100644
index 0000000..fb3885b
--- /dev/null
+++ b/app/src/main/res/layout/external_controller_list_item.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/input_controls_dialog.xml b/app/src/main/res/layout/input_controls_dialog.xml
new file mode 100644
index 0000000..080e547
--- /dev/null
+++ b/app/src/main/res/layout/input_controls_dialog.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/input_controls_fragment.xml b/app/src/main/res/layout/input_controls_fragment.xml
new file mode 100644
index 0000000..32292dd
--- /dev/null
+++ b/app/src/main/res/layout/input_controls_fragment.xml
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/installed_wine_list_item.xml b/app/src/main/res/layout/installed_wine_list_item.xml
new file mode 100644
index 0000000..5d3c724
--- /dev/null
+++ b/app/src/main/res/layout/installed_wine_list_item.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml
new file mode 100644
index 0000000..44ca394
--- /dev/null
+++ b/app/src/main/res/layout/main_activity.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/main_menu_header.xml b/app/src/main/res/layout/main_menu_header.xml
new file mode 100644
index 0000000..e4cb512
--- /dev/null
+++ b/app/src/main/res/layout/main_menu_header.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/preloader_dialog.xml b/app/src/main/res/layout/preloader_dialog.xml
new file mode 100644
index 0000000..0a558f0
--- /dev/null
+++ b/app/src/main/res/layout/preloader_dialog.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/process_info_list_item.xml b/app/src/main/res/layout/process_info_list_item.xml
new file mode 100644
index 0000000..1da852c
--- /dev/null
+++ b/app/src/main/res/layout/process_info_list_item.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml
new file mode 100644
index 0000000..01a600d
--- /dev/null
+++ b/app/src/main/res/layout/settings_fragment.xml
@@ -0,0 +1,255 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/shortcut_list_item.xml b/app/src/main/res/layout/shortcut_list_item.xml
new file mode 100644
index 0000000..e3ca569
--- /dev/null
+++ b/app/src/main/res/layout/shortcut_list_item.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/shortcut_settings_dialog.xml b/app/src/main/res/layout/shortcut_settings_dialog.xml
new file mode 100644
index 0000000..85afa57
--- /dev/null
+++ b/app/src/main/res/layout/shortcut_settings_dialog.xml
@@ -0,0 +1,304 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/shortcuts_fragment.xml b/app/src/main/res/layout/shortcuts_fragment.xml
new file mode 100644
index 0000000..a954448
--- /dev/null
+++ b/app/src/main/res/layout/shortcuts_fragment.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/task_manager_dialog.xml b/app/src/main/res/layout/task_manager_dialog.xml
new file mode 100644
index 0000000..4d784c6
--- /dev/null
+++ b/app/src/main/res/layout/task_manager_dialog.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/touchpad_help_dialog.xml b/app/src/main/res/layout/touchpad_help_dialog.xml
new file mode 100644
index 0000000..ba8e9a5
--- /dev/null
+++ b/app/src/main/res/layout/touchpad_help_dialog.xml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/wine_install_options_dialog.xml b/app/src/main/res/layout/wine_install_options_dialog.xml
new file mode 100644
index 0000000..a769c83
--- /dev/null
+++ b/app/src/main/res/layout/wine_install_options_dialog.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/xserver_display_activity.xml b/app/src/main/res/layout/xserver_display_activity.xml
new file mode 100644
index 0000000..304bcf4
--- /dev/null
+++ b/app/src/main/res/layout/xserver_display_activity.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/container_popup_menu.xml b/app/src/main/res/menu/container_popup_menu.xml
new file mode 100644
index 0000000..4316d43
--- /dev/null
+++ b/app/src/main/res/menu/container_popup_menu.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/app/src/main/res/menu/containers_menu.xml b/app/src/main/res/menu/containers_menu.xml
new file mode 100644
index 0000000..3f1269e
--- /dev/null
+++ b/app/src/main/res/menu/containers_menu.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/app/src/main/res/menu/extra_args_popup_menu.xml b/app/src/main/res/menu/extra_args_popup_menu.xml
new file mode 100644
index 0000000..f497045
--- /dev/null
+++ b/app/src/main/res/menu/extra_args_popup_menu.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml
new file mode 100644
index 0000000..b0c0424
--- /dev/null
+++ b/app/src/main/res/menu/main_menu.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/open_file_popup_menu.xml b/app/src/main/res/menu/open_file_popup_menu.xml
new file mode 100644
index 0000000..c3d6b81
--- /dev/null
+++ b/app/src/main/res/menu/open_file_popup_menu.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/app/src/main/res/menu/process_popup_menu.xml b/app/src/main/res/menu/process_popup_menu.xml
new file mode 100644
index 0000000..79438a4
--- /dev/null
+++ b/app/src/main/res/menu/process_popup_menu.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/shortcut_popup_menu.xml b/app/src/main/res/menu/shortcut_popup_menu.xml
new file mode 100644
index 0000000..3fc992e
--- /dev/null
+++ b/app/src/main/res/menu/shortcut_popup_menu.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/app/src/main/res/menu/xserver_menu.xml b/app/src/main/res/menu/xserver_menu.xml
new file mode 100644
index 0000000..0a1b5db
--- /dev/null
+++ b/app/src/main/res/menu/xserver_menu.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..46b6c06
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..6f61936
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..46b6c06
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..19f15f0
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..c4b8125
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..19f15f0
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..9c47cd0
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..3836e0a
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9c47cd0
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..c1fc266
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..612a34e
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..c1fc266
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ecba3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..7bf7023
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..15ecba3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..82d2048
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,59 @@
+
+
+
+ - Custom
+ - 640x480 (4:3)
+ - 800x600 (4:3)
+ - 854x480 (16:9)
+ - 960x544 (16:9)
+ - 1024x768 (4:3)
+ - 1280x720 (16:9)
+ - 1280x800 (16:10)
+ - 1280x1024 (5:4)
+ - 1366x768 (16:9)
+ - 1440x900 (16:10)
+ - 1600x900 (16:9)
+ - 1920x1080 (16:9)
+
+
+ - LLVMpipe (Software)
+ - Turnip + Zink
+ - VirGL
+
+
+ - ALSA
+ - PulseAudio
+
+
+ - Keyboard
+ - Mouse
+
+
+ - Original WineD3D
+ - WineD3D 7.8
+ - WineD3D 8.14
+ - DXVK 0.96
+ - DXVK 1.10.3
+ - DXVK 1.5.5
+ - DXVK 2.2
+ - CNC DDraw
+ - D8VK 1.0
+
+
+ - Builtin (Wine)
+ - Native (Windows)
+
+
+ - 0.3.0 (old)
+ - 0.3.2 (new)
+
+
+ - 0.2.2 (old)
+ - 0.2.5 (new)
+
+
+ - 23.1.6 (a6xx)
+ - 23.3.0 (a7xx)
+ - 24.0.0 (a7xx)
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ba83d8
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+ #fafafa
+ #607d8b
+ #455a64
+ #0288d1
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..c5d5899
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e033e63
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,210 @@
+
+
+ Winlator
+ App Version
+ OBB Image Version
+ OBB Image
+ Run
+ Edit
+ Remove
+ Add
+ Shortcuts
+ Containers
+ Input Controls
+ No items to display
+ New Container
+ Edit Container
+ Creating Container...
+ Removing Container...
+ Duplicating Container...
+ Installing obb image...
+ Starting up...
+ Unable to install the obb image
+ Keyboard
+ Toggle Fullscreen
+ Exit
+ No control element selected
+ Scale
+ Shape
+ Type
+ Binding
+ Binding Up
+ Binding Right
+ Binding Down
+ Binding Left
+ Overlay Opacity
+ Profile
+ Select Profile
+ Profile Name
+ Name
+ OK
+ Cancel
+ Untitled
+ No profile selected
+ No profile found
+ Do you want to remove this profile?
+ Do you want to duplicate this profile?
+ Do you want to duplicate this preset?
+ Do you want to remove this container?
+ Do you want to duplicate this container?
+ Do you want to remove this shortcut?
+ Do you want to remove this controller?
+ Do you want to remove this wine version?
+ Do you want to remove this preset?
+ Do you want to end this process?
+ Controls Editor
+ Enabled
+ Disabled
+ Container
+ Screen Size
+ New Environment Variable
+ Environment Variables
+ Value
+ Processor Affinity
+ Cursor Speed
+ Cursor Visible
+ Lock Cursor
+ Toggle Switch
+ Profile exported to
+ Unable to import profile
+ Wine Registry Keys
+ CSMT (Command Stream Multi-Thread)
+ GPU Name
+ Offscreen Rendering Mode
+ Strict Shader Math
+ Video Memory Size
+ Show FPS
+ None
+ Custom Text
+ Icon
+ Import Profile
+ Export Profile
+ Touchpad Help
+ Mouse Left-Click
+ Short tap with one finger
+ Mouse Right-Click
+ Tap with one finger and short tap with another finger
+ Mouse Scroll Wheel
+ Two fingers tap and move
+ Main Menu
+ Four fingers tap
+ Duplicate
+ About
+ Credits and Third-party apps
+ External Controllers
+ Graphics Driver
+ Audio Driver
+ Loading...
+ Press any button on your controller
+ bindings
+ Show Touchscreen Controls
+ DX Wrapper
+ Mouse Warp Override
+ Direct3D
+ DirectInput
+ DirectSound
+ DirectMusic
+ DirectPlay
+ DirectShow
+ Settings
+ Exec Arguments
+ Default
+ DX Components
+ Drives
+ Force Fullscreen
+ Range
+ Enable
+ Disable
+ Force
+ Installed Wine
+ Install Wine
+ Arch
+ Version
+ Unable to install Wine
+ Unable to remove this wine version
+ Unable to find the obb image. Do you want to download the file and install?
+ Wine Version
+ Removing Wine...
+ Preparing installation...
+ Finishing installation...
+ Storage Info
+ Drive C:
+ Cache
+ Total
+ Clear Cache
+ Estimated Used Space
+ Advanced
+ Box86/Box64
+ Stop services on startup
+ System
+ Box86 Preset
+ Box64 Preset
+ Box86 Version
+ Box64 Version
+ Letter
+ Target Path
+ XServer
+ Use DRI3 extension (Experimental)
+ Width
+ Height
+ old
+ new
+ Task Manager
+ Process Name
+ PID
+ Memory
+ End Process
+ New Task
+ Processes
+ Turnip Version
+ Orientation
+ Horizontal
+ Vertical
+ Open File
+ Download File
+ Do you want to download the selected profiles?
+ Downloading File...
+ Unable to load profile list
+ Copy
+ Install OBB Image
+ Installed Version
+ Unable to download file
+ Compatibility
+ Intermediate
+ Performance (Unstable)
+ Preset
+ You cannot remove this preset
+ Use single CPU
+ WineD3D is a translation layer for DirectX 1-11 to OpenGL.
+ DXVK is a translation layer for Direct3D 9-11 to Vulkan.
+ D8VK is a translation layer for Direct3D 8 to Vulkan.
+ CNC DDraw is an implementation of DirectDraw for better compatibility with classic games.
+ ]]>
+
+ 0 : Treats CALL/RET as if it never needed flags (faster, unstable)
+ 1 : Most RET will need flags, most CALLS will not
+ 2 : All CALL/RET will need flags (slower)
+ ]]>
+ Generation of -NAN like on x86
+ Generation of precise x86 rounding
+ Use of Float/Double for x87 emulation
+
+ 0 : Don\'t try to build block as big as possible
+ 1 : Build Dynarec block as big as possible
+ 2 : Build Dynarec block bigger (continue when block overlaps, but only for blocks in elf memory)
+ 3 : Build Dynarec block bigger (continue when block overlaps, for all types of memory)
+ ]]>
+
+ 0 : Don\'t try anything special
+ 1 : Enable some Memory Barrier when writing to memory (on some MOV opcode)
+ 2 : All 1 plus a Memory Barrier on every write to memory using MOV
+ 2 : All 2 plus Memory Barrier when reading from memory and on some SSE/SSE2 opcodes
+ ]]>
+ Defines the max allowed forward value when building Block
+ Optimization of CALL/RET opcodes
+ Defines whether or not Dynarec will wait for the FillBlock to be ready
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..b3072b7
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+