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());
                }
        }
Beispiel #10
0
        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));
            }
        }
Beispiel #13
0
        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;
 }
Beispiel #20
0
 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);
                }
            }
        }