Update app

This commit is contained in:
brunodev85 2023-12-23 15:49:09 -03:00
parent 4d3389bc91
commit 4322256d3e
397 changed files with 24750 additions and 1 deletions

2
.github/FUNDING.yml vendored
View File

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

2
app/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.cxx
/build

41
app/build.gradle Normal file
View File

@ -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'
}

19
app/proguard-rules.pro vendored Normal file
View File

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

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.winlator">
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<application
android:icon="@mipmap/ic_launcher"
android:appCategory="game"
android:isGame="true"
android:extractNativeLibs="true"
android:allowAudioPlaybackCapture="true"
android:label="@string/app_name">
<activity android:name="com.winlator.MainActivity"
android:theme="@style/AppTheme"
android:exported="true"
android:screenOrientation="sensor"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|density|navigation">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="com.winlator.XServerDisplayActivity"
android:exported="false"
android:theme="@style/AppThemeFullscreen"
android:launchMode="singleTask"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|density|navigation"
android:screenOrientation="sensorLandscape" />
<activity android:name="com.winlator.ControlsEditorActivity"
android:exported="false"
android:theme="@style/AppThemeFullscreen"
android:screenOrientation="sensorLandscape" />
<activity android:name="com.winlator.ExternalControllerBindingsActivity"
android:theme="@style/AppTheme"
android:exported="false" />
</application>
</manifest>

View File

@ -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"}
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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"}
]

View File

@ -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"]
}

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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
}
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 B

View File

@ -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}]}

View File

@ -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}]}

Binary file not shown.

Binary file not shown.

View File

@ -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"
}
]
}
]

View File

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

228
app/src/main/cpp/drawable.c Normal file
View File

@ -0,0 +1,228 @@
#include <jni.h>
#include <string.h>
#include <malloc.h>
#include <stdbool.h>
#include <stdlib.h>
#include <math.h>
#include <android/bitmap.h>
#include <android/log.h>
#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);
}

View File

@ -0,0 +1,99 @@
#include <android/log.h>
#include <android/hardware_buffer.h>
#include <android/native_window.h>
#define EGL_EGLEXT_PROTOTYPES
#define GL_GLEXT_PROTOTYPES
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <jni.h>
#include <string.h>
#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);
}
}

View File

@ -0,0 +1,88 @@
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdbool.h>
#include <pthread.h>
#include <sys/ipc.h>
#include <sys/syscall.h>
#include <jni.h>
#include <android/log.h>
#define __u32 uint32_t
#include <linux/ashmem.h>
#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;
}

View File

@ -0,0 +1,215 @@
#include <jni.h>
#include <sys/epoll.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/eventfd.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
#include <malloc.h>
#include <jni.h>
#include <android/log.h>
#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;
}

View File

@ -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<String> 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<WineInfo> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String[]> 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);
}
}

View File

@ -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<Container> 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<ContainersAdapter.ViewHolder> {
private final List<Container> 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<Container> 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<Long> 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();
}
}

View File

@ -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);
}
}
}

View File

@ -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<ControllerBindingsAdapter.ViewHolder> {
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();
}
}
}

View File

@ -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<ControlsProfile> 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<Integer> 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<ControlsProfile> profiles = manager.getProfiles();
ArrayList<String> 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<ExternalController> connectedControllers = ExternalController.getControllers();
ArrayList<ExternalController> 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);
}
}

View File

@ -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<Uri> 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<Fragment> fragments = fragmentManager.getFragments();
for (Fragment fragment : fragments) {
if (fragment instanceof ContainersFragment && fragment.isVisible()) {
finish();
return;
}
}
show(new ContainersFragment());
}
public void setOpenFileCallback(Callback<Uri> 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 (<a href=\"https://www.winlator.org\">winlator.org</a>)", 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("<br />",
"Ubuntu RootFs (<a href=\"https://releases.ubuntu.com/focal\">Focal Fossa</a>)",
"Wine (<a href=\"https://www.winehq.org\">winehq.org</a>)",
"Box86/Box64 by <a href=\"https://github.com/ptitSeb\">ptitseb</a>",
"PRoot (<a href=\"https://proot-me.github.io\">proot-me.github.io</a>)",
"Mesa3D (<a href=\"https://www.mesa3d.org\">mesa3d.org</a>)",
"DXVK (<a href=\"https://github.com/doitsujin/dxvk\">github.com/doitsujin/dxvk</a>)",
"D8VK (<a href=\"https://github.com/AlpyneDreams/d8vk\">github.com/AlpyneDreams/d8vk</a>)",
"CNC DDraw (<a href=\"https://github.com/FunkyFr3sh/cnc-ddraw\">github.com/FunkyFr3sh/cnc-ddraw</a>)"
);
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();
}
}

View File

