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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +