diff --git a/app/build.gradle b/app/build.gradle index 05c3a5f..f415597 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId 'com.winlator' minSdkVersion 26 targetSdkVersion 28 - versionCode 9 - versionName "3.2" + versionCode 10 + versionName "4.0" } buildTypes { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a75b95b..64e4e5f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,6 +44,7 @@ + android:exported="false" + android:screenOrientation="sensor" /> diff --git a/app/src/main/assets/box64_env_vars.json b/app/src/main/assets/box64_env_vars.json index 046ca8f..1f8d492 100644 --- a/app/src/main/assets/box64_env_vars.json +++ b/app/src/main/assets/box64_env_vars.json @@ -1,4 +1,5 @@ [ + {"name" : "BOX64_DYNAREC", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"}, {"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"}, diff --git a/app/src/main/assets/box86_64/box64-0.2.5.tzst b/app/src/main/assets/box86_64/box64-0.2.6.tzst similarity index 51% rename from app/src/main/assets/box86_64/box64-0.2.5.tzst rename to app/src/main/assets/box86_64/box64-0.2.6.tzst index fcbdac4..261dde1 100644 Binary files a/app/src/main/assets/box86_64/box64-0.2.5.tzst and b/app/src/main/assets/box86_64/box64-0.2.6.tzst differ diff --git a/app/src/main/assets/box86_env_vars.json b/app/src/main/assets/box86_env_vars.json index 1c988e1..579cb9d 100644 --- a/app/src/main/assets/box86_env_vars.json +++ b/app/src/main/assets/box86_env_vars.json @@ -1,4 +1,5 @@ [ + {"name" : "BOX86_DYNAREC", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"}, {"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"}, diff --git a/app/src/main/assets/inputcontrols/profiles/controls-3.icp b/app/src/main/assets/inputcontrols/profiles/controls-3.icp new file mode 100644 index 0000000..75a2497 --- /dev/null +++ b/app/src/main/assets/inputcontrols/profiles/controls-3.icp @@ -0,0 +1 @@ +{"id":3,"name":"Virtual Gamepad","cursorSpeed":1,"elements":[{"type":"D_PAD","shape":"CIRCLE","bindings":["GAMEPAD_DPAD_UP","GAMEPAD_DPAD_RIGHT","GAMEPAD_DPAD_DOWN","GAMEPAD_DPAD_LEFT"],"scale":0.85,"x":0.21568627655506134,"y":0.35555556416511536,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["GAMEPAD_BUTTON_X","NONE","NONE","NONE"],"scale":1,"x":0.8133170008659363,"y":0.7333333492279053,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["GAMEPAD_BUTTON_Y","NONE","NONE","NONE"],"scale":1,"x":0.8721405267715454,"y":0.6000000238418579,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["GAMEPAD_BUTTON_A","NONE","NONE","NONE"],"scale":1,"x":0.8721405267715454,"y":0.8666666746139526,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["GAMEPAD_BUTTON_B","NONE","NONE","NONE"],"scale":1,"x":0.9309640526771545,"y":0.7333333492279053,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"RECT","bindings":["GAMEPAD_BUTTON_R2","NONE","NONE","NONE"],"scale":1,"x":0.9215686321258545,"y":0.2888889014720917,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"RECT","bindings":["GAMEPAD_BUTTON_R1","NONE","NONE","NONE"],"scale":1,"x":0.9215686321258545,"y":0.42222222685813904,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"RECT","bindings":["GAMEPAD_BUTTON_L1","NONE","NONE","NONE"],"scale":1,"x":0.0784313753247261,"y":0.42222222685813904,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"RECT","bindings":["GAMEPAD_BUTTON_L2","NONE","NONE","NONE"],"scale":1,"x":0.0784313753247261,"y":0.2888889014720917,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","bindings":["GAMEPAD_BUTTON_START","NONE","NONE","NONE"],"scale":0.85,"x":0.538807213306427,"y":0.9111111164093018,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","bindings":["GAMEPAD_BUTTON_SELECT","NONE","NONE","NONE"],"scale":0.85,"x":0.46078431606292725,"y":0.9111111164093018,"toggleSwitch":false,"text":"","iconId":0},{"type":"STICK","shape":"CIRCLE","bindings":["GAMEPAD_LEFT_THUMB_UP","GAMEPAD_LEFT_THUMB_RIGHT","GAMEPAD_LEFT_THUMB_DOWN","GAMEPAD_LEFT_THUMB_LEFT"],"scale":1,"x":0.10784313827753067,"y":0.7333333492279053,"toggleSwitch":false,"text":"","iconId":0},{"type":"STICK","shape":"CIRCLE","bindings":["GAMEPAD_RIGHT_THUMB_UP","GAMEPAD_RIGHT_THUMB_RIGHT","GAMEPAD_RIGHT_THUMB_DOWN","GAMEPAD_RIGHT_THUMB_LEFT"],"scale":1,"x":0.7843137383460999,"y":0.35555556416511536,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["GAMEPAD_BUTTON_L3","NONE","NONE","NONE"],"scale":0.85,"x":0.21568627655506134,"y":0.9111111164093018,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","bindings":["GAMEPAD_BUTTON_R3","NONE","NONE","NONE"],"scale":0.85,"x":0.7843137383460999,"y":0.9111111164093018,"toggleSwitch":false,"text":"","iconId":0}]} \ No newline at end of file diff --git a/app/src/main/assets/patches.tzst b/app/src/main/assets/patches.tzst index 81f2596..f6bfd8a 100644 Binary files a/app/src/main/assets/patches.tzst and b/app/src/main/assets/patches.tzst differ diff --git a/app/src/main/java/com/winlator/ContainerDetailFragment.java b/app/src/main/java/com/winlator/ContainerDetailFragment.java index 4ea1bdd..fdd173a 100644 --- a/app/src/main/java/com/winlator/ContainerDetailFragment.java +++ b/app/src/main/java/com/winlator/ContainerDetailFragment.java @@ -58,7 +58,6 @@ public class ContainerDetailFragment extends Fragment { private PreloaderDialog preloaderDialog; private JSONArray gpuNames; private Callback openDirectoryCallback; - private final String[] defaultDLLOverrides = {"d3d8", "d3d9", "d3d10", "d3d10_1", "d3d10core", "d3d11", "d3d12", "d3d12core", "ddraw", "dxgi", "wined3d"}; public ContainerDetailFragment() { this(0); @@ -276,9 +275,6 @@ public class ContainerDetailFragment extends Fragment { 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"); } } diff --git a/app/src/main/java/com/winlator/ControlsEditorActivity.java b/app/src/main/java/com/winlator/ControlsEditorActivity.java index 760e6ea..94bdd8f 100644 --- a/app/src/main/java/com/winlator/ControlsEditorActivity.java +++ b/app/src/main/java/com/winlator/ControlsEditorActivity.java @@ -28,6 +28,7 @@ import com.winlator.core.AppUtils; import com.winlator.core.FileUtils; import com.winlator.core.UnitUtils; import com.winlator.widget.InputControlsView; +import com.winlator.widget.NumberPicker; import java.io.IOException; import java.io.InputStream; @@ -117,6 +118,14 @@ public class ControlsEditorActivity extends AppCompatActivity implements View.On inputControlsView.invalidate(); }); + NumberPicker npColumns = view.findViewById(R.id.NPColumns); + npColumns.setValue(element.getBindingCount()); + npColumns.setOnValueChangeListener((numberPicker, value) -> { + element.setBindingCount(value); + profile.save(); + inputControlsView.invalidate(); + }); + final TextView tvScale = view.findViewById(R.id.TVScale); SeekBar sbScale = view.findViewById(R.id.SBScale); sbScale.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @@ -229,7 +238,19 @@ public class ControlsEditorActivity extends AppCompatActivity implements View.On final Spinner sBinding = view.findViewById(R.id.SBinding); Runnable update = () -> { - String[] bindingEntries = sBindingType.getSelectedItemPosition() == 0 ? Binding.keyboardBindingLabels() : Binding.mouseBindingLabels(); + String[] bindingEntries = null; + switch (sBindingType.getSelectedItemPosition()) { + case 0: + bindingEntries = Binding.keyboardBindingLabels(); + break; + case 1: + bindingEntries = Binding.mouseBindingLabels(); + break; + case 2: + bindingEntries = Binding.gamepadBindingLabels(); + break; + } + sBinding.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, bindingEntries)); AppUtils.setSpinnerSelectionFromValue(sBinding, element.getBindingAt(index).toString()); }; @@ -243,13 +264,34 @@ public class ControlsEditorActivity extends AppCompatActivity implements View.On @Override public void onNothingSelected(AdapterView parent) {} }); - sBindingType.setSelection(element.getBindingAt(index).isKeyboard() ? 0 : 1, false); + + Binding selectedBinding = element.getBindingAt(index); + if (selectedBinding.isKeyboard()) { + sBindingType.setSelection(0, false); + } + else if (selectedBinding.isMouse()) { + sBindingType.setSelection(1, false); + } + else if (selectedBinding.isGamepad()) { + sBindingType.setSelection(2, 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]; + Binding binding = Binding.NONE; + switch (sBindingType.getSelectedItemPosition()) { + case 0: + binding = Binding.keyboardBindingValues()[position]; + break; + case 1: + binding = Binding.mouseBindingValues()[position]; + break; + case 2: + binding = Binding.gamepadBindingValues()[position]; + break; + } + if (binding != element.getBindingAt(index)) { element.setBindingAt(index, binding); profile.save(); diff --git a/app/src/main/java/com/winlator/ExternalControllerBindingsActivity.java b/app/src/main/java/com/winlator/ExternalControllerBindingsActivity.java index 9e35fca..633cf96 100644 --- a/app/src/main/java/com/winlator/ExternalControllerBindingsActivity.java +++ b/app/src/main/java/com/winlator/ExternalControllerBindingsActivity.java @@ -5,7 +5,6 @@ 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; @@ -76,6 +75,7 @@ public class ExternalControllerBindingsActivity extends AppCompatActivity { private void updateControllerBinding(int keyCode, Binding binding) { if (keyCode == KeyEvent.KEYCODE_UNKNOWN) return; + ExternalControllerBinding controllerBinding = controller.getControllerBinding(keyCode); int position; if (controllerBinding == null) { @@ -93,28 +93,29 @@ public class ExternalControllerBindingsActivity extends AppCompatActivity { recyclerView.scrollToPosition(position); } - private void processJoystickInput(MotionEvent event, int historyPos) { + private void processJoystickInput() { 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) { + final float[] values = {controller.state.thumbLX, controller.state.thumbLY, controller.state.thumbRX, controller.state.thumbRY, controller.state.getDPadX(), controller.state.getDPadY()}; + + byte sign; + for (int i = 0; i < axes.length; i++) { + if ((sign = Mathf.sign(values[i])) != 0) { + if (axes[i] == MotionEvent.AXIS_X || axes[i] == MotionEvent.AXIS_Z) { binding = sign > 0 ? Binding.MOUSE_MOVE_RIGHT : Binding.MOUSE_MOVE_LEFT; } - else if (axis == MotionEvent.AXIS_Y || axis == MotionEvent.AXIS_RZ) { + else if (axes[i] == MotionEvent.AXIS_Y || axes[i] == MotionEvent.AXIS_RZ) { binding = sign > 0 ? Binding.MOUSE_MOVE_DOWN : Binding.MOUSE_MOVE_UP; } - else if (axis == MotionEvent.AXIS_HAT_X) { + else if (axes[i] == MotionEvent.AXIS_HAT_X) { binding = sign > 0 ? Binding.KEY_D : Binding.KEY_A; } - else if (axis == MotionEvent.AXIS_HAT_Y) { + else if (axes[i] == MotionEvent.AXIS_HAT_Y) { binding = sign > 0 ? Binding.KEY_S : Binding.KEY_W; } - keyCode = ExternalControllerBinding.getKeyCodeForAxis(axis, sign); + keyCode = ExternalControllerBinding.getKeyCodeForAxis(axes[i], sign); break; } } @@ -122,41 +123,24 @@ public class ExternalControllerBindingsActivity extends AppCompatActivity { 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); + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (event.getDeviceId() == controller.getDeviceId() && controller.updateStateFromMotionEvent(event)) { + if (controller.state.isPressed(ExternalController.IDX_BUTTON_L2)) updateControllerBinding(KeyEvent.KEYCODE_BUTTON_L2, Binding.NONE); + if (controller.state.isPressed(ExternalController.IDX_BUTTON_R2)) updateControllerBinding(KeyEvent.KEYCODE_BUTTON_R2, Binding.NONE); + processJoystickInput(); return true; } - else return super.onKeyDown(keyCode, event); + return super.dispatchGenericMotionEvent(event); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getDeviceId() == controller.getDeviceId() && event.getRepeatCount() == 0) { + if (event.getAction() == KeyEvent.ACTION_DOWN) updateControllerBinding(event.getKeyCode(), Binding.NONE); + return true; + } + else return super.dispatchKeyEvent(event); } @Override @@ -208,7 +192,19 @@ public class ExternalControllerBindingsActivity extends AppCompatActivity { final Context $this = ExternalControllerBindingsActivity.this; Runnable update = () -> { - String[] bindingEntries = holder.bindingType.getSelectedItemPosition() == 0 ? Binding.keyboardBindingLabels() : Binding.mouseBindingLabels(); + String[] bindingEntries = null; + switch (holder.bindingType.getSelectedItemPosition()) { + case 0: + bindingEntries = Binding.keyboardBindingLabels(); + break; + case 1: + bindingEntries = Binding.mouseBindingLabels(); + break; + case 2: + bindingEntries = Binding.gamepadBindingLabels(); + break; + } + holder.binding.setAdapter(new ArrayAdapter<>($this, android.R.layout.simple_spinner_dropdown_item, bindingEntries)); AppUtils.setSpinnerSelectionFromValue(holder.binding, item.getBinding().toString()); }; @@ -227,8 +223,19 @@ public class ExternalControllerBindingsActivity extends AppCompatActivity { 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]; + Binding binding = Binding.NONE; + switch (holder.bindingType.getSelectedItemPosition()) { + case 0: + binding = Binding.keyboardBindingValues()[position]; + break; + case 1: + binding = Binding.mouseBindingValues()[position]; + break; + case 2: + binding = Binding.gamepadBindingValues()[position]; + break; + } + if (binding != item.getBinding()) { item.setBinding(binding); profile.save(); diff --git a/app/src/main/java/com/winlator/MainActivity.java b/app/src/main/java/com/winlator/MainActivity.java index 786bfd6..25645b1 100644 --- a/app/src/main/java/com/winlator/MainActivity.java +++ b/app/src/main/java/com/winlator/MainActivity.java @@ -26,10 +26,10 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import com.google.android.material.navigation.NavigationView; +import com.winlator.contentdialog.ContentDialog; 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; diff --git a/app/src/main/java/com/winlator/SettingsFragment.java b/app/src/main/java/com/winlator/SettingsFragment.java index ad0cb2b..6c48286 100644 --- a/app/src/main/java/com/winlator/SettingsFragment.java +++ b/app/src/main/java/com/winlator/SettingsFragment.java @@ -53,7 +53,7 @@ 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"; + public static final String DEFAULT_BOX64_VERSION = "0.2.6"; private Callback selectWineFileCallback; private PreloaderDialog preloaderDialog; private SharedPreferences preferences; @@ -96,17 +96,27 @@ public class SettingsFragment extends Fragment { preferences = PreferenceManager.getDefaultSharedPreferences(context); final Spinner sBox86Version = view.findViewById(R.id.SBox86Version); - AppUtils.setSpinnerSelectionFromIdentifier(sBox86Version, preferences.getString("box86_version", DEFAULT_BOX86_VERSION)); + String box86Version = preferences.getString("box86_version", DEFAULT_BOX86_VERSION); + if (!AppUtils.setSpinnerSelectionFromIdentifier(sBox86Version, box86Version)) { + AppUtils.setSpinnerSelectionFromIdentifier(sBox86Version, DEFAULT_BOX86_VERSION); + } final Spinner sBox64Version = view.findViewById(R.id.SBox64Version); - AppUtils.setSpinnerSelectionFromIdentifier(sBox64Version, preferences.getString("box64_version", DEFAULT_BOX64_VERSION)); + String box64Version = preferences.getString("box64_version", DEFAULT_BOX64_VERSION); + if (!AppUtils.setSpinnerSelectionFromIdentifier(sBox64Version, box64Version)) { + AppUtils.setSpinnerSelectionFromIdentifier(sBox64Version, 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))); + String defaultTurnipVersion = getDefaultTurnipVersion(context); + String turnipVersion = preferences.getString("turnip_version", defaultTurnipVersion); + if (!AppUtils.setSpinnerSelectionFromIdentifier(sTurnipVersion, turnipVersion)) { + AppUtils.setSpinnerSelectionFromIdentifier(sTurnipVersion, defaultTurnipVersion); + } final CheckBox cbUseDRI3 = view.findViewById(R.id.CBUseDRI3); cbUseDRI3.setChecked(preferences.getBoolean("use_dri3", true)); @@ -351,4 +361,14 @@ public class SettingsFragment extends Fragment { }); dialog.show(); } + + public static void resetBox86_64Version(AppCompatActivity activity) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + SharedPreferences.Editor editor = preferences.edit(); + editor.putString("box86_version", DEFAULT_BOX86_VERSION); + editor.putString("box64_version", DEFAULT_BOX64_VERSION); + editor.remove("current_box86_version"); + editor.remove("current_box64_version"); + editor.apply(); + } } diff --git a/app/src/main/java/com/winlator/XServerDisplayActivity.java b/app/src/main/java/com/winlator/XServerDisplayActivity.java index 4a19be7..212718e 100644 --- a/app/src/main/java/com/winlator/XServerDisplayActivity.java +++ b/app/src/main/java/com/winlator/XServerDisplayActivity.java @@ -6,6 +6,7 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.view.KeyEvent; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.widget.ArrayAdapter; import android.widget.CheckBox; @@ -27,6 +28,7 @@ import com.winlator.core.AppUtils; import com.winlator.core.CursorLocker; import com.winlator.core.EnvVars; import com.winlator.core.FileUtils; +import com.winlator.core.OBBImageInstaller; import com.winlator.core.OnExtractFileListener; import com.winlator.core.PreloaderDialog; import com.winlator.core.ProcessHelper; @@ -89,7 +91,7 @@ public class XServerDisplayActivity extends AppCompatActivity implements Navigat private boolean firstTimeBoot = false; private SharedPreferences preferences; private OnExtractFileListener onExtractFileListener; - private final WinHandler winHandler = new WinHandler(); + private final WinHandler winHandler = new WinHandler(this); private float globalCursorSpeed = 1.0f; @Override @@ -136,6 +138,9 @@ public class XServerDisplayActivity extends AppCompatActivity implements Navigat graphicsDriver = shortcut.getExtra("graphicsDriver", container.getGraphicsDriver()); audioDriver = shortcut.getExtra("audioDriver", container.getAudioDriver()); screenSize = shortcut.getExtra("screenSize", container.getScreenSize()); + + String dinputMapperType = shortcut.getExtra("dinputMapperType"); + if (!dinputMapperType.isEmpty()) winHandler.setDInputMapperType(Byte.parseByte(dinputMapperType)); } if (!wineInfo.isWin64()) { @@ -199,6 +204,7 @@ public class XServerDisplayActivity extends AppCompatActivity implements Navigat @Override protected void onDestroy() { + winHandler.stop(); if (environment != null) environment.stopEnvironmentComponents(); super.onDestroy(); } @@ -243,6 +249,7 @@ public class XServerDisplayActivity extends AppCompatActivity implements Navigat } private void exit() { + winHandler.stop(); if (environment != null) environment.stopEnvironmentComponents(); AppUtils.restartApplication(this); } @@ -257,9 +264,10 @@ public class XServerDisplayActivity extends AppCompatActivity implements Navigat File pulseaudioDir = new File(getFilesDir(), "pulseaudio"); TarZstdUtils.extract(this, "patches.tzst", rootDir, onExtractFileListener); TarZstdUtils.extract(this, "pulseaudio.tzst", pulseaudioDir); - WineUtils.applyRegistryKeyTweaks(this); + WineUtils.applyRegistryTweaks(this); container.putExtra("appVersion", appVersion); container.putExtra("imgVersion", imgVersion); + SettingsFragment.resetBox86_64Version(this); containerDataChanged = true; } @@ -288,9 +296,11 @@ public class XServerDisplayActivity extends AppCompatActivity implements Navigat envVars.put("MESA_DEBUG", "silent"); envVars.put("MESA_NO_ERROR", "1"); envVars.put("WINEPREFIX", ImageFs.WINEPREFIX); + envVars.put("WINEESYNC", "1"); if (MainActivity.DEBUG_LEVEL <= 1) envVars.put("WINEDEBUG", "-all"); String rootPath = imageFs.getRootDir().getPath(); + FileUtils.clear(imageFs.getTmpDir()); GuestProgramLauncherComponent guestProgramLauncherComponent = new GuestProgramLauncherComponent(); @@ -399,10 +409,6 @@ public class XServerDisplayActivity extends AppCompatActivity implements Navigat }; 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); @@ -423,7 +429,6 @@ public class XServerDisplayActivity extends AppCompatActivity implements Navigat }); 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(); @@ -510,12 +515,21 @@ public class XServerDisplayActivity extends AppCompatActivity implements Navigat dialog.show(); } + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + return !winHandler.onGenericMotionEvent(event) && super.dispatchGenericMotionEvent(event); + } + @Override public boolean dispatchKeyEvent(KeyEvent event) { - return (!inputControlsView.onKeyEvent(event) && xServer.keyboard.onKeyEvent(event)) || + return (!inputControlsView.onKeyEvent(event) && !winHandler.onKeyEvent(event) && xServer.keyboard.onKeyEvent(event)) || (!ExternalController.isGameController(event.getDevice()) && super.dispatchKeyEvent(event)); } + public InputControlsView getInputControlsView() { + return inputControlsView; + } + private void generateWineprefix() { Intent intent = getIntent(); @@ -731,7 +745,6 @@ public class XServerDisplayActivity extends AppCompatActivity implements Navigat 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 : ""; diff --git a/app/src/main/java/com/winlator/container/Container.java b/app/src/main/java/com/winlator/container/Container.java index c21c50e..0b30a7b 100644 --- a/app/src/main/java/com/winlator/container/Container.java +++ b/app/src/main/java/com/winlator/container/Container.java @@ -19,7 +19,7 @@ public class Container { 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_DXCOMPONENTS = "direct3d=1,directsound=1,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; diff --git a/app/src/main/java/com/winlator/contentdialog/ShortcutSettingsDialog.java b/app/src/main/java/com/winlator/contentdialog/ShortcutSettingsDialog.java index 73715ca..ebbb4aa 100644 --- a/app/src/main/java/com/winlator/contentdialog/ShortcutSettingsDialog.java +++ b/app/src/main/java/com/winlator/contentdialog/ShortcutSettingsDialog.java @@ -1,6 +1,5 @@ package com.winlator.contentdialog; -import android.app.Dialog; import android.content.Context; import android.content.res.Resources; import android.view.LayoutInflater; @@ -24,6 +23,7 @@ import com.winlator.core.AppUtils; import com.winlator.core.ArrayUtils; import com.winlator.core.EnvVars; import com.winlator.core.StringUtils; +import com.winlator.winhandler.WinHandler; import java.io.File; @@ -75,9 +75,6 @@ public class ShortcutSettingsDialog extends ContentDialog { 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")); @@ -87,6 +84,9 @@ public class ShortcutSettingsDialog extends ContentDialog { final Spinner sBox64Preset = findViewById(R.id.SBox64Preset); Box86_64PresetManager.loadSpinner("box64", sBox64Preset, shortcut.getExtra("box64Preset", shortcut.container.getBox64Preset())); + final Spinner sDInputMapperType = findViewById(R.id.SDInputMapperType); + sDInputMapperType.setSelection(Byte.parseByte(shortcut.getExtra("dinputMapperType", String.valueOf(WinHandler.DINPUT_MAPPER_TYPE_XINPUT)))); + createDXComponentsTab(); createEnvVarsTab(); @@ -118,7 +118,6 @@ public class ShortcutSettingsDialog extends ContentDialog { 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()); @@ -127,6 +126,8 @@ public class ShortcutSettingsDialog extends ContentDialog { shortcut.putExtra("box86Preset", !box86Preset.equals(shortcut.container.getBox86Preset()) ? box86Preset : null); shortcut.putExtra("box64Preset", !box64Preset.equals(shortcut.container.getBox64Preset()) ? box64Preset : null); + int dinputMapperType = sDInputMapperType.getSelectedItemPosition(); + shortcut.putExtra("dinputMapperType", dinputMapperType != WinHandler.DINPUT_MAPPER_TYPE_XINPUT ? String.valueOf(dinputMapperType) : null); shortcut.saveData(); } }); diff --git a/app/src/main/java/com/winlator/core/OBBImageInstaller.java b/app/src/main/java/com/winlator/core/OBBImageInstaller.java index 5c8fba7..50d996e 100644 --- a/app/src/main/java/com/winlator/core/OBBImageInstaller.java +++ b/app/src/main/java/com/winlator/core/OBBImageInstaller.java @@ -2,14 +2,12 @@ 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.SettingsFragment; import com.winlator.contentdialog.ContentDialog; import com.winlator.xenvironment.ImageFs; @@ -39,11 +37,7 @@ public abstract class OBBImageInstaller { 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(); + SettingsFragment.resetBox86_64Version(activity); if (foundVersion <= 0) foundVersion = imageFs.isValid() ? imageFs.getVersion() + 1 : 1; final int finalVersion = foundVersion; diff --git a/app/src/main/java/com/winlator/core/ProcessHelper.java b/app/src/main/java/com/winlator/core/ProcessHelper.java index 6978521..fb6474d 100644 --- a/app/src/main/java/com/winlator/core/ProcessHelper.java +++ b/app/src/main/java/com/winlator/core/ProcessHelper.java @@ -131,12 +131,6 @@ public abstract class ProcessHelper { 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; diff --git a/app/src/main/java/com/winlator/core/TarXZUtils.java b/app/src/main/java/com/winlator/core/TarXZUtils.java index 038a135..75a8e8e 100644 --- a/app/src/main/java/com/winlator/core/TarXZUtils.java +++ b/app/src/main/java/com/winlator/core/TarXZUtils.java @@ -11,14 +11,45 @@ import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; 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; public abstract class TarXZUtils { + public static boolean extract(Context context, String assetFile, File destination) { + try { + return extract(context.getAssets().open(assetFile), destination); + } + catch (IOException e) { + return false; + } + } + + public static boolean extract(File source, File destination) { + if (source == null || !source.isFile()) return false; + try { + return extract(new BufferedInputStream(new FileInputStream(source), StreamUtils.BUFFER_SIZE), destination); + } + catch (FileNotFoundException e) { + return false; + } + } + 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)); + try { + return extract(context.getContentResolver().openInputStream(source), destination); + } + catch (FileNotFoundException e) { + return false; + } + } + + private static boolean extract(InputStream source, File destination) { + if (source == null) return false; + try (InputStream inStream = new XZCompressorInputStream(source); ArchiveInputStream tar = new TarArchiveInputStream(inStream)) { TarArchiveEntry entry; while ((entry = (TarArchiveEntry)tar.getNextEntry()) != null) { diff --git a/app/src/main/java/com/winlator/core/WineUtils.java b/app/src/main/java/com/winlator/core/WineUtils.java index 188c3fa..2c0e98d 100644 --- a/app/src/main/java/com/winlator/core/WineUtils.java +++ b/app/src/main/java/com/winlator/core/WineUtils.java @@ -83,17 +83,20 @@ public abstract class WineUtils { return; } - File wineBin = new File(binDir, "wine"); - if (!wineBin.isFile()) { + File wineBin32 = new File(binDir, "wine"); + File wineBin64 = new File(binDir, "wine64"); + + if (!wineBin32.isFile()) { callback.call(null); return; } - final String arch = (new File(binDir, "wine64")).isFile() ? "x86_64" : "x86"; + final String arch = wineBin64.isFile() ? "x86_64" : "x86"; ImageFs imageFs = ImageFs.find(context); File rootDir = ImageFs.find(context).getRootDir(); - String wineBinRelPath = FileUtils.toRelativePath(rootDir.getPath(), wineBin.getPath()); + String wineBinAbsPath = arch.equals("x86_64") ? wineBin64.getPath() : wineBin32.getPath(); + String wineBinRelPath = FileUtils.toRelativePath(rootDir.getPath(), wineBinAbsPath); final String winePath = wineDir.getPath(); try { @@ -140,14 +143,26 @@ public abstract class WineUtils { return wineInfos; } - public static void applyRegistryKeyTweaks(Context context) { + public static void applyRegistryTweaks(Context context) { File rootDir = ImageFs.find(context).getRootDir(); File systemRegFile = new File(rootDir, ImageFs.WINEPREFIX+"/system.reg"); + File userRegFile = new File(rootDir, ImageFs.WINEPREFIX+"/user.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\""); + + registryEditor.setDwordValue("System\\CurrentControlSet\\Services\\winebus", "DisableHidraw", 1); + registryEditor.setDwordValue("System\\CurrentControlSet\\Services\\winebus", "DisableInput", 1); + registryEditor.setDwordValue("System\\CurrentControlSet\\Services\\winebus", "Enable SDL", 0); + } + + final String[] dllOverrides = {"d3d8", "d3d9", "d3d10", "d3d10_1", "d3d10core", "d3d11", "d3d12", "d3d12core", "ddraw", "dxgi", "wined3d", "dinput", "dinput8", "xinput1_1", "xinput1_2", "xinput1_3", "xinput1_4", "xinput9_1_0", "xinputuap"}; + final String dllOverridesKey = "Software\\Wine\\DllOverrides"; + + try (WineRegistryEditor registryEditor = new WineRegistryEditor(userRegFile)) { + for (String name : dllOverrides) registryEditor.setStringValue(dllOverridesKey, name, "native,builtin"); } } diff --git a/app/src/main/java/com/winlator/inputcontrols/Binding.java b/app/src/main/java/com/winlator/inputcontrols/Binding.java index 73f0960..472959c 100644 --- a/app/src/main/java/com/winlator/inputcontrols/Binding.java +++ b/app/src/main/java/com/winlator/inputcontrols/Binding.java @@ -8,7 +8,7 @@ import com.winlator.xserver.XKeycode; import java.util.ArrayList; public enum Binding { - NONE, MOUSE_LEFT_BUTTON, MOUSE_MIDDLE_BUTTON, MOUSE_RIGHT_BUTTON, MOUSE_MOVE_LEFT, MOUSE_MOVE_RIGHT, MOUSE_MOVE_UP, MOUSE_MOVE_DOWN, MOUSE_SCROLL_UP, MOUSE_SCROLL_DOWN, KEY_UP, KEY_RIGHT, KEY_DOWN, KEY_LEFT, KEY_ENTER, KEY_ESC, KEY_BKSP, KEY_DEL, KEY_TAB, KEY_SPACE, KEY_CTRL_L, KEY_CTRL_R, KEY_SHIFT_L, KEY_SHIFT_R, KEY_ALT_L, KEY_ALT_R, KEY_HOME, KEY_PRTSCN, KEY_CAPS_LOCK, KEY_NUM_LOCK, KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z, KEY_BRACKET_LEFT, KEY_BRACKET_RIGHT, KEY_BACKSLASH, KEY_SLASH, KEY_SEMICOLON, KEY_COMMA, KEY_PERIOD, KEY_APOSTROPHE, KEY_KP_ADD, KEY_MINUS, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, KEY_KP_0, KEY_KP_1, KEY_KP_2, KEY_KP_3, KEY_KP_4, KEY_KP_5, KEY_KP_6, KEY_KP_7, KEY_KP_8, KEY_KP_9; + NONE, MOUSE_LEFT_BUTTON, MOUSE_MIDDLE_BUTTON, MOUSE_RIGHT_BUTTON, MOUSE_MOVE_LEFT, MOUSE_MOVE_RIGHT, MOUSE_MOVE_UP, MOUSE_MOVE_DOWN, MOUSE_SCROLL_UP, MOUSE_SCROLL_DOWN, KEY_UP, KEY_RIGHT, KEY_DOWN, KEY_LEFT, KEY_ENTER, KEY_ESC, KEY_BKSP, KEY_DEL, KEY_TAB, KEY_SPACE, KEY_CTRL_L, KEY_CTRL_R, KEY_SHIFT_L, KEY_SHIFT_R, KEY_ALT_L, KEY_ALT_R, KEY_HOME, KEY_PRTSCN, KEY_CAPS_LOCK, KEY_NUM_LOCK, KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z, KEY_BRACKET_LEFT, KEY_BRACKET_RIGHT, KEY_BACKSLASH, KEY_SLASH, KEY_SEMICOLON, KEY_COMMA, KEY_PERIOD, KEY_APOSTROPHE, KEY_KP_ADD, KEY_MINUS, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, KEY_KP_0, KEY_KP_1, KEY_KP_2, KEY_KP_3, KEY_KP_4, KEY_KP_5, KEY_KP_6, KEY_KP_7, KEY_KP_8, KEY_KP_9, GAMEPAD_BUTTON_A, GAMEPAD_BUTTON_B, GAMEPAD_BUTTON_X, GAMEPAD_BUTTON_Y, GAMEPAD_BUTTON_L1, GAMEPAD_BUTTON_R1, GAMEPAD_BUTTON_SELECT, GAMEPAD_BUTTON_START, GAMEPAD_BUTTON_L3, GAMEPAD_BUTTON_R3, GAMEPAD_BUTTON_L2, GAMEPAD_BUTTON_R2, GAMEPAD_LEFT_THUMB_UP, GAMEPAD_LEFT_THUMB_RIGHT, GAMEPAD_LEFT_THUMB_DOWN, GAMEPAD_LEFT_THUMB_LEFT, GAMEPAD_RIGHT_THUMB_UP, GAMEPAD_RIGHT_THUMB_RIGHT, GAMEPAD_RIGHT_THUMB_DOWN, GAMEPAD_RIGHT_THUMB_LEFT, GAMEPAD_DPAD_UP, GAMEPAD_DPAD_RIGHT, GAMEPAD_DPAD_DOWN, GAMEPAD_DPAD_LEFT; public final XKeycode keycode; Binding() { @@ -59,7 +59,7 @@ public enum Binding { case KEY_KP_ADD: return "+"; default: - return super.toString().replaceAll("^(MOUSE_)|(KEY_)", "").replace("KP_", "NUMPAD_").replace("_", " "); + return super.toString().replaceAll("^(MOUSE_)|(KEY_)|(GAMEPAD_)", "").replace("KP_", "NUMPAD_").replace("_", " "); } } @@ -101,6 +101,10 @@ public enum Binding { return name().startsWith("KEY_") || this == NONE; } + public boolean isGamepad() { + return name().startsWith("GAMEPAD_"); + } + public static String[] mouseBindingLabels() { ArrayList names = new ArrayList<>(); for (Binding binding : values()) if (binding.isMouse()) names.add(binding.toString()); @@ -113,6 +117,12 @@ public enum Binding { return labels.toArray(new String[0]); } + public static String[] gamepadBindingLabels() { + ArrayList names = new ArrayList<>(); + for (Binding binding : values()) if (binding.isGamepad()) names.add(binding.toString()); + return names.toArray(new String[0]); + } + public static Binding[] mouseBindingValues() { ArrayList labels = new ArrayList<>(); for (Binding binding : values()) if (binding.isMouse()) labels.add(binding); @@ -124,4 +134,10 @@ public enum Binding { for (Binding binding : values()) if (binding.isKeyboard()) values.add(binding); return values.toArray(new Binding[0]); } + + public static Binding[] gamepadBindingValues() { + ArrayList labels = new ArrayList<>(); + for (Binding binding : values()) if (binding.isGamepad()) labels.add(binding); + return labels.toArray(new Binding[0]); + } } diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java index 9cf3ad0..a637a06 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java @@ -7,7 +7,6 @@ import android.graphics.Path; import android.graphics.PointF; import android.graphics.Rect; -import androidx.annotation.IntRange; import androidx.core.graphics.ColorUtils; import com.winlator.math.Mathf; @@ -17,7 +16,12 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.util.Arrays; + public class ControlElement { + public static final float STICK_DEAD_ZONE = 0.15f; + public static final float DPAD_DEAD_ZONE = 0.3f; + public static final float STICK_SENSITIVITY = 3.5f; public enum Type { BUTTON, D_PAD, RANGE_BUTTON, STICK; @@ -56,7 +60,7 @@ public class ControlElement { private final InputControlsView inputControlsView; private Type type = Type.BUTTON; private Shape shape = Shape.CIRCLE; - private final Binding[] bindings = {Binding.NONE, Binding.NONE, Binding.NONE, Binding.NONE}; + private Binding[] bindings = {Binding.NONE, Binding.NONE, Binding.NONE, Binding.NONE}; private float scale = 1.0f; private short x; private short y; @@ -64,7 +68,7 @@ public class ControlElement { private boolean toggleSwitch = false; private int currentPointerId = -1; private final Rect boundingBox = new Rect(); - private final boolean[] states = new boolean[4]; + private boolean[] states = new boolean[4]; private boolean boundingBoxNeedsUpdate = true; private String text = ""; private byte iconId; @@ -79,7 +83,7 @@ public class ControlElement { } private void reset() { - bindings[0] = bindings[1] = bindings[2] = bindings[3] = Binding.NONE; + Arrays.fill(bindings, Binding.NONE); if (type == Type.D_PAD || type == Type.STICK) { bindings[0] = Binding.KEY_W; bindings[1] = Binding.KEY_D; @@ -102,6 +106,17 @@ public class ControlElement { reset(); } + public int getBindingCount() { + return bindings.length; + } + + public void setBindingCount(int bindingCount) { + bindings = new Binding[bindingCount]; + Arrays.fill(bindings, Binding.NONE); + states = new boolean[bindingCount]; + boundingBoxNeedsUpdate = true; + } + public Shape getShape() { return shape; } @@ -136,12 +151,19 @@ public class ControlElement { this.toggleSwitch = toggleSwitch; } - public Binding getBindingAt(@IntRange(from = 0, to = 4) int index) { - return bindings[index]; + public Binding getBindingAt(int index) { + return index < bindings.length ? bindings[index] : Binding.NONE; } - public void setBindingAt(@IntRange(from = 0, to = 4) int index, Binding binding) { - this.bindings[index] = binding; + public void setBindingAt(int index, Binding binding) { + if (index >= bindings.length) { + int oldLength = bindings.length; + bindings = Arrays.copyOf(bindings, index+1); + Arrays.fill(bindings, oldLength-1, bindings.length, Binding.NONE); + states = new boolean[bindings.length]; + boundingBoxNeedsUpdate = true; + } + bindings[index] = binding; } public float getScale() { @@ -223,24 +245,27 @@ public class ControlElement { break; } break; - case D_PAD: + case D_PAD: { halfWidth = snappingSize * 7; halfHeight = snappingSize * 7; break; - case STICK: + } + case STICK: { halfWidth = snappingSize * 6; halfHeight = snappingSize * 6; break; - case RANGE_BUTTON: - if (orientation == 0) { - halfWidth = snappingSize * 12; - halfHeight = (int)(snappingSize * 1.5f); - } - else { - halfWidth = (int)(snappingSize * 1.5f); - halfHeight = snappingSize * 12; + } + case RANGE_BUTTON: { + halfWidth = snappingSize * ((bindings.length * 4 + 8) / 2); + halfHeight = snappingSize * 2; + + if (orientation == 1) { + int tmp = halfWidth; + halfWidth = halfHeight; + halfHeight = tmp; } break; + } } halfWidth *= scale; @@ -256,7 +281,7 @@ public class ControlElement { } else { Binding binding = getBindingAt(0); - String text = binding.toString().replace("NUMPAD ", "NP"); + String text = binding.toString().replace("NUMPAD ", "NP").replace("BUTTON ", ""); if (text.length() > 7) { String[] parts = text.split(" "); StringBuilder sb = new StringBuilder(); @@ -326,7 +351,8 @@ public class ControlElement { if (iconId > 0) { drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId); - } else { + } + else { String text = getDisplayText(); paint.setTextSize(Math.min(getTextSizeForWidth(paint, text, boundingBox.width() - strokeWidth * 2), snappingSize * 2 * scale)); paint.setTextAlign(Paint.Align.CENTER); @@ -379,9 +405,9 @@ public class ControlElement { case RANGE_BUTTON: { Range range = getRange(); int oldColor = paint.getColor(); - float radius = snappingSize * 0.5f * scale; + float minSize = Math.min(boundingBox.width(), boundingBox.height()); + float radius = minSize * 0.5f; float elementSize = (float)Math.max(boundingBox.width(), boundingBox.height()) / (bindings.length + 2); - float iconSize = Math.min(boundingBox.width(), boundingBox.height()) * 1.35f; float minTextSize = snappingSize * 2 * scale; if (orientation == 0) { @@ -391,7 +417,7 @@ public class ControlElement { float startX = boundingBox.left; canvas.drawRoundRect(startX, boundingBox.top, boundingBox.right, boundingBox.bottom, radius, radius, paint); - drawIcon(canvas, startX + elementSize * 0.5f, cy, iconSize, iconSize, 8); + drawIcon(canvas, startX + elementSize * 0.5f, cy, minSize, minSize, 8); startX += elementSize; for (byte i = 0; i < bindings.length; i++) { @@ -412,7 +438,7 @@ public class ControlElement { paint.setStyle(Paint.Style.STROKE); paint.setColor(oldColor); canvas.drawLine(startX, lineTop, startX, lineBottom, paint); - drawIcon(canvas, startX + elementSize * 0.5f, cy, iconSize, iconSize, 10); + drawIcon(canvas, startX + elementSize * 0.5f, cy, minSize, minSize, 10); } else { float lineLeft = boundingBox.left + strokeWidth * 0.5f; @@ -421,7 +447,7 @@ public class ControlElement { float startY = boundingBox.top; canvas.drawRoundRect(boundingBox.left, startY, boundingBox.right, boundingBox.bottom, radius, radius, paint); - drawIcon(canvas, cx, startY + elementSize * 0.5f, iconSize, iconSize, 9); + drawIcon(canvas, cx, startY + elementSize * 0.5f, minSize, minSize, 9); startY += elementSize; for (byte i = 0; i < bindings.length; i++) { @@ -442,7 +468,7 @@ public class ControlElement { paint.setStyle(Paint.Style.STROKE); paint.setColor(oldColor); canvas.drawLine(lineLeft, startY, lineRight, startY, paint); - drawIcon(canvas, cx, startY + elementSize * 0.5f, iconSize, iconSize, 11); + drawIcon(canvas, cx, startY + elementSize * 0.5f, minSize, minSize, 11); } break; } @@ -536,7 +562,7 @@ public class ControlElement { break; } - bindings[0] = bindings[1] = bindings[2] = bindings[3] = Binding.NONE; + Arrays.fill(bindings, Binding.NONE); bindings[currentElementIndex-1] = binding; states[currentElementIndex-1] = true; inputControlsView.handleInputEvent(binding, true); @@ -551,62 +577,51 @@ public class ControlElement { public boolean handleTouchMove(int pointerId, float x, float y) { if (pointerId == currentPointerId && (type == ControlElement.Type.D_PAD || type == Type.STICK)) { Rect boundingBox = getBoundingBox(); - x -= boundingBox.left; - y -= boundingBox.top; - float radius = boundingBox.width() * 0.5f; - float distance = Mathf.lengthSq(radius - x, radius - y); + float localX = x - boundingBox.left; + float localY = y - boundingBox.top; + float offsetX = localX - radius; + float offsetY = localY - radius; + + float distance = Mathf.lengthSq(radius - localX, radius - localY); if (distance > radius * radius) { - float angle = (float)Math.atan2(y - radius, x - radius); - x = (float)((Math.cos(angle) * radius) + radius); - y = (float)((Math.sin(angle) * radius) + radius); + float angle = (float)Math.atan2(offsetY, offsetX); + offsetX = (float)(Math.cos(angle) * radius); + offsetY = (float)(Math.sin(angle) * radius); } - float invWidth = 1.0f / boundingBox.width(); - float deltaX = Mathf.clamp(-1 + 2 * ((radius - x) + radius) * invWidth, -1, 1) * -1; - float deltaY = Mathf.clamp(-1 + 2 * ((radius - y) + radius) * invWidth, -1, 1) * -1; + float deltaX = Mathf.clamp(offsetX / radius, -1, 1); + float deltaY = Mathf.clamp(offsetY / radius, -1, 1); if (type == Type.STICK) { if (thumbstickPosition == null) thumbstickPosition = new PointF(); thumbstickPosition.x = boundingBox.left + deltaX * radius + radius; thumbstickPosition.y = boundingBox.top + deltaY * radius + radius; - boolean[] states = { - deltaY <= -InputControlsView.JOYSTICK_DEAD_ZONE, - deltaX >= InputControlsView.JOYSTICK_DEAD_ZONE, - deltaY >= InputControlsView.JOYSTICK_DEAD_ZONE, - deltaX <= -InputControlsView.JOYSTICK_DEAD_ZONE - }; - for (byte i = 0; i < 4; i++) { float value = i == 1 || i == 3 ? deltaX : deltaY; - if (states[i]) { - inputControlsView.handleInputEvent(getBindingAt(i), true, value); - this.states[i] = true; - } - else { - inputControlsView.handleInputEvent(getBindingAt(i), false, value); - this.states[i] = false; - } + value = Mathf.clamp(Math.max(0, Math.abs(value) - STICK_DEAD_ZONE) * Mathf.sign(value) * STICK_SENSITIVITY, -1, 1); + inputControlsView.handleInputEvent(getBindingAt(i), true, value); + this.states[i] = true; } inputControlsView.invalidate(); } else { - boolean[] states = { - deltaY <= -InputControlsView.DPAD_DEAD_ZONE, - deltaX >= InputControlsView.DPAD_DEAD_ZONE, - deltaY >= InputControlsView.DPAD_DEAD_ZONE, - deltaX <= -InputControlsView.DPAD_DEAD_ZONE - }; + final boolean[] states = {deltaY <= -DPAD_DEAD_ZONE, deltaX >= DPAD_DEAD_ZONE, deltaY >= DPAD_DEAD_ZONE, deltaX <= -DPAD_DEAD_ZONE}; for (byte i = 0; i < 4; i++) { - if (states[i] && !this.states[i]) { - inputControlsView.handleInputEvent(getBindingAt(i), true); + Binding binding = getBindingAt(i); + if (binding.isGamepad()) { + inputControlsView.handleInputEvent(binding, states[i]); + this.states[i] = states[i]; + } + else if (states[i] && !this.states[i]) { + inputControlsView.handleInputEvent(binding, true); this.states[i] = true; } else if (!states[i] && this.states[i]) { - inputControlsView.handleInputEvent(getBindingAt(i), false); + inputControlsView.handleInputEvent(binding, false); this.states[i] = false; } } @@ -627,7 +642,7 @@ public class ControlElement { } } else if (type == Type.RANGE_BUTTON || type == Type.D_PAD || type == Type.STICK) { - for (byte i = 0; i < 4; i++) { + for (byte i = 0; i < states.length; i++) { if (states[i]) inputControlsView.handleInputEvent(getBindingAt(i), false); states[i] = false; } diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java b/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java index b450fd6..18a8950 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java @@ -26,7 +26,9 @@ public class ControlsProfile implements Comparable { private final List immutableElements = Collections.unmodifiableList(elements); private boolean elementsLoaded = false; private boolean controllersLoaded = false; + private boolean virtualGamepad = false; private final Context context; + private GamepadState gamepadState; public ControlsProfile(Context context, int id) { this.context = context; @@ -49,9 +51,19 @@ public class ControlsProfile implements Comparable { this.cursorSpeed = cursorSpeed; } + public boolean isVirtualGamepad() { + return virtualGamepad; + } + + public GamepadState getGamepadState() { + if (gamepadState == null) gamepadState = new GamepadState(); + return gamepadState; + } + public ExternalController addController(String id) { ExternalController controller = getController(id); if (controller == null) controllers.add(controller = ExternalController.getController(id)); + controllersLoaded = true; return controller; } @@ -95,6 +107,7 @@ public class ControlsProfile implements Comparable { data.put("id", id); data.put("name", name); data.put("cursorSpeed", Float.valueOf(cursorSpeed)); + if (virtualGamepad) data.put("virtualGamepad", true); JSONArray elementsJSONArray = new JSONArray(); if (!elementsLoaded && file.isFile()) { @@ -183,8 +196,9 @@ public class ControlsProfile implements Comparable { public void loadElements(InputControlsView inputControlsView) { elements.clear(); elementsLoaded = false; + virtualGamepad = false; - File file = getProfileFile(inputControlsView.getContext(), id); + File file = getProfileFile(context, id); if (!file.isFile()) return; try { @@ -204,10 +218,15 @@ public class ControlsProfile implements Comparable { if (elementJSONObject.has("range")) element.setRange(ControlElement.Range.valueOf(elementJSONObject.getString("range"))); if (elementJSONObject.has("orientation")) element.setOrientation((byte)elementJSONObject.getInt("orientation")); + boolean hasGamepadBinding = true; JSONArray bindingsJSONArray = elementJSONObject.getJSONArray("bindings"); for (int j = 0; j < bindingsJSONArray.length(); j++) { + Binding binding = Binding.fromString(bindingsJSONArray.getString(j)); element.setBindingAt(j, Binding.fromString(bindingsJSONArray.getString(j))); + if (!binding.isGamepad()) hasGamepadBinding = false; } + + if (!virtualGamepad && hasGamepadBinding) virtualGamepad = true; elements.add(element); } elementsLoaded = true; diff --git a/app/src/main/java/com/winlator/inputcontrols/ExternalController.java b/app/src/main/java/com/winlator/inputcontrols/ExternalController.java index 51c465d..e776cee 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ExternalController.java +++ b/app/src/main/java/com/winlator/inputcontrols/ExternalController.java @@ -1,6 +1,7 @@ package com.winlator.inputcontrols; import android.view.InputDevice; +import android.view.KeyEvent; import android.view.MotionEvent; import androidx.annotation.Nullable; @@ -12,10 +13,23 @@ import org.json.JSONObject; import java.util.ArrayList; public class ExternalController { + public static final byte IDX_BUTTON_A = 0; + public static final byte IDX_BUTTON_B = 1; + public static final byte IDX_BUTTON_X = 2; + public static final byte IDX_BUTTON_Y = 3; + public static final byte IDX_BUTTON_L1 = 4; + public static final byte IDX_BUTTON_R1 = 5; + public static final byte IDX_BUTTON_SELECT = 6; + public static final byte IDX_BUTTON_START = 7; + public static final byte IDX_BUTTON_L3 = 8; + public static final byte IDX_BUTTON_R3 = 9; + public static final byte IDX_BUTTON_L2 = 10; + public static final byte IDX_BUTTON_R2 = 11; private String name; private String id; private int deviceId = -1; private final ArrayList controllerBindings = new ArrayList<>(); + public final GamepadState state = new GamepadState(); public String getName() { return name; @@ -104,11 +118,70 @@ public class ExternalController { return obj instanceof ExternalController ? ((ExternalController)obj).id.equals(this.id) : super.equals(obj); } + private void processJoystickInput(MotionEvent event, int historyPos) { + state.thumbLX = getCenteredAxis(event, MotionEvent.AXIS_X, historyPos); + state.thumbLY = getCenteredAxis(event, MotionEvent.AXIS_Y, historyPos); + state.thumbRX = getCenteredAxis(event, MotionEvent.AXIS_Z, historyPos); + state.thumbRY = getCenteredAxis(event, MotionEvent.AXIS_RZ, historyPos); + + if (historyPos == -1) { + float axisX = getCenteredAxis(event, MotionEvent.AXIS_HAT_X, historyPos); + float axisY = getCenteredAxis(event, MotionEvent.AXIS_HAT_Y, historyPos); + + state.dpad[0] = axisY == -1.0f && Math.abs(state.thumbLY) < ControlElement.STICK_DEAD_ZONE; + state.dpad[1] = axisX == 1.0f && Math.abs(state.thumbLX) < ControlElement.STICK_DEAD_ZONE; + state.dpad[2] = axisY == 1.0f && Math.abs(state.thumbLY) < ControlElement.STICK_DEAD_ZONE; + state.dpad[3] = axisX == -1.0f && Math.abs(state.thumbLX) < ControlElement.STICK_DEAD_ZONE; + } + } + + private void processTriggerButton(MotionEvent event) { + state.setPressed(IDX_BUTTON_L2, event.getAxisValue(MotionEvent.AXIS_LTRIGGER) == 1.0f || event.getAxisValue(MotionEvent.AXIS_BRAKE) == 1.0f); + state.setPressed(IDX_BUTTON_R2, event.getAxisValue(MotionEvent.AXIS_RTRIGGER) == 1.0f || event.getAxisValue(MotionEvent.AXIS_GAS) == 1.0f); + } + + public boolean updateStateFromMotionEvent(MotionEvent event) { + if (isJoystickDevice(event)) { + processTriggerButton(event); + int historySize = event.getHistorySize(); + for (int i = 0; i < historySize; i++) processJoystickInput(event, i); + processJoystickInput(event, -1); + return true; + } + return false; + } + + public boolean updateStateFromKeyEvent(KeyEvent event) { + boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN; + int keyCode = event.getKeyCode(); + int buttonIdx = getButtonIdxByKeyCode(keyCode); + if (buttonIdx != -1) { + state.setPressed(buttonIdx, pressed); + return true; + } + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + state.dpad[0] = pressed && Math.abs(state.thumbLY) < ControlElement.STICK_DEAD_ZONE; + return true; + case KeyEvent.KEYCODE_DPAD_RIGHT: + state.dpad[1] = pressed && Math.abs(state.thumbLX) < ControlElement.STICK_DEAD_ZONE; + return true; + case KeyEvent.KEYCODE_DPAD_DOWN: + state.dpad[2] = pressed && Math.abs(state.thumbLY) < ControlElement.STICK_DEAD_ZONE; + return true; + case KeyEvent.KEYCODE_DPAD_LEFT: + state.dpad[3] = pressed && Math.abs(state.thumbLX) < ControlElement.STICK_DEAD_ZONE; + return true; + } + return false; + } + public static ArrayList getControllers() { ArrayList controllers = new ArrayList<>(); for (int deviceId : InputDevice.getDeviceIds()) { InputDevice device = InputDevice.getDevice(deviceId); - if (device != null && !device.isVirtual() && isGameController(device)) { + if (isGameController(device)) { ExternalController controller = new ExternalController(); controller.setId(device.getDescriptor()); controller.setName(device.getName()); @@ -123,19 +196,42 @@ public class ExternalController { return null; } + public static ExternalController getController(int deviceId) { + for (int deviceId2 : InputDevice.getDeviceIds()) { + if (deviceId2 == deviceId || deviceId == 0) { + InputDevice device = InputDevice.getDevice(deviceId2); + if (isGameController(device)) { + ExternalController controller = new ExternalController(); + controller.setId(device.getDescriptor()); + controller.setName(device.getName()); + controller.deviceId = deviceId2; + return controller; + } + } + } + return null; + } + public static boolean isGameController(InputDevice device) { + if (device == null) return false; int sources = device.getSources(); - return (sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD || - (sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK; + return !device.isVirtual() && ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD || + (sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK); } public static float getCenteredAxis(MotionEvent event, int axis, int historyPos) { - InputDevice device = event.getDevice(); - InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource()); - if (range != null) { - float flat = range.getFlat(); - float value = historyPos < 0 ? event.getAxisValue(axis) : event.getHistoricalAxisValue(axis, historyPos); - if (Math.abs(value) > flat) return value; + if (axis == MotionEvent.AXIS_HAT_X || axis == MotionEvent.AXIS_HAT_Y) { + float value = event.getAxisValue(axis); + if (Math.abs(value) == 1.0f) return value; + } + else { + InputDevice device = event.getDevice(); + InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource()); + if (range != null) { + float flat = range.getFlat(); + float value = historyPos < 0 ? event.getAxisValue(axis) : event.getHistoricalAxisValue(axis, historyPos); + if (Math.abs(value) > flat) return value; + } } return 0; } @@ -144,7 +240,65 @@ public class ExternalController { return (event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.getAction() == MotionEvent.ACTION_MOVE; } - public static boolean isDPadDevice(MotionEvent event) { - return (event.getSource() & InputDevice.SOURCE_DPAD) != InputDevice.SOURCE_DPAD && event.getAction() == MotionEvent.ACTION_MOVE; + public static int getButtonIdxByKeyCode(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_BUTTON_A: + return IDX_BUTTON_A; + case KeyEvent.KEYCODE_BUTTON_B: + return IDX_BUTTON_B; + case KeyEvent.KEYCODE_BUTTON_X: + return IDX_BUTTON_X; + case KeyEvent.KEYCODE_BUTTON_Y: + return IDX_BUTTON_Y; + case KeyEvent.KEYCODE_BUTTON_L1: + return IDX_BUTTON_L1; + case KeyEvent.KEYCODE_BUTTON_R1: + return IDX_BUTTON_R1; + case KeyEvent.KEYCODE_BUTTON_SELECT: + return IDX_BUTTON_SELECT; + case KeyEvent.KEYCODE_BUTTON_START: + return IDX_BUTTON_START; + case KeyEvent.KEYCODE_BUTTON_THUMBL: + return IDX_BUTTON_L3; + case KeyEvent.KEYCODE_BUTTON_THUMBR: + return IDX_BUTTON_R3; + case KeyEvent.KEYCODE_BUTTON_L2: + return IDX_BUTTON_L2; + case KeyEvent.KEYCODE_BUTTON_R2: + return IDX_BUTTON_R2; + default: + return -1; + } + } + + public static int getButtonIdxByName(String name) { + switch (name) { + case "A": + return IDX_BUTTON_A; + case "B": + return IDX_BUTTON_B; + case "X": + return IDX_BUTTON_X; + case "Y": + return IDX_BUTTON_Y; + case "L1": + return IDX_BUTTON_L1; + case "R1": + return IDX_BUTTON_R1; + case "SELECT": + return IDX_BUTTON_SELECT; + case "START": + return IDX_BUTTON_START; + case "L3": + return IDX_BUTTON_L3; + case "R3": + return IDX_BUTTON_R3; + case "L2": + return IDX_BUTTON_L2; + case "R2": + return IDX_BUTTON_R2; + default: + return -1; + } } } diff --git a/app/src/main/java/com/winlator/inputcontrols/ExternalControllerBinding.java b/app/src/main/java/com/winlator/inputcontrols/ExternalControllerBinding.java index d111ef3..8e46fe1 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ExternalControllerBinding.java +++ b/app/src/main/java/com/winlator/inputcontrols/ExternalControllerBinding.java @@ -17,8 +17,6 @@ public class ExternalControllerBinding { public static final byte AXIS_Z_POSITIVE = -6; public static final byte AXIS_RZ_NEGATIVE = -7; public static final byte AXIS_RZ_POSITIVE = -8; - public static final byte AXIS_LTRIGGER = -9; - public static final byte AXIS_RTRIGGER = -10; private short keyCode; private Binding binding = Binding.NONE; @@ -70,10 +68,6 @@ public class ExternalControllerBinding { return "AXIS RZ-"; case AXIS_RZ_POSITIVE: return "AXIS RZ+"; - case AXIS_LTRIGGER: - return "L TRIGGER"; - case AXIS_RTRIGGER: - return "R TRIGGER"; default: return KeyEvent.keyCodeToString(keyCode).replace("KEYCODE_", "").replace("_", " "); } diff --git a/app/src/main/java/com/winlator/inputcontrols/GamepadState.java b/app/src/main/java/com/winlator/inputcontrols/GamepadState.java new file mode 100644 index 0000000..057bc85 --- /dev/null +++ b/app/src/main/java/com/winlator/inputcontrols/GamepadState.java @@ -0,0 +1,61 @@ +package com.winlator.inputcontrols; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class GamepadState { + public float thumbLX = 0; + public float thumbLY = 0; + public float thumbRX = 0; + public float thumbRY = 0; + public final boolean[] dpad = new boolean[4]; + public short buttons = 0; + + public byte getPovHat() { + byte povHat = -1; + if (dpad[0] && dpad[1]) povHat = 1; + else if (dpad[1] && dpad[2]) povHat = 3; + else if (dpad[2] && dpad[3]) povHat = 5; + else if (dpad[3] && dpad[0]) povHat = 7; + else if (dpad[0]) povHat = 0; + else if (dpad[1]) povHat = 2; + else if (dpad[2]) povHat = 4; + else if (dpad[3]) povHat = 6; + return povHat; + } + + public void writeTo(ByteBuffer buffer) { + buffer.putShort(buttons); + buffer.put(getPovHat()); + buffer.putShort((short)(thumbLX * Short.MAX_VALUE)); + buffer.putShort((short)(thumbLY * Short.MAX_VALUE)); + buffer.putShort((short)(thumbRX * Short.MAX_VALUE)); + buffer.putShort((short)(thumbRY * Short.MAX_VALUE)); + } + + public byte[] toByteArray() { + ByteBuffer buffer = ByteBuffer.allocate(11).order(ByteOrder.LITTLE_ENDIAN); + writeTo(buffer); + return buffer.array(); + } + + public void setPressed(int buttonIdx, boolean pressed) { + int flag = 1< JOYSTICK_DEAD_ZONE) { - controllerBinding = controller.getControllerBinding(ExternalControllerBinding.getKeyCodeForAxis(axis, Mathf.sign(value))); - if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), true, value); + final float[] values = {controller.state.thumbLX, controller.state.thumbLY, controller.state.thumbRX, controller.state.thumbRY, controller.state.getDPadX(), controller.state.getDPadY()}; + + for (byte i = 0; i < axes.length; i++) { + if (Math.abs(values[i]) > ControlElement.STICK_DEAD_ZONE) { + controllerBinding = controller.getControllerBinding(ExternalControllerBinding.getKeyCodeForAxis(axes[i], Mathf.sign(values[i]))); + if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), true, values[i]); } else { - controllerBinding = controller.getControllerBinding(ExternalControllerBinding.getKeyCodeForAxis(axis, (byte) 1)); - if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), false, value); - controllerBinding = controller.getControllerBinding(ExternalControllerBinding.getKeyCodeForAxis(axis, (byte)-1)); - if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), false, value); + controllerBinding = controller.getControllerBinding(ExternalControllerBinding.getKeyCodeForAxis(axes[i], (byte) 1)); + if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), false, values[i]); + controllerBinding = controller.getControllerBinding(ExternalControllerBinding.getKeyCodeForAxis(axes[i], (byte)-1)); + if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), false, values[i]); } } } - private void processTriggerButton(MotionEvent event, ExternalController controller) { - ExternalControllerBinding controllerBinding; - controllerBinding = controller.getControllerBinding(ExternalControllerBinding.AXIS_LTRIGGER); - if (controllerBinding != null) { - boolean pressed = event.getAxisValue(MotionEvent.AXIS_LTRIGGER) == 1 || event.getAxisValue(MotionEvent.AXIS_BRAKE) == 1; - handleInputEvent(controllerBinding.getBinding(), pressed); - } - - controllerBinding = controller.getControllerBinding(ExternalControllerBinding.AXIS_RTRIGGER); - if (controllerBinding != null) { - boolean pressed = event.getAxisValue(MotionEvent.AXIS_RTRIGGER) == 1 || event.getAxisValue(MotionEvent.AXIS_GAS) == 1; - handleInputEvent(controllerBinding.getBinding(), pressed); - } - } - @Override public boolean onGenericMotionEvent(MotionEvent event) { if (!editMode && profile != null) { ExternalController controller = profile.getController(event.getDeviceId()); - if (controller != null) { - if (ExternalController.isDPadDevice(event)) { - processTriggerButton(event, controller); - processJoystickInput(event, controller, -1); - return true; - } - else if (ExternalController.isJoystickDevice(event)) { - int historySize = event.getHistorySize(); - for (int i = 0; i < historySize; i++) processJoystickInput(event, controller, i); - processJoystickInput(event, controller, -1); - return true; - } + if (controller != null && controller.updateStateFromMotionEvent(event)) { + ExternalControllerBinding controllerBinding; + controllerBinding = controller.getControllerBinding(KeyEvent.KEYCODE_BUTTON_L2); + if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), controller.state.isPressed(ExternalController.IDX_BUTTON_L2)); + + controllerBinding = controller.getControllerBinding(KeyEvent.KEYCODE_BUTTON_R2); + if (controllerBinding != null) handleInputEvent(controllerBinding.getBinding(), controller.state.isPressed(ExternalController.IDX_BUTTON_R2)); + + processJoystickInput(controller); + return true; } } - return super.onGenericMotionEvent(event); } @@ -395,9 +381,6 @@ public class InputControlsView extends View { } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: - for (ControlElement element : profile.getElements()) if (element.handleTouchUp(pointerId)) handled = true; - if (!handled) touchpadView.onTouchEvent(event); - break; case MotionEvent.ACTION_CANCEL: for (ControlElement element : profile.getElements()) if (element.handleTouchUp(pointerId)) handled = true; if (!handled) touchpadView.onTouchEvent(event); @@ -433,36 +416,54 @@ public class InputControlsView extends View { } public void handleInputEvent(Binding binding, boolean isActionDown, float offset) { - if (isActionDown) { - if (binding == Binding.MOUSE_MOVE_LEFT || binding == Binding.MOUSE_MOVE_RIGHT) { - mouseMoveOffset.x = offset != 0 ? offset : (binding == Binding.MOUSE_MOVE_LEFT ? -1 : 1); - createMouseMoveTimer(); + if (binding.isGamepad()) { + GamepadState state = profile.getGamepadState(); + + int buttonIdx = binding.ordinal() - Binding.GAMEPAD_BUTTON_A.ordinal(); + if (buttonIdx <= 11) { + state.setPressed(buttonIdx, isActionDown); + if (xServer != null) xServer.getWinHandler().saveGamepadState(state); } - else if (binding == Binding.MOUSE_MOVE_DOWN || binding == Binding.MOUSE_MOVE_UP) { - mouseMoveOffset.y = offset != 0 ? offset : (binding == Binding.MOUSE_MOVE_UP ? -1 : 1); - createMouseMoveTimer(); + else if (binding == Binding.GAMEPAD_LEFT_THUMB_UP || binding == Binding.GAMEPAD_LEFT_THUMB_DOWN) { + state.thumbLY = isActionDown ? offset : 0; } - else { - Pointer.Button pointerButton = binding.getPointerButton(); - if (pointerButton != null) { - xServer.injectPointerButtonPress(pointerButton); - } - else xServer.injectKeyPress(binding.keycode); + else if (binding == Binding.GAMEPAD_LEFT_THUMB_LEFT || binding == Binding.GAMEPAD_LEFT_THUMB_RIGHT) { + state.thumbLX = isActionDown ? offset : 0; + } + else if (binding == Binding.GAMEPAD_RIGHT_THUMB_UP || binding == Binding.GAMEPAD_RIGHT_THUMB_DOWN) { + state.thumbRY = isActionDown ? offset : 0; + } + else if (binding == Binding.GAMEPAD_RIGHT_THUMB_LEFT || binding == Binding.GAMEPAD_RIGHT_THUMB_RIGHT) { + state.thumbRX = isActionDown ? offset : 0; + } + else if (binding == Binding.GAMEPAD_DPAD_UP || binding == Binding.GAMEPAD_DPAD_RIGHT || + binding == Binding.GAMEPAD_DPAD_DOWN || binding == Binding.GAMEPAD_DPAD_LEFT) { + state.dpad[binding.ordinal() - Binding.GAMEPAD_DPAD_UP.ordinal()] = isActionDown; } } else { if (binding == Binding.MOUSE_MOVE_LEFT || binding == Binding.MOUSE_MOVE_RIGHT) { - if (Math.abs(offset) < InputControlsView.JOYSTICK_DEAD_ZONE) mouseMoveOffset.x = 0; + mouseMoveOffset.x = isActionDown ? offset : 0; + createMouseMoveTimer(); } - else if (binding == Binding.MOUSE_MOVE_UP || binding == Binding.MOUSE_MOVE_DOWN) { - if (Math.abs(offset) < InputControlsView.JOYSTICK_DEAD_ZONE) mouseMoveOffset.y = 0; + else if (binding == Binding.MOUSE_MOVE_DOWN || binding == Binding.MOUSE_MOVE_UP) { + mouseMoveOffset.y = isActionDown ? offset : 0; + createMouseMoveTimer(); } else { Pointer.Button pointerButton = binding.getPointerButton(); - if (pointerButton != null) { - xServer.injectPointerButtonRelease(pointerButton); + if (isActionDown) { + if (pointerButton != null) { + xServer.injectPointerButtonPress(pointerButton); + } + else xServer.injectKeyPress(binding.keycode); + } + else { + if (pointerButton != null) { + xServer.injectPointerButtonRelease(pointerButton); + } + else xServer.injectKeyRelease(binding.keycode); } - else xServer.injectKeyRelease(binding.keycode); } } } diff --git a/app/src/main/java/com/winlator/widget/NumberPicker.java b/app/src/main/java/com/winlator/widget/NumberPicker.java new file mode 100644 index 0000000..b4067d6 --- /dev/null +++ b/app/src/main/java/com/winlator/widget/NumberPicker.java @@ -0,0 +1,127 @@ +package com.winlator.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.EditText; +import android.widget.FrameLayout; + +import com.winlator.R; +import com.winlator.math.Mathf; + +public class NumberPicker extends FrameLayout implements View.OnTouchListener { + private int value = 0; + private int minValue = 0; + private int maxValue = 100; + private int step = 1; + private final EditText editText; + private OnValueChangeListener onValueChangeListener; + + public interface OnValueChangeListener { + void onValueChange(NumberPicker numberPicker, int value); + } + + public NumberPicker(Context context) { + this(context, null); + } + + public NumberPicker(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + LayoutInflater.from(context).inflate(R.layout.number_picker, this, true); + editText = findViewById(R.id.EditText); + findViewById(R.id.BTDecrement).setOnTouchListener(this); + findViewById(R.id.BTIncrement).setOnTouchListener(this); + + if (attrs != null) { + TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NumberPicker); + minValue = ta.getInt(R.styleable.NumberPicker_minValue, minValue); + maxValue = ta.getInt(R.styleable.NumberPicker_maxValue, maxValue); + setStep(ta.getInt(R.styleable.NumberPicker_step, step)); + int value = ta.getInt(R.styleable.NumberPicker_value, 0); + ta.recycle(); + setValue(value); + } + else setStep(step); + } + + public void setValue(int value) { + this.value = Mathf.clamp(value, minValue, maxValue); + editText.setText(String.valueOf(this.value)); + } + + public int getValue() { + return value; + } + + public int getMinValue() { + return minValue; + } + + public void setMinValue(int minValue) { + this.minValue = minValue; + } + + public int getMaxValue() { + return maxValue; + } + + public void setMaxValue(int maxValue) { + this.maxValue = maxValue; + } + + public void increment() { + setValue(value+step); + } + + public void decrement() { + setValue(value-step); + } + + public int getStep() { + return step; + } + + public void setStep(int step) { + this.step = step; + } + + public OnValueChangeListener getOnValueChangeListener() { + return onValueChangeListener; + } + + public void setOnValueChangeListener(OnValueChangeListener onValueChangeListener) { + this.onValueChangeListener = onValueChangeListener; + } + + private void onButtonClick(View v) { + if (!isEnabled()) return; + + int id = v.getId(); + if (id == R.id.BTIncrement) { + increment(); + } + else if (id == R.id.BTDecrement) { + decrement(); + } + if (onValueChangeListener != null) { + onValueChangeListener.onValueChange(NumberPicker.this, value); + } + } + + @Override + public boolean onTouch(final View v, MotionEvent event) { + int action = event.getAction(); + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + onButtonClick(v); + } + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/winlator/winhandler/OnGetProcessInfoListener.java b/app/src/main/java/com/winlator/winhandler/OnGetProcessInfoListener.java new file mode 100644 index 0000000..f668f3f --- /dev/null +++ b/app/src/main/java/com/winlator/winhandler/OnGetProcessInfoListener.java @@ -0,0 +1,5 @@ +package com.winlator.winhandler; + +public interface OnGetProcessInfoListener { + void onGetProcessInfo(int index, int count, ProcessInfo processInfo); +} diff --git a/app/src/main/java/com/winlator/winhandler/RequestCodes.java b/app/src/main/java/com/winlator/winhandler/RequestCodes.java index 5dabb48..78ce604 100644 --- a/app/src/main/java/com/winlator/winhandler/RequestCodes.java +++ b/app/src/main/java/com/winlator/winhandler/RequestCodes.java @@ -2,9 +2,14 @@ package com.winlator.winhandler; abstract class RequestCodes { public static final byte EXIT = 0; - public static final byte EXEC = 1; - public static final byte KILL_PROCESS = 2; - public static final byte GET_PROCESSES = 3; - public static final byte SET_PROCESS_AFFINITY = 4; - public static final byte MOUSE_EVENT = 5; -} + public static final byte INIT = 1; + public static final byte EXEC = 2; + public static final byte KILL_PROCESS = 3; + public static final byte LIST_PROCESSES = 4; + public static final byte GET_PROCESS = 5; + public static final byte SET_PROCESS_AFFINITY = 6; + public static final byte MOUSE_EVENT = 7; + public static final byte GET_GAMEPAD = 8; + public static final byte GET_GAMEPAD_STATE = 9; + public static final byte RELEASE_GAMEPAD = 10; +} \ No newline at end of file diff --git a/app/src/main/java/com/winlator/winhandler/TaskManagerDialog.java b/app/src/main/java/com/winlator/winhandler/TaskManagerDialog.java index bc21432..2028e17 100644 --- a/app/src/main/java/com/winlator/winhandler/TaskManagerDialog.java +++ b/app/src/main/java/com/winlator/winhandler/TaskManagerDialog.java @@ -19,10 +19,11 @@ import com.winlator.contentdialog.ContentDialog; import java.util.Timer; import java.util.TimerTask; -public class TaskManagerDialog extends ContentDialog { +public class TaskManagerDialog extends ContentDialog implements OnGetProcessInfoListener { private final XServerDisplayActivity activity; private final LayoutInflater inflater; private Timer timer; + private final Object lock = new Object(); public TaskManagerDialog(XServerDisplayActivity activity) { super(activity, R.layout.task_manager_dialog); @@ -43,32 +44,17 @@ public class TaskManagerDialog extends ContentDialog { timer.cancel(); timer = null; } + + activity.getWinHandler().setOnGetProcessInfoListener(null); }); inflater = LayoutInflater.from(activity); } private void update() { - WinHandler winHandler = activity.getWinHandler(); - winHandler.getProcesses((processes) -> activity.runOnUiThread(() -> { - final LinearLayout container = findViewById(R.id.LLProcessList); - container.removeAllViews(); - setBottomBarText(activity.getString(R.string.processes)+": "+processes.size()); - - if (!processes.isEmpty()) { - findViewById(R.id.TVEmptyText).setVisibility(View.GONE); - - for (final ProcessInfo processInfo : processes) { - View itemView = inflater.inflate(R.layout.process_info_list_item, container, false); - ((TextView)itemView.findViewById(R.id.TVName)).setText(processInfo.name); - ((TextView)itemView.findViewById(R.id.TVPID)).setText(String.valueOf(processInfo.pid)); - ((TextView)itemView.findViewById(R.id.TVMemoryUsage)).setText(processInfo.getFormattedMemoryUsage()); - itemView.findViewById(R.id.BTMenu).setOnClickListener((v) -> showListItemMenu(v, processInfo)); - container.addView(itemView); - } - } - else findViewById(R.id.TVEmptyText).setVisibility(View.VISIBLE); - })); + synchronized (lock) { + activity.getWinHandler().listProcesses(); + } LinearLayout tableHead = findViewById(R.id.LLTableHead); TextView tvMemoryInfo = (TextView)tableHead.getChildAt(2); @@ -120,13 +106,45 @@ public class TaskManagerDialog extends ContentDialog { @Override public void show() { update(); + activity.getWinHandler().setOnGetProcessInfoListener(this); + timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { - update(); + activity.runOnUiThread(TaskManagerDialog.this::update); } }, 0, 1000); super.show(); } + + @Override + public void onGetProcessInfo(int index, int numProcesses, ProcessInfo processInfo) { + activity.runOnUiThread(() -> { + synchronized (lock) { + final LinearLayout container = findViewById(R.id.LLProcessList); + setBottomBarText(activity.getString(R.string.processes)+": " + numProcesses); + + if (numProcesses == 0) { + container.removeAllViews(); + findViewById(R.id.TVEmptyText).setVisibility(View.VISIBLE); + return; + } + + findViewById(R.id.TVEmptyText).setVisibility(View.GONE); + + int childCount = container.getChildCount(); + View itemView = index < childCount ? container.getChildAt(index) : inflater.inflate(R.layout.process_info_list_item, container, false); + ((TextView)itemView.findViewById(R.id.TVName)).setText(processInfo.name); + ((TextView)itemView.findViewById(R.id.TVPID)).setText(String.valueOf(processInfo.pid)); + ((TextView)itemView.findViewById(R.id.TVMemoryUsage)).setText(processInfo.getFormattedMemoryUsage()); + itemView.findViewById(R.id.BTMenu).setOnClickListener((v) -> showListItemMenu(v, processInfo)); + if (index >= childCount) container.addView(itemView); + + if (index == numProcesses-1 && childCount > numProcesses) { + for (int i = numProcesses; i < childCount; i++) container.removeViewAt(i); + } + } + }); + } } diff --git a/app/src/main/java/com/winlator/winhandler/WinHandler.java b/app/src/main/java/com/winlator/winhandler/WinHandler.java index db1c234..e9caf02 100644 --- a/app/src/main/java/com/winlator/winhandler/WinHandler.java +++ b/app/src/main/java/com/winlator/winhandler/WinHandler.java @@ -1,35 +1,55 @@ package com.winlator.winhandler; -import com.winlator.core.Callback; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import com.winlator.XServerDisplayActivity; import com.winlator.core.StringUtils; +import com.winlator.inputcontrols.ControlsProfile; +import com.winlator.inputcontrols.ExternalController; +import com.winlator.inputcontrols.GamepadState; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.DatagramSocket; import java.net.InetAddress; -import java.net.Socket; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.concurrent.Executors; public class WinHandler { - private static final short DEFAULT_PORT = 7946; - private Socket socket; - private final Object lock = new Object(); - private ByteBuffer data = ByteBuffer.allocate(128).order(ByteOrder.LITTLE_ENDIAN); + private static final short SERVER_PORT = 7947; + private static final short CLIENT_PORT = 7946; + public static final byte DINPUT_MAPPER_TYPE_STANDARD = 0; + public static final byte DINPUT_MAPPER_TYPE_XINPUT = 1; + private DatagramSocket socket; + private final ByteBuffer sendData = ByteBuffer.allocate(64).order(ByteOrder.LITTLE_ENDIAN); + private final ByteBuffer receiveData = ByteBuffer.allocate(64).order(ByteOrder.LITTLE_ENDIAN); + private final DatagramPacket sendPacket = new DatagramPacket(sendData.array(), 64); + private final DatagramPacket receivePacket = new DatagramPacket(receiveData.array(), 64); private final ArrayDeque actions = new ArrayDeque<>(); + private boolean initReceived = false; + private boolean running = false; + private OnGetProcessInfoListener onGetProcessInfoListener; + private ExternalController currentController; + private final ArrayDeque gamepadStateQueue = new ArrayDeque<>(); + private InetAddress localhost; + private byte dinputMapperType = DINPUT_MAPPER_TYPE_XINPUT; + private final XServerDisplayActivity activity; - private boolean sendData() { + public WinHandler(XServerDisplayActivity activity) { + this.activity = activity; + } + + private boolean sendPacket(int port) { try { - int size = data.position(); - if (size == 0) return true; - OutputStream outStream = socket.getOutputStream(); - outStream.write(data.array(), 0, size); + int size = sendData.position(); + if (size == 0) return false; + sendPacket.setAddress(localhost); + sendPacket.setPort(port); + socket.send(sendPacket); return true; } catch (IOException e) { @@ -37,158 +57,269 @@ public class WinHandler { } } - private boolean receiveData(int length) { - try { - while (length > data.capacity()) { - data = ByteBuffer.allocate(data.capacity() * 2).order(ByteOrder.LITTLE_ENDIAN); - } - - data.rewind(); - InputStream inStream = socket.getInputStream(); - int bytesRead = inStream.read(data.array(), 0, length); - if (bytesRead > 0) { - data.limit(length); - return true; - } - } - catch (IOException e) {} - return false; - } - public void exec(String command) { - command = command.trim(); - if (command.isEmpty()) return; - String[] cmdList = command.split(" ", 2); - final String filename = cmdList[0]; - final String parameters = cmdList.length > 1 ? cmdList[1] : ""; + synchronized (actions) { + command = command.trim(); + if (command.isEmpty()) return; + String[] cmdList = command.split(" ", 2); + final String filename = cmdList[0]; + final String parameters = cmdList.length > 1 ? cmdList[1] : ""; - synchronized (lock) { actions.add(() -> { byte[] filenameBytes = filename.getBytes(); byte[] parametersBytes = parameters.getBytes(); - data.rewind(); - data.put(RequestCodes.EXEC); - data.putInt(filenameBytes.length + parametersBytes.length + 8); - data.putInt(filenameBytes.length); - data.putInt(parametersBytes.length); - data.put(filenameBytes); - data.put(parametersBytes); - sendData(); + sendData.rewind(); + sendData.put(RequestCodes.EXEC); + sendData.putInt(filenameBytes.length + parametersBytes.length + 8); + sendData.putInt(filenameBytes.length); + sendData.putInt(parametersBytes.length); + sendData.put(filenameBytes); + sendData.put(parametersBytes); + sendPacket(CLIENT_PORT); }); } } public void killProcess(final String processName) { - synchronized (lock) { + synchronized (actions) { actions.add(() -> { - data.rewind(); - data.put(RequestCodes.KILL_PROCESS); + sendData.rewind(); + sendData.put(RequestCodes.KILL_PROCESS); byte[] bytes = processName.getBytes(); - data.putInt(bytes.length); - data.put(bytes); - sendData(); + sendData.putInt(bytes.length); + sendData.put(bytes); + sendPacket(CLIENT_PORT); }); } } - public void getProcesses(final Callback> callback) { - synchronized (lock) { + public void listProcesses() { + synchronized (actions) { actions.add(() -> { - data.rewind(); - data.put(RequestCodes.GET_PROCESSES); - data.putInt(0); + sendData.rewind(); + sendData.put(RequestCodes.LIST_PROCESSES); + sendData.putInt(0); - if (sendData() && receiveData(5)) { - boolean success = data.get() == 1; - int responseLength = data.getInt(); - if (success && receiveData(responseLength)) { - ArrayList processes = new ArrayList<>(); - while (data.position() < data.limit()) { - int numBytesBefore = data.position(); - int pid = data.getInt(); - long memoryUsage = data.getLong(); - int affinityMask = data.getInt(); - - byte[] bytes = new byte[32]; - data.get(bytes); - String name = StringUtils.fromANSIString(bytes); - - processes.add(new ProcessInfo(pid, name, memoryUsage, affinityMask)); - - int bytesRead = data.position() - numBytesBefore; - data.position(data.position() + (64 - bytesRead)); - } - callback.call(processes); - } - else callback.call(Collections.emptyList()); + if (!sendPacket(CLIENT_PORT) && onGetProcessInfoListener != null) { + onGetProcessInfoListener.onGetProcessInfo(0, 0, null); } - else callback.call(Collections.emptyList()); }); } } public void setProcessAffinity(final int pid, final int affinityMask) { - synchronized (lock) { + synchronized (actions) { actions.add(() -> { - data.rewind(); - data.put(RequestCodes.SET_PROCESS_AFFINITY); - data.putInt(8); - data.putInt(pid); - data.putInt(affinityMask); - sendData(); + sendData.rewind(); + sendData.put(RequestCodes.SET_PROCESS_AFFINITY); + sendData.putInt(8); + sendData.putInt(pid); + sendData.putInt(affinityMask); + sendPacket(CLIENT_PORT); }); } } public void mouseEvent(int flags, int dx, int dy, int wheelDelta) { - synchronized (lock) { - if (!isConnected()) return; + synchronized (actions) { + if (!initReceived) return; actions.add(() -> { - data.rewind(); - data.put(RequestCodes.MOUSE_EVENT); - data.putInt(10); - data.putInt(flags); - data.putShort((short)dx); - data.putShort((short)dy); - data.putShort((short)wheelDelta); - sendData(); + sendData.rewind(); + sendData.put(RequestCodes.MOUSE_EVENT); + sendData.putInt(10); + sendData.putInt(flags); + sendData.putShort((short)dx); + sendData.putShort((short)dy); + sendData.putShort((short)wheelDelta); + sendPacket(CLIENT_PORT); }); } } - public boolean isConnected() { - return socket != null && !socket.isClosed() && socket.isConnected(); + public OnGetProcessInfoListener getOnGetProcessInfoListener() { + return onGetProcessInfoListener; } - private void connect() { - InetAddress address = null; - try { - address = InetAddress.getLocalHost(); + public void setOnGetProcessInfoListener(OnGetProcessInfoListener onGetProcessInfoListener) { + synchronized (actions) { + this.onGetProcessInfoListener = onGetProcessInfoListener; } - catch (UnknownHostException e) {} - - try { - socket = new Socket(address.getHostAddress(), DEFAULT_PORT); - } - catch (IOException e) {} } - public void start() { + private void startSendThread() { Executors.newSingleThreadExecutor().execute(() -> { - while (true) { - synchronized (lock) { - if (!isConnected()) connect(); - if (isConnected()) while (!actions.isEmpty()) actions.poll().run(); - } - - if (!isConnected()) { - try { - Thread.sleep(100); - } - catch (InterruptedException e) {} + while (running) { + synchronized (actions) { + while (initReceived && !actions.isEmpty()) actions.poll().run(); } } }); } + + public void stop() { + running = false; + if (socket != null) { + socket.close(); + socket = null; + } + } + + private void handleRequest(byte requestCode, final int port) { + switch (requestCode) { + case RequestCodes.INIT: { + initReceived = true; + break; + } + case RequestCodes.GET_PROCESS: { + if (onGetProcessInfoListener == null) return; + receiveData.position(receiveData.position() + 4); + int numProcesses = receiveData.getShort(); + int index = receiveData.getShort(); + int pid = receiveData.getInt(); + long memoryUsage = receiveData.getLong(); + int affinityMask = receiveData.getInt(); + + byte[] bytes = new byte[32]; + receiveData.get(bytes); + String name = StringUtils.fromANSIString(bytes); + + onGetProcessInfoListener.onGetProcessInfo(index, numProcesses, new ProcessInfo(pid, name, memoryUsage, affinityMask)); + break; + } + case RequestCodes.GET_GAMEPAD: { + boolean isXInput = receiveData.get() == 1; + final ControlsProfile profile = activity.getInputControlsView().getProfile(); + boolean useVirtualGamepad = profile != null && profile.isVirtualGamepad(); + + if (!useVirtualGamepad && (currentController == null || !currentController.isConnected())) { + releaseCurrentController(); + currentController = ExternalController.getController(0); + } + + actions.add(() -> { + sendData.rewind(); + sendData.put(RequestCodes.GET_GAMEPAD); + + if (currentController != null || useVirtualGamepad) { + sendData.putInt(!useVirtualGamepad ? currentController.getDeviceId() : profile.id); + sendData.put(dinputMapperType); + byte[] bytes = (useVirtualGamepad ? profile.getName() : currentController.getName()).getBytes(); + sendData.putInt(bytes.length); + sendData.put(bytes); + } + else sendData.putInt(0); + + sendPacket(port); + }); + break; + } + case RequestCodes.GET_GAMEPAD_STATE: { + int gamepadId = receiveData.getInt(); + final ControlsProfile profile = activity.getInputControlsView().getProfile(); + boolean useVirtualGamepad = profile != null && profile.isVirtualGamepad(); + + if (currentController != null && currentController.getDeviceId() != gamepadId) currentController = null; + + actions.add(() -> { + sendData.rewind(); + sendData.put(RequestCodes.GET_GAMEPAD_STATE); + sendData.put((byte)(currentController != null || useVirtualGamepad ? 1 : 0)); + + if (currentController != null || useVirtualGamepad) { + sendData.putInt(gamepadId); + + synchronized (gamepadStateQueue) { + if (gamepadStateQueue.isEmpty()) { + if (useVirtualGamepad) { + profile.getGamepadState().writeTo(sendData); + } + else currentController.state.writeTo(sendData); + } + else sendData.put(gamepadStateQueue.poll()); + } + } + + sendPacket(port); + }); + break; + } + case RequestCodes.RELEASE_GAMEPAD: + releaseCurrentController(); + break; + } + } + + public void start() { + try { + localhost = InetAddress.getLocalHost(); + } + catch (UnknownHostException e) {} + + running = true; + startSendThread(); + Executors.newSingleThreadExecutor().execute(() -> { + try { + socket = new DatagramSocket(SERVER_PORT); + + while (running) { + socket.receive(receivePacket); + + synchronized (actions) { + receiveData.rewind(); + byte requestCode = receiveData.get(); + handleRequest(requestCode, receivePacket.getPort()); + } + } + } + catch (IOException e) {} + }); + } + + public void saveGamepadState(GamepadState state) { + synchronized (gamepadStateQueue) { + if (gamepadStateQueue.size() > 20) gamepadStateQueue.removeLast(); + gamepadStateQueue.add(state.toByteArray()); + } + } + + private void releaseCurrentController() { + currentController = null; + synchronized (gamepadStateQueue) { + gamepadStateQueue.clear(); + } + } + + public boolean onGenericMotionEvent(MotionEvent event) { + boolean handled = false; + if (currentController != null && currentController.getDeviceId() == event.getDeviceId()) { + handled = currentController.updateStateFromMotionEvent(event); + } + return handled; + } + + public boolean onKeyEvent(KeyEvent event) { + boolean handled = false; + if (currentController != null && currentController.getDeviceId() == event.getDeviceId() && event.getRepeatCount() == 0) { + int action = event.getAction(); + + if (action == KeyEvent.ACTION_DOWN) { + handled = currentController.updateStateFromKeyEvent(event); + } + else if (action == KeyEvent.ACTION_UP) { + handled = currentController.updateStateFromKeyEvent(event); + } + + if (handled) saveGamepadState(currentController.state); + } + return handled; + } + + public byte getDInputMapperType() { + return dinputMapperType; + } + + public void setDInputMapperType(byte dinputMapperType) { + this.dinputMapperType = dinputMapperType; + } } diff --git a/app/src/main/java/com/winlator/xenvironment/ImageFs.java b/app/src/main/java/com/winlator/xenvironment/ImageFs.java index 5626a2f..3163f28 100644 --- a/app/src/main/java/com/winlator/xenvironment/ImageFs.java +++ b/app/src/main/java/com/winlator/xenvironment/ImageFs.java @@ -75,6 +75,10 @@ public class ImageFs { return new File(rootDir, "/opt/installed-wine"); } + public File getTmpDir() { + return new File(rootDir, "/tmp"); + } + @NonNull @Override public String toString() { diff --git a/app/src/main/java/com/winlator/xenvironment/components/GuestProgramLauncherComponent.java b/app/src/main/java/com/winlator/xenvironment/components/GuestProgramLauncherComponent.java index f22e2f7..b42a646 100644 --- a/app/src/main/java/com/winlator/xenvironment/components/GuestProgramLauncherComponent.java +++ b/app/src/main/java/com/winlator/xenvironment/components/GuestProgramLauncherComponent.java @@ -127,11 +127,15 @@ public class GuestProgramLauncherComponent extends EnvironmentComponent { envVars.put("ANDROID_SYSVSHM_SERVER", UnixSocketConfig.SYSVSHM_SERVER_PATH); if (this.envVars != null) envVars.putAll(this.envVars); + File shmDir = new File(rootDir, "/tmp/shm"); + shmDir.mkdirs(); + String command = nativeLibraryDir+"/libproot.so"; command += " --kill-on-exit"; command += " --rootfs="+rootDir; command += " --cwd="+ImageFs.HOME_PATH; command += " --bind=/dev"; + command += " --bind="+shmDir.getAbsolutePath()+":/dev/shm"; command += " --bind=/proc"; command += " --bind=/sys"; diff --git a/app/src/main/java/com/winlator/xserver/Keyboard.java b/app/src/main/java/com/winlator/xserver/Keyboard.java index 5c0b2df..a021991 100644 --- a/app/src/main/java/com/winlator/xserver/Keyboard.java +++ b/app/src/main/java/com/winlator/xserver/Keyboard.java @@ -4,6 +4,8 @@ import android.view.KeyEvent; import androidx.collection.ArraySet; +import com.winlator.inputcontrols.ExternalController; + import java.util.ArrayList; public class Keyboard { @@ -92,6 +94,8 @@ public class Keyboard { } public boolean onKeyEvent(KeyEvent event) { + if (ExternalController.isGameController(event.getDevice())) return false; + int action = event.getAction(); if (action == KeyEvent.ACTION_DOWN || action == KeyEvent.ACTION_UP) { int keyCode = event.getKeyCode(); diff --git a/app/src/main/res/drawable-hdpi/number_picker_decrement.png b/app/src/main/res/drawable-hdpi/number_picker_decrement.png new file mode 100644 index 0000000..78f1e21 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/number_picker_decrement.png differ diff --git a/app/src/main/res/drawable-hdpi/number_picker_increment.png b/app/src/main/res/drawable-hdpi/number_picker_increment.png new file mode 100644 index 0000000..13bbe36 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/number_picker_increment.png differ diff --git a/app/src/main/res/drawable/number_picker_background.xml b/app/src/main/res/drawable/number_picker_background.xml new file mode 100644 index 0000000..bb69356 --- /dev/null +++ b/app/src/main/res/drawable/number_picker_background.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/control_element_settings.xml b/app/src/main/res/layout/control_element_settings.xml index 473caf7..d5e29d4 100644 --- a/app/src/main/res/layout/control_element_settings.xml +++ b/app/src/main/res/layout/control_element_settings.xml @@ -1,8 +1,8 @@ - + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + android:hint="@string/none" + android:inputType="textCapCharacters" /> - - + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml index 01a600d..5177801 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -204,11 +204,12 @@ android:layout_height="wrap_content" android:min="10" android:max="200" /> + diff --git a/app/src/main/res/layout/shortcut_settings_dialog.xml b/app/src/main/res/layout/shortcut_settings_dialog.xml index 85afa57..ff0adea 100644 --- a/app/src/main/res/layout/shortcut_settings_dialog.xml +++ b/app/src/main/res/layout/shortcut_settings_dialog.xml @@ -255,6 +255,17 @@ android:layout_marginTop="8dp"> + + + + - - Keyboard Mouse + Gamepad Original WineD3D @@ -49,11 +50,15 @@ 0.2.2 (old) - 0.2.5 (new) + 0.2.6 (new) 23.1.6 (a6xx) 23.3.0 (a7xx) 24.0.0 (a7xx) + + Standard (Old Gamepads) + XInput + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..d69ea4a --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e033e63..26443c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,7 +62,6 @@ Value Processor Affinity Cursor Speed - Cursor Visible Lock Cursor Toggle Switch Profile exported to @@ -144,7 +143,7 @@ Letter Target Path XServer - Use DRI3 extension (Experimental) + Use DRI3 extension Width Height old @@ -174,7 +173,6 @@ Performance (Unstable) Preset You cannot remove this preset - Use single CPU WineD3D is a translation layer for DirectX 1-11 to OpenGL.

DXVK is a translation layer for Direct3D 9-11 to Vulkan.

@@ -207,4 +205,6 @@ Defines the max allowed forward value when building Block Optimization of CALL/RET opcodes Defines whether or not Dynarec will wait for the FillBlock to be ready + Columns + DirectInput Mapper Type \ No newline at end of file