Update app
2
.github/FUNDING.yml
vendored
@ -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
@ -0,0 +1,2 @@
|
||||
/.cxx
|
||||
/build
|
41
app/build.gradle
Normal 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
@ -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
|
49
app/src/main/AndroidManifest.xml
Normal 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>
|
11
app/src/main/assets/box64_env_vars.json
Normal 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"}
|
||||
]
|
BIN
app/src/main/assets/box86_64/box64-0.2.2.tzst
Normal file
BIN
app/src/main/assets/box86_64/box64-0.2.5.tzst
Normal file
BIN
app/src/main/assets/box86_64/box86-0.3.0.tzst
Normal file
BIN
app/src/main/assets/box86_64/box86-0.3.2.tzst
Normal file
10
app/src/main/assets/box86_env_vars.json
Normal 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"}
|
||||
]
|
7
app/src/main/assets/dxcomponents.json
Normal 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"]
|
||||
}
|
@ -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
|
@ -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
|
901
app/src/main/assets/dxwrapper/cnc-ddraw/ddraw.ini
Normal 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
|
||||
|
BIN
app/src/main/assets/dxwrapper/cnc-ddraw/ddraw.tzst
Normal file
BIN
app/src/main/assets/dxwrapper/d8vk-1.0.tzst
Normal file
BIN
app/src/main/assets/dxwrapper/dxvk-0.96.tzst
Normal file
BIN
app/src/main/assets/dxwrapper/dxvk-1.10.3.tzst
Normal file
BIN
app/src/main/assets/dxwrapper/dxvk-1.5.5.tzst
Normal file
BIN
app/src/main/assets/dxwrapper/dxvk-2.2.tzst
Normal file
BIN
app/src/main/assets/dxwrapper/wined3d-7.8.tzst
Normal file
BIN
app/src/main/assets/dxwrapper/wined3d-8.14.tzst
Normal file
57
app/src/main/assets/gpu_names.json
Normal 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
|
||||
}
|
||||
]
|
BIN
app/src/main/assets/graphics_driver/llvmpipe-23.1.6.tzst
Normal file
BIN
app/src/main/assets/graphics_driver/turnip-23.1.6.tzst
Normal file
BIN
app/src/main/assets/graphics_driver/turnip-23.3.0.tzst
Normal file
BIN
app/src/main/assets/graphics_driver/turnip-24.0.0.tzst
Normal file
BIN
app/src/main/assets/graphics_driver/virgl-22.1.7.tzst
Normal file
BIN
app/src/main/assets/graphics_driver/zink-22.2.2.tzst
Normal file
BIN
app/src/main/assets/inputcontrols/icons/0.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/assets/inputcontrols/icons/1.png
Normal file
After Width: | Height: | Size: 367 B |
BIN
app/src/main/assets/inputcontrols/icons/10.png
Normal file
After Width: | Height: | Size: 558 B |
BIN
app/src/main/assets/inputcontrols/icons/11.png
Normal file
After Width: | Height: | Size: 755 B |
BIN
app/src/main/assets/inputcontrols/icons/12.png
Normal file
After Width: | Height: | Size: 879 B |
BIN
app/src/main/assets/inputcontrols/icons/13.png
Normal file
After Width: | Height: | Size: 874 B |
BIN
app/src/main/assets/inputcontrols/icons/14.png
Normal file
After Width: | Height: | Size: 785 B |
BIN
app/src/main/assets/inputcontrols/icons/2.png
Normal file
After Width: | Height: | Size: 465 B |
BIN
app/src/main/assets/inputcontrols/icons/3.png
Normal file
After Width: | Height: | Size: 418 B |
BIN
app/src/main/assets/inputcontrols/icons/4.png
Normal file
After Width: | Height: | Size: 396 B |
BIN
app/src/main/assets/inputcontrols/icons/5.png
Normal file
After Width: | Height: | Size: 411 B |
BIN
app/src/main/assets/inputcontrols/icons/6.png
Normal file
After Width: | Height: | Size: 1004 B |
BIN
app/src/main/assets/inputcontrols/icons/7.png
Normal file
After Width: | Height: | Size: 961 B |
BIN
app/src/main/assets/inputcontrols/icons/8.png
Normal file
After Width: | Height: | Size: 639 B |
BIN
app/src/main/assets/inputcontrols/icons/9.png
Normal file
After Width: | Height: | Size: 761 B |
@ -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}]}
|
@ -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}]}
|
BIN
app/src/main/assets/patches.tzst
Normal file
BIN
app/src/main/assets/pulseaudio.tzst
Normal file
57
app/src/main/assets/wine_startmenu.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
17
app/src/main/cpp/CMakeLists.txt
Normal 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
@ -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);
|
||||
}
|
99
app/src/main/cpp/gpu_image.c
Normal 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);
|
||||
}
|
||||
}
|
88
app/src/main/cpp/sysvshared_memory.c
Normal 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;
|
||||
}
|
215
app/src/main/cpp/xconnector_epoll.c
Normal 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;
|
||||
}
|
522
app/src/main/java/com/winlator/ContainerDetailFragment.java
Normal 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);
|
||||
}
|
||||
}
|
250
app/src/main/java/com/winlator/ContainersFragment.java
Normal 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();
|
||||
}
|
||||
}
|
323
app/src/main/java/com/winlator/ControlsEditorActivity.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
366
app/src/main/java/com/winlator/InputControlsFragment.java
Normal 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);
|
||||
}
|
||||
}
|
215
app/src/main/java/com/winlator/MainActivity.java
Normal 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();
|
||||
}
|
||||
}
|
354
app/src/main/java/com/winlator/SettingsFragment.java
Normal 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();
|
||||
}
|
||||
}
|
143
app/src/main/java/com/winlator/ShortcutsFragment.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
781
app/src/main/java/com/winlator/XServerDisplayActivity.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
167
app/src/main/java/com/winlator/alsaserver/ALSAClient.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
12
app/src/main/java/com/winlator/alsaserver/RequestCodes.java
Normal 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;
|
||||
}
|
@ -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) {}
|
||||
}
|
||||
}
|
27
app/src/main/java/com/winlator/box86_64/Box86_64Preset.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
290
app/src/main/java/com/winlator/container/Container.java
Normal 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) {}
|
||||
}
|
||||
}
|
224
app/src/main/java/com/winlator/container/ContainerManager.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
122
app/src/main/java/com/winlator/container/Shortcut.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
171
app/src/main/java/com/winlator/contentdialog/ContentDialog.java
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
289
app/src/main/java/com/winlator/core/AppUtils.java
Normal 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();
|
||||
}
|
||||
}
|
40
app/src/main/java/com/winlator/core/ArrayUtils.java
Normal 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;
|
||||
}
|
||||
}
|
5
app/src/main/java/com/winlator/core/Callback.java
Normal file
@ -0,0 +1,5 @@
|
||||
package com.winlator.core;
|
||||
|
||||
public interface Callback<T> {
|
||||
void call(T object);
|
||||
}
|
76
app/src/main/java/com/winlator/core/CursorLocker.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
84
app/src/main/java/com/winlator/core/EnvVars.java
Normal 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();
|
||||
}
|
||||
}
|
320
app/src/main/java/com/winlator/core/FileUtils.java
Normal 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()));
|
||||
}
|
||||
}
|
112
app/src/main/java/com/winlator/core/GPUInformation.java
Normal 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}.*");
|
||||
}
|
||||
}
|
92
app/src/main/java/com/winlator/core/HttpUtils.java
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
125
app/src/main/java/com/winlator/core/MSLink.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
160
app/src/main/java/com/winlator/core/OBBImageInstaller.java
Normal 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.winlator.core;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public interface OnExtractFileListener {
|
||||
File onExtractFile(File destination, String entryName);
|
||||
}
|
62
app/src/main/java/com/winlator/core/PreloaderDialog.java
Normal 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();
|
||||
}
|
||||
}
|
157
app/src/main/java/com/winlator/core/ProcessHelper.java
Normal 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;
|
||||
}
|
||||
}
|
33
app/src/main/java/com/winlator/core/StreamUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
63
app/src/main/java/com/winlator/core/StringUtils.java
Normal 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;
|
||||
}
|
||||
}
|
50
app/src/main/java/com/winlator/core/TarXZUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
152
app/src/main/java/com/winlator/core/TarZstdUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
13
app/src/main/java/com/winlator/core/UnitUtils.java
Normal 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;
|
||||
}
|
||||
}
|
104
app/src/main/java/com/winlator/core/WineInfo.java
Normal 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;
|
||||
}
|
||||
}
|
363
app/src/main/java/com/winlator/core/WineRegistryEditor.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
||||
}
|
178
app/src/main/java/com/winlator/core/WineUtils.java
Normal 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) {}
|
||||
}
|
||||
}
|