private void ChangedString(string text, InlineKeyboardState state) { if (_encoding == Encoding.UTF8) { if (_useChangedStringV2) { _interactiveSession.Push(InlineResponses.ChangedStringUtf8V2(text, state)); } else { _interactiveSession.Push(InlineResponses.ChangedStringUtf8(text, state)); } } else { if (_useChangedStringV2) { _interactiveSession.Push(InlineResponses.ChangedStringV2(text, state)); } else { _interactiveSession.Push(InlineResponses.ChangedString(text, state)); } } }
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) { _normalSession = normalSession; _interactiveSession = interactiveSession; _interactiveSession.DataAvailable += OnInteractiveData; _alreadyShown = false; _useChangedStringV2 = false; var launchParams = _normalSession.Pop(); var keyboardConfig = _normalSession.Pop(); if (keyboardConfig.Length == Marshal.SizeOf <SoftwareKeyboardInitialize>()) { // Initialize the keyboard applet in background mode. _isBackground = true; _keyboardBackgroundInitialize = ReadStruct <SoftwareKeyboardInitialize>(keyboardConfig); _backgroundState = InlineKeyboardState.Uninitialized; return(ResultCode.Success); } else { // Initialize the keyboard applet in foreground mode. _isBackground = false; if (keyboardConfig.Length < Marshal.SizeOf <SoftwareKeyboardConfig>()) { Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf<SoftwareKeyboardConfig>():x}. Got {keyboardConfig.Length:x}"); } else { _keyboardForegroundConfig = ReadStruct <SoftwareKeyboardConfig>(keyboardConfig); } if (!_normalSession.TryPop(out _transferMemory)) { Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null"); } if (_keyboardForegroundConfig.UseUtf8) { _encoding = Encoding.UTF8; } _foregroundState = SoftwareKeyboardState.Ready; ExecuteForegroundKeyboard(); return(ResultCode.Success); } }
private void PushUpdatedState(string text, int cursorBegin, KeyboardResult result) { _lastResult = result; _textValue = text; bool cancel = result == KeyboardResult.Cancel; bool accept = result == KeyboardResult.Accept; if (!IsKeyboardActive()) { // Keyboard is not active. return; } if (accept == false && cancel == false) { Logger.Debug?.Print(LogClass.ServiceAm, $"Updating keyboard text to {text} and cursor position to {cursorBegin}"); PushChangedString(text, (uint)cursorBegin, _backgroundState); } else { // Disable the frontend. DeactivateFrontend(); // The 'Complete' state indicates the Calc request has been fulfilled by the applet. _backgroundState = InlineKeyboardState.Disappearing; if (accept) { Logger.Debug?.Print(LogClass.ServiceAm, $"Sending keyboard OK with text {text}"); DecidedEnter(text, _backgroundState); } else if (cancel) { Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel"); DecidedCancel(_backgroundState); } _interactiveSession.Push(InlineResponses.Default(_backgroundState)); Logger.Debug?.Print(LogClass.ServiceAm, $"Resetting state of the keyboard to {_backgroundState}"); // Set the state of the applet to 'Initialized' as it is the only known state so far // that does not soft-lock the keyboard after use. _backgroundState = InlineKeyboardState.Initialized; _interactiveSession.Push(InlineResponses.Default(_backgroundState)); } }
private void DecidedEnter(string text, InlineKeyboardState state) { if (_encoding == Encoding.UTF8) { _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(text, state)); } else { _interactiveSession.Push(InlineResponses.DecidedEnter(text, state)); } }
public static byte[] Default(InlineKeyboardState state) { uint resSize = 2 * sizeof(uint); using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { BeginResponse(state, InlineKeyboardResponse.Default, writer); return(stream.ToArray()); } }
public static byte[] UnsetCustomizedDictionaries(InlineKeyboardState state) { uint resSize = 2 * sizeof(uint); using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { BeginResponse(state, InlineKeyboardResponse.UnsetCustomizedDictionaries, writer); return(stream.ToArray()); } }
public static byte[] DecidedEnterUtf8(string text, InlineKeyboardState state) { uint resSize = 3 * sizeof(uint) + MaxStrLenUTF8; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { BeginResponse(state, InlineKeyboardResponse.DecidedEnterUtf8, writer); WriteString(text, writer, MaxStrLenUTF8, Encoding.UTF8); return(stream.ToArray()); } }
public static byte[] FinishedInitialize(InlineKeyboardState state) { uint resSize = 2 * sizeof(uint) + 0x1; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { BeginResponse(state, InlineKeyboardResponse.FinishedInitialize, writer); writer.Write((byte)1); // Data (ignored by the program) return(stream.ToArray()); } }
public static byte[] MovedCursor(string text, uint cursor, InlineKeyboardState state) { uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { BeginResponse(state, InlineKeyboardResponse.MovedCursor, writer); WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode); return(stream.ToArray()); } }
public static byte[] ChangedStringUtf8(string text, uint cursor, InlineKeyboardState state) { uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8, writer); WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF8, Encoding.UTF8, true); return(stream.ToArray()); } }
public static byte[] MovedCursorUtf8V2(string text, uint cursor, InlineKeyboardState state) { uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8 + 0x1; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8V2, writer); WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF8, Encoding.UTF8); writer.Write((byte)0); // Flag == 0 return(stream.ToArray()); } }
private void PushChangedString(string text, uint cursor, InlineKeyboardState state) { // TODO (Caian): The *V2 methods are not supported because the applications that request // them do not seem to accept them. The regular methods seem to work just fine in all cases. if (_encoding == Encoding.UTF8) { _interactiveSession.Push(InlineResponses.ChangedStringUtf8(text, cursor, state)); } else { _interactiveSession.Push(InlineResponses.ChangedString(text, cursor, state)); } }
public static byte[] ChangedString(string text, InlineKeyboardState state) { uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { BeginResponse(state, InlineKeyboardResponse.ChangedString, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); writer.Write((int)0); // ? writer.Write((int)0); // ? return(stream.ToArray()); } }
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) { _normalSession = normalSession; _interactiveSession = interactiveSession; _interactiveSession.DataAvailable += OnInteractiveData; var launchParams = _normalSession.Pop(); var keyboardConfig = _normalSession.Pop(); if (keyboardConfig.Length == Marshal.SizeOf <SoftwareKeyboardInitialize>()) { // Initialize the keyboard applet in background mode. _isBackground = true; _keyboardBackgroundInitialize = ReadStruct <SoftwareKeyboardInitialize>(keyboardConfig); InlineKeyboardState state = InlineKeyboardState.Uninitialized; SetInlineState(state); string acceptKeyName; string cancelKeyName; if (_device.UiHandler != null) { _dynamicTextInputHandler = _device.UiHandler.CreateDynamicTextInputHandler(); _dynamicTextInputHandler.TextChanged += DynamicTextChanged; acceptKeyName = _dynamicTextInputHandler.AcceptKeyName; cancelKeyName = _dynamicTextInputHandler.CancelKeyName; } else { Logger.Error?.Print(LogClass.ServiceAm, "GUI Handler is not set, software keyboard applet will not work properly"); acceptKeyName = ""; cancelKeyName = ""; } _keyboardRenderer = new SoftwareKeyboardRenderer(acceptKeyName, cancelKeyName); _interactiveSession.Push(InlineResponses.FinishedInitialize(state)); return(ResultCode.Success); } else { // Initialize the keyboard applet in foreground mode. _isBackground = false; if (keyboardConfig.Length < Marshal.SizeOf <SoftwareKeyboardConfig>()) { Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf<SoftwareKeyboardConfig>():x}. Got {keyboardConfig.Length:x}"); } else { _keyboardForegroundConfig = ReadStruct <SoftwareKeyboardConfig>(keyboardConfig); } if (!_normalSession.TryPop(out _transferMemory)) { Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null"); } if (_keyboardForegroundConfig.UseUtf8) { _encoding = Encoding.UTF8; } _foregroundState = SoftwareKeyboardState.Ready; ExecuteForegroundKeyboard(); return(ResultCode.Success); } }
private void DecidedCancel(InlineKeyboardState state) { _interactiveSession.Push(InlineResponses.DecidedCancel(state)); }
private void OnBackgroundInteractiveData(byte[] data) { // WARNING: Only invoke applet state changes after an explicit finalization // request from the game, this is because the inline keyboard is expected to // keep running in the background sending data by itself. using (MemoryStream stream = new MemoryStream(data)) using (BinaryReader reader = new BinaryReader(stream)) { InlineKeyboardRequest request = (InlineKeyboardRequest)reader.ReadUInt32(); InlineKeyboardState state = GetInlineState(); long remaining; Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {state}"); switch (request) { case InlineKeyboardRequest.UseChangedStringV2: _useChangedStringV2 = true; break; case InlineKeyboardRequest.UseMovedCursorV2: // Not used because we only reply with the final string. break; case InlineKeyboardRequest.SetUserWordInfo: // Read the user word info data. remaining = stream.Length - stream.Position; if (remaining < sizeof(int)) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes"); } else { int wordsCount = reader.ReadInt32(); int wordSize = Marshal.SizeOf <SoftwareKeyboardUserWord>(); remaining = stream.Length - stream.Position; if (wordsCount > MaxUserWords) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}"); } else if (wordsCount * wordSize != remaining) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words"); } else { _keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount]; for (int word = 0; word < wordsCount; word++) { byte[] wordData = reader.ReadBytes(wordSize); _keyboardBackgroundUserWords[word] = ReadStruct <SoftwareKeyboardUserWord>(wordData); } } } _interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(state)); break; case InlineKeyboardRequest.SetCustomizeDic: // Read the custom dic data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf <SoftwareKeyboardCustomizeDic>()) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes"); } else { var keyboardDicData = reader.ReadBytes((int)remaining); _keyboardBackgroundDic = ReadStruct <SoftwareKeyboardCustomizeDic>(keyboardDicData); } _interactiveSession.Push(InlineResponses.UnsetCustomizeDic(state)); break; case InlineKeyboardRequest.SetCustomizedDictionaries: // Read the custom dictionaries data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf <SoftwareKeyboardDictSet>()) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes"); } else { var keyboardDictData = reader.ReadBytes((int)remaining); _keyboardBackgroundDictSet = ReadStruct <SoftwareKeyboardDictSet>(keyboardDictData); } _interactiveSession.Push(InlineResponses.UnsetCustomizedDictionaries(state)); break; case InlineKeyboardRequest.Calc: // The Calc request tells the Applet to enter the main input handling loop, which will end // with either a text being submitted or a cancel request from the user. // NOTE: Some Calc requests happen early in the application and are not meant to be shown. This possibly // happens because the game has complete control over when the inline keyboard is drawn, but here it // would cause a dialog to pop in the emulator, which is inconvenient. An algorithm is applied to // decide whether it is a dummy Calc or not, but regardless of the result, the dummy Calc appears to // never happen twice, so the keyboard will always show if it has already been shown before. bool shouldShowKeyboard = _alreadyShown; _alreadyShown = true; // Read the Calc data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf <SoftwareKeyboardCalc>()) { Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes"); } else { var keyboardCalcData = reader.ReadBytes((int)remaining); _keyboardBackgroundCalc = ReadStruct <SoftwareKeyboardCalc>(keyboardCalcData); // Check if the application expects UTF8 encoding instead of UTF16. if (_keyboardBackgroundCalc.UseUtf8) { _encoding = Encoding.UTF8; } // Force showing the keyboard regardless of the state, an unwanted // input dialog may show, but it is better than a soft lock. if (_keyboardBackgroundCalc.Appear.ShouldBeHidden == 0) { shouldShowKeyboard = true; } } // Send an initialization finished signal. state = InlineKeyboardState.Ready; SetInlineState(state); _interactiveSession.Push(InlineResponses.FinishedInitialize(state)); // Start a task with the GUI handler to get user's input. new Task(() => { GetInputTextAndSend(shouldShowKeyboard, state); }).Start(); break; case InlineKeyboardRequest.Finalize: // The calling application wants to close the keyboard applet and will wait for a state change. _backgroundState = InlineKeyboardState.Uninitialized; AppletStateChanged?.Invoke(this, null); break; default: // We shouldn't be able to get here through standard swkbd execution. Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_backgroundState}"); _interactiveSession.Push(InlineResponses.Default(state)); break; } } }
private void DynamicTextChanged(string text, int cursorBegin, int cursorEnd, bool isAccept, bool isCancel, bool force) { // Launch as a task to avoid blocking the UI Task.Run(() => { if (force) { Logger.Warning?.Print(LogClass.ServiceAm, "Forcing keyboard out of soft-lock..."); // Repeat the response sequence from a Calc to try to exit a soft-lock. text = DefaultText; PushChangedString(text, 0, InlineKeyboardState.Ready); _interactiveSession.Push(InlineResponses.Default(InlineKeyboardState.Ready)); Thread.Sleep(SoftUnlockerDelayMilliseconds); } InlineKeyboardState state = GetInlineState(); if (!force && (state < InlineKeyboardState.Ready || state == InlineKeyboardState.Complete)) { return; } if (isAccept == false && isCancel == false) { Logger.Debug?.Print(LogClass.ServiceAm, $"Updating keyboard text to {text} and cursor position to {cursorBegin}"); state = InlineKeyboardState.Complete; PushChangedString(text, (uint)cursorBegin, state); } else { // The 'Complete' state indicates the Calc request has been fulfilled by the applet, // but do not change the state of the entire applet, only the responses. state = InlineKeyboardState.Complete; if (isAccept) { Logger.Debug?.Print(LogClass.ServiceAm, $"Sending keyboard OK with text {text}"); DecidedEnter(text, state); } else if (isCancel) { Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel"); DecidedCancel(state); } _interactiveSession.Push(InlineResponses.Default(state)); Logger.Debug?.Print(LogClass.ServiceAm, $"Resetting state of the keyboard to {state}"); // Set the state of the applet to 'Initialized' as it is the only known state so far // that does not soft-lock the keyboard after use. state = InlineKeyboardState.Initialized; _interactiveSession.Push(InlineResponses.Default(state)); SetInlineState(state); } }); }
private void OnBackgroundInteractiveData(byte[] data) { // WARNING: Only invoke applet state changes after an explicit finalization // request from the game, this is because the inline keyboard is expected to // keep running in the background sending data by itself. using (MemoryStream stream = new MemoryStream(data)) using (BinaryReader reader = new BinaryReader(stream)) { InlineKeyboardRequest request = (InlineKeyboardRequest)reader.ReadUInt32(); InlineKeyboardState state = GetInlineState(); long remaining; Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {state}"); switch (request) { case InlineKeyboardRequest.UseChangedStringV2: Logger.Stub?.Print(LogClass.ServiceAm, "Keyboard response ChangedStringV2"); break; case InlineKeyboardRequest.UseMovedCursorV2: Logger.Stub?.Print(LogClass.ServiceAm, "Keyboard response MovedCursorV2"); break; case InlineKeyboardRequest.SetUserWordInfo: // Read the user word info data. remaining = stream.Length - stream.Position; if (remaining < sizeof(int)) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes"); } else { int wordsCount = reader.ReadInt32(); int wordSize = Marshal.SizeOf <SoftwareKeyboardUserWord>(); remaining = stream.Length - stream.Position; if (wordsCount > MaxUserWords) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}"); } else if (wordsCount * wordSize != remaining) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words"); } else { _keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount]; for (int word = 0; word < wordsCount; word++) { byte[] wordData = reader.ReadBytes(wordSize); _keyboardBackgroundUserWords[word] = ReadStruct <SoftwareKeyboardUserWord>(wordData); } } } _interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(state)); break; case InlineKeyboardRequest.SetCustomizeDic: // Read the custom dic data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf <SoftwareKeyboardCustomizeDic>()) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes"); } else { var keyboardDicData = reader.ReadBytes((int)remaining); _keyboardBackgroundDic = ReadStruct <SoftwareKeyboardCustomizeDic>(keyboardDicData); } break; case InlineKeyboardRequest.SetCustomizedDictionaries: // Read the custom dictionaries data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf <SoftwareKeyboardDictSet>()) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes"); } else { var keyboardDictData = reader.ReadBytes((int)remaining); _keyboardBackgroundDictSet = ReadStruct <SoftwareKeyboardDictSet>(keyboardDictData); } break; case InlineKeyboardRequest.Calc: // The Calc request tells the Applet to enter the main input handling loop, which will end // with either a text being submitted or a cancel request from the user. // Read the Calc data. SoftwareKeyboardCalc newCalc; remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf <SoftwareKeyboardCalc>()) { Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes"); newCalc = new SoftwareKeyboardCalc(); } else { var keyboardCalcData = reader.ReadBytes((int)remaining); newCalc = ReadStruct <SoftwareKeyboardCalc>(keyboardCalcData); } // Make the state transition. if (state < InlineKeyboardState.Ready) { // This field consistently is -1 when the calc is not meant to be shown. if (newCalc.Appear.Padding1 == -1) { state = InlineKeyboardState.Initialized; Logger.Debug?.Print(LogClass.ServiceAm, $"Calc during state {state} is probably a dummy"); } else { // Set the new calc _keyboardBackgroundCalc = newCalc; // Check if the application expects UTF8 encoding instead of UTF16. if (_keyboardBackgroundCalc.UseUtf8) { _encoding = Encoding.UTF8; } string newText = _keyboardBackgroundCalc.InputText; uint cursorPosition = (uint)_keyboardBackgroundCalc.CursorPos; _dynamicTextInputHandler?.SetText(newText); state = InlineKeyboardState.Ready; PushChangedString(newText, cursorPosition, state); } SetInlineState(state); } else if (state == InlineKeyboardState.Complete) { state = InlineKeyboardState.Initialized; } // Send the response to the Calc _interactiveSession.Push(InlineResponses.Default(state)); break; case InlineKeyboardRequest.Finalize: // Destroy the dynamic text input handler if (_dynamicTextInputHandler != null) { _dynamicTextInputHandler.TextChanged -= DynamicTextChanged; _dynamicTextInputHandler.Dispose(); } // The calling application wants to close the keyboard applet and will wait for a state change. SetInlineState(InlineKeyboardState.Uninitialized); AppletStateChanged?.Invoke(this, null); break; default: // We shouldn't be able to get here through standard swkbd execution. Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {state}"); _interactiveSession.Push(InlineResponses.Default(state)); break; } } }
private void SetInlineState(InlineKeyboardState state) { _backgroundState = state; }
private static void BeginResponse(InlineKeyboardState state, InlineKeyboardResponse resCode, BinaryWriter writer) { writer.Write((uint)state); writer.Write((uint)resCode); }
private void GetInputTextAndSend(bool shouldShowKeyboard, InlineKeyboardState oldState) { bool submit = true; // Use the text specified by the Calc if it is available, otherwise use the default one. string inputText = (!string.IsNullOrWhiteSpace(_keyboardBackgroundCalc.InputText) ? _keyboardBackgroundCalc.InputText : DefaultText); // Compute the elapsed time for the debouncing algorithm. long currentMillis = PerformanceCounter.ElapsedMilliseconds; long inputElapsedMillis = currentMillis - _lastTextSetMillis; // Reset the input text before submitting the final result, that's because some games do not expect // consecutive submissions to abruptly shrink and they will crash if it happens. Changing the string // before the final submission prevents that. InlineKeyboardState newState = InlineKeyboardState.DataAvailable; SetInlineState(newState); ChangedString("", newState); if (!_lastWasHidden && (inputElapsedMillis < DebounceTimeMillis)) { // A repeated Calc request has been received without player interaction, after the input has been // sent. This behavior happens in some games, so instead of showing another dialog, just apply a // time-based debouncing algorithm and repeat the last submission, either a value or a cancel. // It is also possible that the first Calc request was hidden by accident, in this case use the // debouncing as an oportunity to properly ask for input. inputText = _textValue; submit = _textValue != null; _lastWasHidden = false; Logger.Warning?.Print(LogClass.Application, "Debouncing repeated keyboard request"); } else if (!shouldShowKeyboard) { // Submit the default text to avoid soft locking if the keyboard was ignored by // accident. It's better to change the name than being locked out of the game. inputText = DefaultText; _lastWasHidden = true; Logger.Debug?.Print(LogClass.Application, "Received a dummy Calc, keyboard will not be shown"); } else if (_device.UiHandler == null) { Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default"); _lastWasHidden = false; } else { // Call the configured GUI handler to get user's input. var args = new SoftwareKeyboardUiArgs { HeaderText = "", // The inline keyboard lacks these texts SubtitleText = "", GuideText = "", SubmitText = (!string.IsNullOrWhiteSpace(_keyboardBackgroundCalc.Appear.OkText) ? _keyboardBackgroundCalc.Appear.OkText : "OK"), StringLengthMin = 0, StringLengthMax = 100, InitialText = inputText }; submit = _device.UiHandler.DisplayInputDialog(args, out inputText); inputText = submit ? inputText : null; _lastWasHidden = false; } // The 'Complete' state indicates the Calc request has been fulfilled by the applet. newState = InlineKeyboardState.Complete; if (submit) { Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard OK"); DecidedEnter(inputText, newState); } else { Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel"); DecidedCancel(newState); } _interactiveSession.Push(InlineResponses.Default(newState)); // The constant calls to PopInteractiveData suggest that the keyboard applet continuously reports // data back to the application and this can also be time-sensitive. Pushing a state reset right // after the data has been sent does not work properly and the application will soft-lock. This // delay gives time for the application to catch up with the data and properly process the state // reset. Thread.Sleep(ResetDelayMillis); // 'Initialized' is the only known state so far that does not soft-lock the keyboard after use. newState = InlineKeyboardState.Initialized; Logger.Debug?.Print(LogClass.ServiceAm, $"Resetting state of the keyboard to {newState}"); SetInlineState(newState); _interactiveSession.Push(InlineResponses.Default(newState)); // Keep the text and the timestamp of the input for the debouncing algorithm. _textValue = inputText; _lastTextSetMillis = PerformanceCounter.ElapsedMilliseconds; }
private void OnBackgroundInteractiveData(byte[] data) { // WARNING: Only invoke applet state changes after an explicit finalization // request from the game, this is because the inline keyboard is expected to // keep running in the background sending data by itself. using (MemoryStream stream = new MemoryStream(data)) using (BinaryReader reader = new BinaryReader(stream)) { var request = (InlineKeyboardRequest)reader.ReadUInt32(); long remaining; Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {_backgroundState}"); switch (request) { case InlineKeyboardRequest.UseChangedStringV2: Logger.Stub?.Print(LogClass.ServiceAm, "Inline keyboard request UseChangedStringV2"); break; case InlineKeyboardRequest.UseMovedCursorV2: Logger.Stub?.Print(LogClass.ServiceAm, "Inline keyboard request UseMovedCursorV2"); break; case InlineKeyboardRequest.SetUserWordInfo: // Read the user word info data. remaining = stream.Length - stream.Position; if (remaining < sizeof(int)) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes"); } else { int wordsCount = reader.ReadInt32(); int wordSize = Marshal.SizeOf <SoftwareKeyboardUserWord>(); remaining = stream.Length - stream.Position; if (wordsCount > MaxUserWords) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}"); } else if (wordsCount * wordSize != remaining) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words"); } else { _keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount]; for (int word = 0; word < wordsCount; word++) { byte[] wordData = reader.ReadBytes(wordSize); _keyboardBackgroundUserWords[word] = ReadStruct <SoftwareKeyboardUserWord>(wordData); } } } _interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(_backgroundState)); break; case InlineKeyboardRequest.SetCustomizeDic: // Read the custom dic data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf <SoftwareKeyboardCustomizeDic>()) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes"); } else { var keyboardDicData = reader.ReadBytes((int)remaining); _keyboardBackgroundDic = ReadStruct <SoftwareKeyboardCustomizeDic>(keyboardDicData); } break; case InlineKeyboardRequest.SetCustomizedDictionaries: // Read the custom dictionaries data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf <SoftwareKeyboardDictSet>()) { Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes"); } else { var keyboardDictData = reader.ReadBytes((int)remaining); _keyboardBackgroundDictSet = ReadStruct <SoftwareKeyboardDictSet>(keyboardDictData); } break; case InlineKeyboardRequest.Calc: // The Calc request is used to communicate configuration changes and commands to the keyboard. // Fields in the Calc struct and operations are masked by the Flags field. // Read the Calc data. SoftwareKeyboardCalcEx newCalc; remaining = stream.Length - stream.Position; if (remaining == Marshal.SizeOf <SoftwareKeyboardCalc>()) { var keyboardCalcData = reader.ReadBytes((int)remaining); var keyboardCalc = ReadStruct <SoftwareKeyboardCalc>(keyboardCalcData); newCalc = keyboardCalc.ToExtended(); } else if (remaining == Marshal.SizeOf <SoftwareKeyboardCalcEx>() || remaining == SoftwareKeyboardCalcEx.AlternativeSize) { var keyboardCalcData = reader.ReadBytes((int)remaining); newCalc = ReadStruct <SoftwareKeyboardCalcEx>(keyboardCalcData); } else { Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes"); newCalc = new SoftwareKeyboardCalcEx(); } // Process each individual operation specified in the flags. bool updateText = false; if ((newCalc.Flags & KeyboardCalcFlags.Initialize) != 0) { _interactiveSession.Push(InlineResponses.FinishedInitialize(_backgroundState)); _backgroundState = InlineKeyboardState.Initialized; } if ((newCalc.Flags & KeyboardCalcFlags.SetCursorPos) != 0) { _cursorBegin = newCalc.CursorPos; updateText = true; Logger.Debug?.Print(LogClass.ServiceAm, $"Cursor position set to {_cursorBegin}"); } if ((newCalc.Flags & KeyboardCalcFlags.SetInputText) != 0) { _textValue = newCalc.InputText; updateText = true; Logger.Debug?.Print(LogClass.ServiceAm, $"Input text set to {_textValue}"); } if ((newCalc.Flags & KeyboardCalcFlags.SetUtf8Mode) != 0) { _encoding = newCalc.UseUtf8 ? Encoding.UTF8 : Encoding.Default; Logger.Debug?.Print(LogClass.ServiceAm, $"Encoding set to {_encoding}"); } if (updateText) { _dynamicTextInputHandler.SetText(_textValue, _cursorBegin); _keyboardRenderer.UpdateTextState(_textValue, _cursorBegin, _cursorBegin, null, null); } if ((newCalc.Flags & KeyboardCalcFlags.MustShow) != 0) { ActivateFrontend(); _backgroundState = InlineKeyboardState.Shown; PushChangedString(_textValue, (uint)_cursorBegin, _backgroundState); } // Send the response to the Calc _interactiveSession.Push(InlineResponses.Default(_backgroundState)); break; case InlineKeyboardRequest.Finalize: // Destroy the frontend. DestroyFrontend(); // The calling application wants to close the keyboard applet and will wait for a state change. _backgroundState = InlineKeyboardState.Uninitialized; AppletStateChanged?.Invoke(this, null); break; default: // We shouldn't be able to get here through standard swkbd execution. Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_backgroundState}"); _interactiveSession.Push(InlineResponses.Default(_backgroundState)); break; } } }
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) { lock (_lock) { _normalSession = normalSession; _interactiveSession = interactiveSession; _interactiveSession.DataAvailable += OnInteractiveData; var launchParams = _normalSession.Pop(); var keyboardConfig = _normalSession.Pop(); _isBackground = keyboardConfig.Length == Marshal.SizeOf <SoftwareKeyboardInitialize>(); if (_isBackground) { // Initialize the keyboard applet in background mode. _keyboardBackgroundInitialize = ReadStruct <SoftwareKeyboardInitialize>(keyboardConfig); _backgroundState = InlineKeyboardState.Uninitialized; if (_device.UiHandler == null) { Logger.Error?.Print(LogClass.ServiceAm, "GUI Handler is not set, software keyboard applet will not work properly"); } else { // Create a text handler that converts keyboard strokes to strings. _dynamicTextInputHandler = _device.UiHandler.CreateDynamicTextInputHandler(); _dynamicTextInputHandler.TextChangedEvent += HandleTextChangedEvent; _dynamicTextInputHandler.KeyPressedEvent += HandleKeyPressedEvent; _npads = new NpadReader(_device); _npads.NpadButtonDownEvent += HandleNpadButtonDownEvent; _npads.NpadButtonUpEvent += HandleNpadButtonUpEvent; _keyboardRenderer = new SoftwareKeyboardRenderer(_device.UiHandler.HostUiTheme); } return(ResultCode.Success); } else { // Initialize the keyboard applet in foreground mode. if (keyboardConfig.Length < Marshal.SizeOf <SoftwareKeyboardConfig>()) { Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf<SoftwareKeyboardConfig>():x}. Got {keyboardConfig.Length:x}"); } else { _keyboardForegroundConfig = ReadStruct <SoftwareKeyboardConfig>(keyboardConfig); } if (!_normalSession.TryPop(out _transferMemory)) { Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null"); } if (_keyboardForegroundConfig.UseUtf8) { _encoding = Encoding.UTF8; } _foregroundState = SoftwareKeyboardState.Ready; ExecuteForegroundKeyboard(); return(ResultCode.Success); } } }