@ -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<Uri> 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<String, Spinner> spinners = new ArrayMap<String, Spinner>() {{
put("box86", sBox86Preset);
put("box64", sBox64Preset);
}};
final Context context = getContext();
Callback<String> updateSpinner = (prefix) -> {
Box86_64PresetManager.loadSpinner(prefix, spinners.get(prefix), preferences.getString(prefix+"_preset", Box86_64Preset.COMPATIBILITY));
};
Callback<String> onAddPreset = (prefix) -> {
Box86_64EditPresetDialog dialog = new Box86_64EditPresetDialog(context, prefix, null);
dialog.setOnConfirmCallback(() -> updateSpinner.call(prefix));
dialog.show();
};
Callback<String> onEditPreset = (prefix) -> {
Box86_64EditPresetDialog dialog = new Box86_64EditPresetDialog(context, prefix, Box86_64PresetManager.getSpinnerSelectedId(spinners.get(prefix)));
dialog.setOnConfirmCallback(() -> updateSpinner.call(prefix));
dialog.show();
};
Callback<String> 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<String> 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<Container> 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<WineInfo> 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<String> 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();
}
}

View File

@ -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<Shortcut> shortcuts = manager.loadShortcuts();
recyclerView.setAdapter(new ShortcutsAdapter(shortcuts));
if (shortcuts.isEmpty()) emptyTextView.setVisibility(View.VISIBLE);
}
private class ShortcutsAdapter extends RecyclerView.Adapter<ShortcutsAdapter.ViewHolder> {
private final List<Shortcut> 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<Shortcut> 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);
}
}
}

View File

@ -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<String> 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<ControlsProfile> profiles = inputControlsManager.getProfiles();
ArrayList<String> 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<String> 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<String[]> 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();
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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) {}
}
}

View File

@ -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;
}
}

View File

@ -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<Box86_64Preset> getPresets(String prefix, Context context) {
ArrayList<Box86_64Preset> 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<String[]> 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<String[]>() {
@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<Box86_64Preset> 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<Box86_64Preset> 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;
}
}

View File

@ -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<String[]> drivesIterator() {
return drivesIterator(drives);
}
public static Iterable<String[]> drivesIterator(final String drives) {
final int[] index = {drives.indexOf(":")};
final String[] item = new String[2];
return () -> new Iterator<String[]>() {
@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<String[]> dxcomponentsIterator() {
return dxcomponentsIterator(dxcomponents);
}
public static Iterable<String[]> dxcomponentsIterator(final String dxcomponents) {
final int[] start = {0};
final int[] end = {dxcomponents.indexOf(",")};
final String[] item = new String[2];
return () -> new Iterator<String[]>() {
@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) {}
}
}

View File

@ -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<Container> 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<Container> 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<Container> 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<Shortcut> loadShortcuts() {
ArrayList<Shortcut> 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");
}
}
}

View File

@ -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<String> keys = extraData.keys();
while (keys.hasNext()) {
String key = keys.next();
try {
content += key + "=" + extraData.getString(key) + "\n";
}
catch (JSONException e) {}
}
}
FileUtils.writeString(file, content);
}
}

View File

@ -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);
}
});
}
}

View File

@ -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<String> 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<ArrayList<Integer>> 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<Integer> 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();
}
}

View File

@ -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);
}
}

View File

@ -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<Toast> 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<Boolean> 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<Integer> 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();
}
}

View File

@ -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> 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;
}
}

View File

@ -0,0 +1,5 @@
package com.winlator.core;
public interface Callback<T> {
void call(T object);
}

View File

@ -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));
}
}
}
}

View File

@ -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();
}
}

View File

@ -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<String> {
private final LinkedHashMap<String, String> 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<String> iterator() {
return data.keySet().iterator();
}
}

View File

@ -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<File> 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<String> readLines(File file) {
List<String> 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<Long> callback) {
Executors.newSingleThreadExecutor().execute(() -> getSize(file, callback));
}
private static void getSize(File file, Callback<Long> callback) {
if (file == null) return;
if (file.isFile()) {
callback.call(file.length());
return;
}
Stack<File> 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()));
}
}

View File

@ -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<String, String> loadGPUInformation(Context context) {
final Thread thread = Thread.currentThread();
final ArrayMap<String, String> 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<String, String> 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<String, String> 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<String, String> 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}.*");
}
}

View File

@ -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<String> 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<String> onDownloadComplete) {
Executors.newSingleThreadExecutor().execute(() -> downloadAsync(url, onDownloadComplete));
}
private static void downloadAsync(String url, File destination, AtomicBoolean interruptRef, Callback<Integer> onPublishProgress, Callback<Boolean> 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<Boolean> 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);
});
});
});
}
}

View File

@ -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();
}
}
}

View File

@ -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<File> 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<File> 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;
}
}

View File

@ -0,0 +1,7 @@
package com.winlator.core;
import java.io.File;
public interface OnExtractFileListener {
File onExtractFile(File destination, String entryName);
}

View File

@ -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();
}
}

View File

@ -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<String> 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<Integer> 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<String> 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<String> 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<Integer> terminationCallback) {
Executors.newSingleThreadExecutor().execute(() -> {
try {
int status = process.waitFor();
terminationCallback.call(status);
}
catch (InterruptedException e) {}
});
}
public static String[] splitCommand(String command) {
ArrayList<String> 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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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<WineInfo> CREATOR = new Parcelable.Creator<WineInfo>() {
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;
}
}

View File

@ -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<String> 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;
}
}
}

View File

@ -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) {}
}
}

View File

@ -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<File> 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<WineInfo> 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<WineInfo> 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<WineInfo> getInstalledWineInfos(Context context) {
ArrayList<WineInfo> 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<String[]> 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) {}
}
}

Some files were not shown because too many files have changed in this diff Show More