Initial swkbd implementation (#826)

* am: Initial swkbd implementation

Currently only implements the full screen keyboard, inline keyboard will come later.

* Remove unnecessary logging

* Miscellaneous tidy up

* am: Always pop incoming interactive session data

* am: Add a reminder to implement the full config struct

* am: Check for a max length of zero

We should only limit/truncate text when the max length is set to a non-zero value.

* Add documentation

* am: Return IStorage not available when queue is empty

We should be returning the appropriate error code when the FIFO is empty, rather than just throwing an exception and killing the emulator.

* Fix typo

* Code style changes
This commit is contained in:
jduncanator 2019-11-18 22:16:26 +11:00 committed by Ac_K
parent 79abc6ed93
commit ee81ab547e
14 changed files with 522 additions and 38 deletions

View file

@ -0,0 +1,179 @@
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace Ryujinx.HLE.HOS.Applets
{
internal class SoftwareKeyboardApplet : IApplet
{
private const string DEFAULT_NUMB = "1";
private const string DEFAULT_TEXT = "Ryujinx";
private const int STANDARD_BUFFER_SIZE = 0x7D8;
private const int INTERACTIVE_BUFFER_SIZE = 0x7D4;
private SoftwareKeyboardState _state = SoftwareKeyboardState.Uninitialized;
private AppletSession _normalSession;
private AppletSession _interactiveSession;
private SoftwareKeyboardConfig _keyboardConfig;
private string _textValue = DEFAULT_TEXT;
public event EventHandler AppletStateChanged;
public SoftwareKeyboardApplet(Horizon system) { }
public ResultCode Start(AppletSession normalSession,
AppletSession interactiveSession)
{
_normalSession = normalSession;
_interactiveSession = interactiveSession;
_interactiveSession.DataAvailable += OnInteractiveData;
var launchParams = _normalSession.Pop();
var keyboardConfig = _normalSession.Pop();
var transferMemory = _normalSession.Pop();
_keyboardConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
_state = SoftwareKeyboardState.Ready;
Execute();
return ResultCode.Success;
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
private void Execute()
{
// If the keyboard type is numbers only, we swap to a default
// text that only contains numbers.
if (_keyboardConfig.Type == SoftwareKeyboardType.NumbersOnly)
{
_textValue = DEFAULT_NUMB;
}
// If the max string length is 0, we set it to a large default
// length.
if (_keyboardConfig.StringLengthMax == 0)
{
_keyboardConfig.StringLengthMax = 100;
}
// If our default text is longer than the allowed length,
// we truncate it.
if (_textValue.Length > _keyboardConfig.StringLengthMax)
{
_textValue = _textValue.Substring(0, (int)_keyboardConfig.StringLengthMax);
}
if (!_keyboardConfig.CheckText)
{
// If the application doesn't need to validate the response,
// we push the data to the non-interactive output buffer
// and poll it for completion.
_state = SoftwareKeyboardState.Complete;
_normalSession.Push(BuildResponse(_textValue, false));
AppletStateChanged?.Invoke(this, null);
}
else
{
// The application needs to validate the response, so we
// submit it to the interactive output buffer, and poll it
// for validation. Once validated, the application will submit
// back a validation status, which is handled in OnInteractiveDataPushIn.
_state = SoftwareKeyboardState.ValidationPending;
_interactiveSession.Push(BuildResponse(_textValue, true));
}
}
private void OnInteractiveData(object sender, EventArgs e)
{
// Obtain the validation status response,
var data = _interactiveSession.Pop();
if (_state == SoftwareKeyboardState.ValidationPending)
{
// TODO(jduncantor):
// If application rejects our "attempt", submit another attempt,
// and put the applet back in PendingValidation state.
// For now we assume success, so we push the final result
// to the standard output buffer and carry on our merry way.
_normalSession.Push(BuildResponse(_textValue, false));
AppletStateChanged?.Invoke(this, null);
_state = SoftwareKeyboardState.Complete;
}
else if(_state == SoftwareKeyboardState.Complete)
{
// If we have already completed, we push the result text
// back on the output buffer and poll the application.
_normalSession.Push(BuildResponse(_textValue, false));
AppletStateChanged?.Invoke(this, null);
}
else
{
// We shouldn't be able to get here through standard swkbd execution.
throw new InvalidOperationException("Software Keyboard is in an invalid state.");
}
}
private byte[] BuildResponse(string text, bool interactive)
{
int bufferSize = !interactive ? STANDARD_BUFFER_SIZE : INTERACTIVE_BUFFER_SIZE;
using (MemoryStream stream = new MemoryStream(new byte[bufferSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
byte[] output = Encoding.Unicode.GetBytes(text);
if (!interactive)
{
// Result Code
writer.Write((uint)0);
}
else
{
// In interactive mode, we write the length of the text
// as a long, rather than a result code.
writer.Write((long)output.Length);
}
writer.Write(output);
return stream.ToArray();
}
}
private static T ReadStruct<T>(byte[] data)
where T : struct
{
GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
try
{
return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
}
finally
{
handle.Free();
}
}
}
}

View file

@ -0,0 +1,33 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
// TODO(jduncanator): Define all fields
[StructLayout(LayoutKind.Explicit)]
struct SoftwareKeyboardConfig
{
/// <summary>
/// Type of keyboard.
/// </summary>
[FieldOffset(0x0)]
public SoftwareKeyboardType Type;
/// <summary>
/// When non-zero, specifies the max string length. When the input is too long, swkbd will stop accepting more input until text is deleted via the B button (Backspace).
/// </summary>
[FieldOffset(0x3AC)]
public uint StringLengthMax;
/// <summary>
/// When non-zero, specifies the max string length. When the input is too long, swkbd will display an icon and disable the ok-button.
/// </summary>
[FieldOffset(0x3B0)]
public uint StringLengthMaxExtended;
/// <summary>
/// When set, the application will validate the entered text whilst the swkbd is still on screen.
/// </summary>
[FieldOffset(0x3D0), MarshalAs(UnmanagedType.I1)]
public bool CheckText;
}
}

View file

@ -0,0 +1,25 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
internal enum SoftwareKeyboardState
{
/// <summary>
/// swkbd is uninitialized.
/// </summary>
Uninitialized,
/// <summary>
/// swkbd is ready to process data.
/// </summary>
Ready,
/// <summary>
/// swkbd is awaiting an interactive reply with a validation status.
/// </summary>
ValidationPending,
/// <summary>
/// swkbd has completed.
/// </summary>
Complete
}
}

View file

@ -0,0 +1,20 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
internal enum SoftwareKeyboardType : uint
{
/// <summary>
/// Normal keyboard.
/// </summary>
Default = 0,
/// <summary>
/// Number pad. The buttons at the bottom left/right are only available when they're set in the config by leftButtonText / rightButtonText.
/// </summary>
NumbersOnly = 1,
/// <summary>
/// QWERTY (and variants) keyboard only.
/// </summary>
LettersOnly = 2
}
}