Android Open Source - MorseKeyboard Gtap






From Project

Back to project page MorseKeyboard.

License

The source code is released under:

Apache License

If you think the Android project MorseKeyboard listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/* 
** Copyright 2012 Google Inc. All Rights Reserved.
** // www.j a va2  s .c  o  m
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
** 
**   http://www.apache.org/licenses/LICENSE-2.0
** 
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/

package com.rgam.morsekeyboard;

import android.content.SharedPreferences;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.os.Handler;
import android.text.method.MetaKeyKeyListener;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.PopupMenu;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * This code is adapted from the Android Soft Keyboard example.
 *
 * @author Reed Morse
 * @author Vasya Drobushkov changed:
 *         - added toggleLanguage functionality
 *         - added Shift functionality
 */
public class Gtap extends InputMethodService
        implements KeyboardView.OnKeyboardActionListener {
    static final boolean DEBUG = false;

    /**
     * This boolean indicates the optional example code for performing
     * processing of hard keys in addition to regular text generation
     * from on-screen interaction.  It would be used for input methods that
     * perform language translations (such as converting text entered on
     * a QWERTY keyboard to Chinese), but may not be used for input methods
     * that are primarily intended to be used for on-screen text entry.
     */
    static final boolean PROCESS_HARD_KEYS = true;

    private KeyboardView mInputView;
    private CandidateView mCandidateView;
    private CompletionInfo[] mCompletions;

    private StringBuilder mComposing = new StringBuilder();
    private boolean mPredictionOn;
    private boolean mCompletionOn;
    private int mLastDisplayWidth;
    private boolean mCapsLock;
    private long mLastShiftTime;
    private long mMetaState;

    private LatinKeyboard mSymbolsKeyboard;
    private LatinKeyboard mMorseKeyboard;

    private LatinKeyboard mCurKeyboard;

    private String mWordSeparators;

    // This is used to keep track of location in Predictive Mode.
    private int poemLocation;
    private boolean predictiveEnabled;
    private SharedPreferences settings;
    private Handler morseHandler;

    // Constants.
    private final int KEYCODE_SPACE = 32;
    private final int DOT_LENGTH_MS = 300;
    private final int DASH_MULTIPLIER = 3;
    private final int LETTER_MULTIPLIER = 4;
    private final int WORD_MULTIPLIER = 7;
    private final long DASH_LENGTH_MILLIS = 300L;

    private final int KEYCODE_LETTER_SEPARATOR = -77;
    private final int KEYCODE_WORD_SEPARATOR = -78;

    /* CUSTOM LONG CLICK DURATION */
    private boolean mIsMorseButtonPressed = false;
    private long mDotDashKeyLastPressTime = 0L;

    // This is typed in Predictive Mode.
    //private final String poem = "A shimmering global phenomenon. Surfing invisible currents of information. Design the soul of an intelligent machine. Do androids dream of electric sheep?";
    private final String poem = "I've been meaning to tell you for some time about my strong feelings for you. That time we went to get tacos was a wonderful night that creeps into my memories frequently. April fools! ";
    //private final String poem = "I am standing here doing an interview at the Academy of Country Music Awards. It is really exciting that I can email you at the same time. ";

    /**
     * Main initialization of the input method component.
     */
    @Override
    public void onCreate() {
        super.onCreate();
        mWordSeparators = getResources().getString(R.string.word_separators);
        poemLocation = 0;
        settings = getSharedPreferences(Constants.GTAP_PREFS, 0);
    }

    /**
     * UI initialization, called after creation and configuration changes.
     */
    @Override
    public void onInitializeInterface() {
        if (mMorseKeyboard != null) {
            // Configuration changes can happen after the keyboard gets recreated,
            // so we need to be able to re-build the keyboards if the available
            // space has changed.
            int displayWidth = getMaxWidth();
            if (displayWidth == mLastDisplayWidth) return;
            mLastDisplayWidth = displayWidth;
        }
        mMorseKeyboard = new LatinKeyboard(this, R.xml.qwerty);

        updateShiftKeyState(getCurrentInputEditorInfo());
        updateLanguageKeyLabel();
    }

    /**
     * Called by the framework when your view for creating input needs to
     * be generated.  This will be called the first time your input method
     * is displayed, and every time it needs to be re-created such as due to
     * a configuration change.
     */
    @Override
    public View onCreateInputView() {
        mInputView = (KeyboardView) getLayoutInflater().inflate(
                R.layout.input, null);
        mInputView.setOnKeyboardActionListener(this);
        mInputView.setKeyboard(mMorseKeyboard);
        return mInputView;
    }

    /**
     * Called by the framework when your view for showing candidates needs to
     * be generated, like {@link #onCreateInputView}.
     */
    @Override
    public View onCreateCandidatesView() {
        mCandidateView = new CandidateView(this);
        mCandidateView.setService(this);
        return mCandidateView;
    }

    /**
     * This is the main point where we do our initialization of the input method
     * to begin operating on an application.  At this point we have been
     * bound to the client, and are now receiving all of the detailed information
     * about the target of our edits.
     */
    @Override
    public void onStartInput(EditorInfo attribute, boolean restarting) {
        super.onStartInput(attribute, restarting);

        // predictiveEnabled = settings.getBoolean(Constants.GTAP_PREDICTIVE_MODE, false);

        // Reset our state.  We want to do this even if restarting, because
        // the underlying state of the text editor could have changed in any way.
        mComposing.setLength(0);
        updateCandidates();

        if (!restarting) {
            // Clear shift states.
            mMetaState = 0;
        }

        mPredictionOn = false;
        mCompletionOn = false;
        mCompletions = null;

        // We are now going to initialize our state based on the type of
        // text being edited.
        switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) {
            // Normally we would switch to a different keyboard,
            // but everything is going to be the Morse keyboard.
            case EditorInfo.TYPE_CLASS_NUMBER:
            case EditorInfo.TYPE_CLASS_DATETIME:
            case EditorInfo.TYPE_CLASS_PHONE:
            case EditorInfo.TYPE_CLASS_TEXT:
                // This is general text editing.  We will default to the
                // normal alphabetic keyboard, and assume that we should
                // be doing predictive text (showing candidates as the
                // user types).
                mCurKeyboard = mMorseKeyboard;
                mPredictionOn = true;

                // We now look for a few special variations of text that will
                // modify our behavior.
                int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION;
                if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD ||
                        variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) {
                    // Do not display predictions / what the user is typing
                    // when they are entering a password.
                    mPredictionOn = false;
                }

                if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
                        || variation == EditorInfo.TYPE_TEXT_VARIATION_URI
                        || variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) {
                    // Our predictions are not useful for e-mail addresses
                    // or URIs.
                    mPredictionOn = false;
                }

                if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
                    // If this is an auto-complete text view, then our predictions
                    // will not be shown and instead we will allow the editor
                    // to supply their own.  We only show the editor's
                    // candidates when in fullscreen mode, otherwise relying
                    // own it displaying its own UI.
                    mPredictionOn = false;
                    mCompletionOn = isFullscreenMode();
                }

                break;

            default:
                // For all unknown input types, default to the alphabetic
                // keyboard with no special features.
                mCurKeyboard = mMorseKeyboard;
                updateShiftKeyState(attribute);
        }

        // Update the label on the enter key, depending on what the application
        // says it will do.
        mCurKeyboard.setImeOptions(getResources(), attribute.imeOptions);
    }

    /**
     * This is called when the user is done editing a field.  We can use
     * this to reset our state.
     */
    @Override
    public void onFinishInput() {
        super.onFinishInput();

        // Clear current composing text and candidates.
        mComposing.setLength(0);
        updateCandidates();

        // We only hide the candidates window when finishing input on
        // a particular editor, to avoid popping the underlying application
        // up and down if the user is entering text into the bottom of
        // its window.
        setCandidatesViewShown(false);

        mCurKeyboard = mMorseKeyboard;
        if (mInputView != null) {
            mInputView.closing();
        }
    }

    @Override
    public void onStartInputView(EditorInfo attribute, boolean restarting) {
        super.onStartInputView(attribute, restarting);
        // Apply the selected keyboard to the input view.
        mInputView.setKeyboard(mCurKeyboard);
        mInputView.closing();
    }

    /**
     * Deal with the editor reporting movement of its cursor.
     */
    @Override
    public void onUpdateSelection(int oldSelStart, int oldSelEnd,
                                  int newSelStart, int newSelEnd,
                                  int candidatesStart, int candidatesEnd) {
        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
                candidatesStart, candidatesEnd);

        // If the current selection in the text view changes, we should
        // clear whatever candidate text we have.
        if (mComposing.length() > 0 && (newSelStart != candidatesEnd
                || newSelEnd != candidatesEnd)) {
            mComposing.setLength(0);
            updateCandidates();
            InputConnection ic = getCurrentInputConnection();
            if (ic != null) {
                ic.finishComposingText();
            }
        }
    }

    /**
     * This tells us about completions that the editor has determined based
     * on the current text in it.  We want to use this in fullscreen mode
     * to show the completions ourself, since the editor can not be seen
     * in that situation.
     */
    @Override
    public void onDisplayCompletions(CompletionInfo[] completions) {
        if (mCompletionOn) {
            mCompletions = completions;
            if (completions == null) {
                setSuggestions(null, false, false);
                return;
            }

            List<String> stringList = new ArrayList<String>();
            for (int i = 0; i < (completions != null ? completions.length : 0); i++) {
                CompletionInfo ci = completions[i];
                if (ci != null) stringList.add(ci.getText().toString());
            }
            setSuggestions(stringList, true, true);
        }
    }

    public void predictiveModeChanged() {
    }

    /**
     * This translates incoming hard key events in to edit operations on an
     * InputConnection.  It is only needed when using the
     * PROCESS_HARD_KEYS option.
     */
    private boolean translateKeyDown(int keyCode, KeyEvent event) {
        mMetaState = MetaKeyKeyListener.handleKeyDown(mMetaState,
                keyCode, event);
        int c = event.getUnicodeChar(MetaKeyKeyListener.getMetaState(mMetaState));
        mMetaState = MetaKeyKeyListener.adjustMetaAfterKeypress(mMetaState);
        InputConnection ic = getCurrentInputConnection();
        if (c == 0 || ic == null) {
            return false;
        }

        boolean dead = false;

        if ((c & KeyCharacterMap.COMBINING_ACCENT) != 0) {
            dead = true;
            c = c & KeyCharacterMap.COMBINING_ACCENT_MASK;
        }

        if (mComposing.length() > 0) {
            char accent = mComposing.charAt(mComposing.length() - 1);
            int composed = KeyEvent.getDeadChar(accent, c);

            if (composed != 0) {
                c = composed;
                mComposing.setLength(mComposing.length() - 1);
            }
        }

        onKey(c, null);

        return true;
    }

    /**
     * Use this to monitor key events being delivered to the application.
     * We get first crack at them, and can either resume them or let them
     * continue to the app.
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_BACK:
                // The InputMethodService already takes care of the back
                // key for us, to dismiss the input method if it is shown.
                // However, our keyboard could be showing a pop-up window
                // that back should dismiss, so we first allow it to do that.
                if (event.getRepeatCount() == 0 && mInputView != null) {
                    if (mInputView.handleBack()) {
                        return true;
                    }
                }
                break;

            case KeyEvent.KEYCODE_DEL:
                // Special handling of the delete key: if we currently are
                // composing text for the user, we want to modify that instead
                // of let the application to the delete itself.
                if (mComposing.length() > 0) {
                    onKey(Keyboard.KEYCODE_DELETE, null);
                    return true;
                }
                break;

            case KeyEvent.KEYCODE_ENTER:
                // Let the underlying text editor always handle these.
                return false;

            default:
                // For all other keys, if we want to do transformations on
                // text being entered with a hard keyboard, we need to process
                // it and do the appropriate action.
                if (PROCESS_HARD_KEYS) {
                    if (mPredictionOn && translateKeyDown(keyCode, event)) {
                        return true;
                    }
                }
        }
        return super.onKeyDown(keyCode, event);
    }

    /**
     * Use this to monitor key events being delivered to the application.
     * We get first crack at them, and can either resume them or let them
     * continue to the app.
     */
    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {

        // If we want to do transformations on text being entered with a hard
        // keyboard, we need to process the up events to update the meta key
        // state we are tracking.
        if (PROCESS_HARD_KEYS) {
            if (mPredictionOn) {
                mMetaState = MetaKeyKeyListener.handleKeyUp(mMetaState,
                        keyCode, event);
            }
        }
        return super.onKeyUp(keyCode, event);
    }

    /**
     * Helper function to commit any text being composed in to the editor.
     */
    private void commitTyped(InputConnection inputConnection) {
        if (mComposing.length() > 0) {
            if (predictiveEnabled) {
                inputConnection.commitText(mComposing, mComposing.length());
            } else {
                Character morseCharacter = Morse.characterFromCode(mComposing.toString(),
                        mInputView.isShifted());
                if (morseCharacter != null) {
                    String morseString = String.valueOf(morseCharacter.charValue());
                    inputConnection.commitText(morseString, morseString.length());
                } else {
                    // Do nothing, it's not a valid combo.
                    //inputConnection.commitText(mComposing, mComposing.length());
                }
            }
            mComposing.setLength(0);
            updateCandidates();
        }
    }

    private void commitTyped(InputConnection inputConnection, String string) {
        if (predictiveEnabled) {
            inputConnection.commitText(mComposing, mComposing.length());
        } else {
            inputConnection.commitText(string, string.length());
        }
        updateCandidates();
    }

    /**
     * Helper to update the shift state of our keyboard based on the initial
     * editor state.
     */
    private void updateShiftKeyState(EditorInfo attr) {
        if (attr != null
                && mInputView != null && mMorseKeyboard == mInputView.getKeyboard()) {
            int caps = 0;
            EditorInfo ei = getCurrentInputEditorInfo();
            if (ei != null && ei.inputType != EditorInfo.TYPE_NULL) {
                caps = getCurrentInputConnection().getCursorCapsMode(attr.inputType);
            }
            mInputView.setShifted(mCapsLock || caps != 0);
        }
    }

    private void updateLanguageKeyLabel() {
        for (Keyboard.Key key : mMorseKeyboard.getKeys()) {
            if (Arrays.binarySearch(key.codes, Keyboard.KEYCODE_MODE_CHANGE) == 0) {
                key.label = getCurrentLanguageShort();
                return;
            }
        }
    }

    private String getCurrentLanguageShort() {
        switch (Morse.getCurrentLanguage()) {
            case LANGUAGE_ENGLISH:
                return "EN";
            case LANGUAGE_RUSSIAN:
                return "RU";
            default:
                return "";
        }
    }

    /**
     * Helper to send a key down / key up pair to the current editor.
     */
    private void keyDownUp(int keyEventCode) {
        getCurrentInputConnection().sendKeyEvent(
                new KeyEvent(KeyEvent.ACTION_DOWN, keyEventCode));
        getCurrentInputConnection().sendKeyEvent(
                new KeyEvent(KeyEvent.ACTION_UP, keyEventCode));
    }

    // Implementation of KeyboardViewListener
    public void onKey(int primaryCode, int[] keyCodes) {

        if (mIsMorseButtonPressed) {
            // wait for release morse button
            return;
        }

        invalidateLetterAndWordTimers();
        predictiveEnabled = settings.getBoolean(Constants.GTAP_PREDICTIVE_MODE, false);

        if (primaryCode == KEYCODE_SPACE) {
            if (mComposing.length() > 0) {
                commitTyped(getCurrentInputConnection());
                beginLetterAndWordTimers();
            } else {
                // Nothing typed, commit a space.
                commitTyped(getCurrentInputConnection(), " ");
            }
        } else if (primaryCode == Keyboard.KEYCODE_DELETE) {
            handleBackspace();
        } else if (primaryCode == Keyboard.KEYCODE_CANCEL) {
            handleClose();
            return;
        } else if (primaryCode == Keyboard.KEYCODE_MODE_CHANGE) {
            if (isLongClick(keyCodes)) {
                showLanguageHintDialog();
            } else {
                toggleLanguage();
                updateLanguageKeyLabel();
            }
        } else if (primaryCode == LatinKeyboardView.KEYCODE_OPTIONS) {
            // Show a menu or something.
        } else if (primaryCode == Keyboard.KEYCODE_SHIFT) {
            mCapsLock = !mCapsLock;
            updateShiftKeyState(getCurrentInputEditorInfo());
        } else {
            handleCharacter(primaryCode, keyCodes);
        }
    }

    private boolean isLongClick(int[] keyCodes) {
        if (keyCodes == null) {
            return false;
        }
        if (keyCodes.length == 0) {
            return false;
        }
        return keyCodes[0] == LatinKeyboardView.KEY_LONG_PRESS;
    }

    private void showLanguageHintDialog() {
        switch (Morse.getCurrentLanguage()) {
            case LANGUAGE_ENGLISH: {
                PopupMenu popupMenu = new PopupMenu(this, mInputView);
                popupMenu.inflate(R.menu.morse_hint_english);
                popupMenu.show();
                break;
            }
            case LANGUAGE_RUSSIAN: {
                PopupMenu popupMenu = new PopupMenu(this, mInputView);
                popupMenu.inflate(R.menu.morse_hint_russian);
                popupMenu.show();
                break;
            }
            default:
                return;
        }
    }

    private void toggleLanguage() {
        Morse.toggleLanguage();
    }

    public void onText(CharSequence text) {
        InputConnection ic = getCurrentInputConnection();
        if (ic == null) return;
        ic.beginBatchEdit();
        if (mComposing.length() > 0) {
            commitTyped(ic);
        }
        ic.commitText(text, 0);
        ic.endBatchEdit();
        updateShiftKeyState(getCurrentInputEditorInfo());
    }

    /**
     * Update the list of available candidates from the current composing
     * text.  This will need to be filled in by however you are determining
     * candidates.
     */
    private void updateCandidates() {
        if (!mCompletionOn) {
            if (mComposing.length() > 0) {
                ArrayList<String> list = new ArrayList<String>();
                list.add(mComposing.toString());
                setSuggestions(list, true, true);
            } else {
                setSuggestions(null, false, false);
            }
        }
    }

    public void setSuggestions(List<String> suggestions, boolean completions,
                               boolean typedWordValid) {
        if (suggestions != null && suggestions.size() > 0) {
            setCandidatesViewShown(true);
        } else if (isExtractViewShown()) {
            setCandidatesViewShown(true);
        }
        if (mCandidateView != null) {
            mCandidateView.setSuggestions(suggestions, completions, typedWordValid);
        }
    }

    private void handleBackspace() {
        final int length = mComposing.length();
        if (length > 1) {
            mComposing.delete(length - 1, length);
            getCurrentInputConnection().setComposingText(mComposing, 1);
            updateCandidates();
        } else if (length > 0) {
            mComposing.setLength(0);
            getCurrentInputConnection().commitText("", 0);
            updateCandidates();
        } else {
            keyDownUp(KeyEvent.KEYCODE_DEL);
        }
        updateShiftKeyState(getCurrentInputEditorInfo());
    }

    private int tapCounter = 0;

    private void handleCharacter(int primaryCode, int[] keyCodes) {
        if (predictiveEnabled) {
            /*
        // Each tap is one word
        String[] poemFragment = poem.split(" ");
        poemLocation = (poemLocation >= poemFragment.length - 1) ? 0 : poemLocation;
        mComposing.append(poemFragment[poemLocation++] + " ");
        commitTyped(getCurrentInputConnection());
        */

            // One tap is one character
            if (tapCounter++ % 1 == 0) {
                poemLocation = (poemLocation >= poem.length()) ? 0 : poemLocation;
                mComposing.append(poem.charAt(poemLocation++));
                commitTyped(getCurrentInputConnection());
            }
        
        /*
        // For LL Cool J each tap is worth three letters.
        poemLocation = (poemLocation < 0) ? 0 : poemLocation;
        poemLocation = (poemLocation >= poem.length()) ? 0 : poemLocation;
        int offset = (poem.length() - poemLocation < 3) ? poem.length() - poemLocation : 3;
        mComposing.append(poem.substring(poemLocation, poemLocation + offset));
        poemLocation += 3;
        commitTyped(getCurrentInputConnection());
        */
        } else {
            mComposing.append((char) primaryCode);
            updateCandidates();
        }

        beginLetterAndWordTimers();
    }

    private void handleClose() {
        commitTyped(getCurrentInputConnection());
        requestHideSelf(0);
        mInputView.closing();
    }

    private String getWordSeparators() {
        return mWordSeparators;
    }

    public boolean isWordSeparator(int code) {
        String separators = getWordSeparators();
        return separators.contains(String.valueOf((char) code));
    }

    public void pickDefaultCandidate() {
        pickSuggestionManually(0);
    }

    public void pickSuggestionManually(int index) {
        if (mCompletionOn && mCompletions != null && index >= 0
                && index < mCompletions.length) {
            CompletionInfo ci = mCompletions[index];
            getCurrentInputConnection().commitCompletion(ci);
            if (mCandidateView != null) {
                mCandidateView.clear();
            }
            updateShiftKeyState(getCurrentInputEditorInfo());
        } else if (mComposing.length() > 0) {
            // If we were generating candidate suggestions for the current
            // text, we would commit one of them here.  But for this sample,
            // we will just commit the current text.
            commitTyped(getCurrentInputConnection());
        }
    }

    public void swipeRight() {
        if (mCompletionOn) {
            pickDefaultCandidate();
        }
    }

    public void swipeLeft() {
        handleBackspace();
    }

    public void swipeDown() {
        handleClose();
    }

    public void swipeUp() {
    }

    public void onPress(int primaryCode) {
        if (primaryCode == KeyEvent.KEYCODE_R) {
            mIsMorseButtonPressed = true;
            mDotDashKeyLastPressTime = System.currentTimeMillis();
        }
    }

    public void onRelease(int primaryCode) {
        if (primaryCode == KeyEvent.KEYCODE_R) {
            mIsMorseButtonPressed = false;
            if (System.currentTimeMillis() - mDotDashKeyLastPressTime < DASH_LENGTH_MILLIS) {
                onKey(KeyEvent.KEYCODE_R, null);
            } else {
                onKey(KeyEvent.KEYCODE_Q, null);
            }
        }
        mDotDashKeyLastPressTime = 0L;
    }

    private void beginLetterAndWordTimers() {
        invalidateLetterAndWordTimers();
        morseHandler = new Handler();

        int interval = predictiveEnabled ? 0 : DOT_LENGTH_MS * LETTER_MULTIPLIER;
        morseHandler.postDelayed(insertLetterTask, interval);
    }

    private void invalidateLetterAndWordTimers() {
        if (morseHandler != null) {
            morseHandler.removeCallbacks(insertLetterTask);
        }
    }

    private Runnable insertLetterTask = new Runnable() {
        public void run() {
            if (mComposing.length() > 0) {
                commitTyped(getCurrentInputConnection());
            }
        }
    };
}




Java Source Code List

com.rgam.morsekeyboard.CandidateView.java
com.rgam.morsekeyboard.Constants.java
com.rgam.morsekeyboard.Gtap.java
com.rgam.morsekeyboard.LatinKeyboardView.java
com.rgam.morsekeyboard.LatinKeyboard.java
com.rgam.morsekeyboard.Morse.java