| /* |
| * Copyright (C) 2008-2009 Google Inc. |
| * |
| * 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.example.android.softkeyboard; |
| |
| import android.content.Context; |
| import android.inputmethodservice.InputMethodService; |
| import android.inputmethodservice.Keyboard; |
| import android.inputmethodservice.KeyboardView; |
| import android.text.method.MetaKeyKeyListener; |
| import android.util.Log; |
| import android.view.KeyCharacterMap; |
| import android.view.KeyEvent; |
| import android.view.View; |
| import android.view.WindowManager; |
| import android.view.inputmethod.CompletionInfo; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputConnection; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Example of writing an input method for a soft keyboard. This code is |
| * focused on simplicity over completeness, so it should in no way be considered |
| * to be a complete soft keyboard implementation. Its purpose is to provide |
| * a basic example for how you would get started writing an input method, to |
| * be fleshed out as appropriate. |
| */ |
| public class SoftKeyboard extends InputMethodService |
| implements KeyboardView.OnKeyboardActionListener { |
| static final boolean DEBUG = false; |
| |
| 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 Keyboard mSymbolsKeyboard; |
| private Keyboard mSymbolsShiftedKeyboard; |
| private Keyboard mQwertyKeyboard; |
| |
| private String mWordSeparators; |
| |
| private void makeKeyboards() { |
| // Configuration change is coming after the keyboard gets recreated. So don't rely on that. |
| // If keyboards have already been made, check if we have a screen width change and |
| // create the keyboard layouts again at the correct orientation |
| if (mQwertyKeyboard != null) { |
| WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); |
| int displayWidth = wm.getDefaultDisplay().getWidth(); |
| if (displayWidth == mLastDisplayWidth) return; |
| mLastDisplayWidth = displayWidth; |
| } |
| mQwertyKeyboard = new LatinKeyboard(this, R.xml.qwerty); |
| mSymbolsKeyboard = new LatinKeyboard(this, R.xml.symbols); |
| mSymbolsShiftedKeyboard = new LatinKeyboard(this, R.xml.symbols_shift); |
| } |
| |
| @Override public void onCreate() { |
| super.onCreate(); |
| makeKeyboards(); |
| mWordSeparators = getResources().getString(R.string.word_separators); |
| } |
| |
| @Override |
| public View onCreateInputView() { |
| makeKeyboards(); |
| mInputView = (KeyboardView) getLayoutInflater().inflate( |
| R.layout.input, null); |
| mInputView.setOnKeyboardActionListener(this); |
| mInputView.setKeyboard(mQwertyKeyboard); |
| return mInputView; |
| } |
| |
| @Override |
| public View onCreateCandidatesView() { |
| mCandidateView = new CandidateView(this); |
| mCandidateView.setService(this); |
| return mCandidateView; |
| } |
| |
| @Override |
| public void onStartInputView(EditorInfo attribute, boolean restarting) { |
| mComposing.setLength(0); |
| updateCandidates(); |
| |
| if (!restarting) { |
| // Clear shift states. |
| mMetaState = 0; |
| } |
| |
| mPredictionOn = false; |
| mCompletionOn = false; |
| mCompletions = null; |
| Keyboard keyboard; |
| switch (attribute.inputType&EditorInfo.TYPE_MASK_CLASS) { |
| case EditorInfo.TYPE_CLASS_NUMBER: |
| case EditorInfo.TYPE_CLASS_DATETIME: |
| keyboard = mSymbolsKeyboard; |
| break; |
| case EditorInfo.TYPE_CLASS_PHONE: |
| keyboard = mSymbolsKeyboard; |
| break; |
| default: |
| keyboard = mQwertyKeyboard; |
| mPredictionOn = true; |
| // Make sure that passwords are not displayed in candidate view |
| int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION; |
| if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD) { |
| mPredictionOn = false; |
| } |
| if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS |
| || variation == EditorInfo.TYPE_TEXT_VARIATION_URI) { |
| mPredictionOn = false; |
| } |
| if ((attribute.inputType&EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { |
| mPredictionOn = false; |
| mCompletionOn = isFullscreenMode(); |
| } |
| updateShiftKeyState(attribute); |
| break; |
| } |
| |
| if (mInputView != null) { |
| mInputView.setKeyboard(keyboard); |
| mInputView.closing(); |
| } |
| |
| mComposing.setLength(0); |
| setSuggestions(null, false, false); |
| } |
| |
| @Override |
| public void onFinishInput() { |
| super.onFinishInput(); |
| mComposing.setLength(0); |
| updateCandidates(); |
| if (mInputView != null) { |
| mInputView.closing(); |
| } |
| } |
| |
| @Override |
| public void onUpdateSelection(int oldSelStart, int oldSelEnd, |
| int newSelStart, int newSelEnd, |
| int candidatesStart, int 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(); |
| } |
| } |
| } |
| |
| @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); |
| } |
| } |
| |
| 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; |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_BACK: |
| if (event.getRepeatCount() == 0 && mInputView != null) { |
| if (mInputView.handleBack()) { |
| return true; |
| } |
| } |
| break; |
| case KeyEvent.KEYCODE_DEL: |
| if (mComposing.length() > 0) { |
| onKey(Keyboard.KEYCODE_DELETE, null); |
| return true; |
| } |
| break; |
| default: |
| if (mPredictionOn && translateKeyDown(keyCode, event)) { |
| return true; |
| } |
| } |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_DPAD_DOWN: |
| case KeyEvent.KEYCODE_DPAD_UP: |
| case KeyEvent.KEYCODE_DPAD_LEFT: |
| case KeyEvent.KEYCODE_DPAD_RIGHT: |
| // Enable shift key and DPAD to do selections |
| if (mInputView != null && mInputView.isShown() && mInputView.isShifted()) { |
| event = new KeyEvent(event.getDownTime(), event.getEventTime(), |
| event.getAction(), event.getKeyCode(), event.getRepeatCount(), |
| KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); |
| getCurrentInputConnection().sendKeyEvent(event); |
| return true; |
| } |
| break; |
| default: |
| if (mPredictionOn) { |
| mMetaState = MetaKeyKeyListener.handleKeyUp(mMetaState, |
| keyCode, event); |
| } |
| } |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| private void commitTyped(InputConnection inputConnection) { |
| if (mComposing.length() > 0) { |
| inputConnection.commitText(mComposing, mComposing.length()); |
| mComposing.setLength(0); |
| updateCandidates(); |
| } |
| } |
| |
| public void updateShiftKeyState(EditorInfo attr) { |
| if (attr != null |
| && mInputView != null && mQwertyKeyboard == mInputView.getKeyboard()) { |
| int caps = getCurrentInputConnection().getCursorCapsMode(attr.inputType); |
| mInputView.setShifted(mCapsLock || caps != 0); |
| } |
| } |
| |
| private boolean isAlphabet(int code) { |
| if (Character.isLetter(code)) { |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| private void keyDownUp(int keyEventCode) { |
| getCurrentInputConnection().sendKeyEvent( |
| new KeyEvent(KeyEvent.ACTION_DOWN, keyEventCode)); |
| getCurrentInputConnection().sendKeyEvent( |
| new KeyEvent(KeyEvent.ACTION_UP, keyEventCode)); |
| } |
| |
| private void sendKey(int keyCode) { |
| switch (keyCode) { |
| case '\n': |
| keyDownUp(KeyEvent.KEYCODE_ENTER); |
| break; |
| default: |
| if (keyCode >= '0' && keyCode <= '9') { |
| keyDownUp(keyCode - '0' + KeyEvent.KEYCODE_0); |
| } else { |
| getCurrentInputConnection().commitText(String.valueOf((char) keyCode), 1); |
| } |
| break; |
| } |
| } |
| |
| // Implementation of KeyboardViewListener |
| |
| public void onKey(int primaryCode, int[] keyCodes) { |
| if (isWordSeparator(primaryCode)) { |
| // Handle separator |
| if (mComposing.length() > 0) { |
| commitTyped(getCurrentInputConnection()); |
| } |
| sendKey(primaryCode); |
| updateShiftKeyState(getCurrentInputEditorInfo()); |
| } else if (primaryCode == Keyboard.KEYCODE_DELETE) { |
| handleBackspace(); |
| } else if (primaryCode == Keyboard.KEYCODE_SHIFT) { |
| handleShift(); |
| } else if (primaryCode == Keyboard.KEYCODE_CANCEL) { |
| handleClose(); |
| return; |
| } else if (primaryCode == LatinKeyboardView.KEYCODE_OPTIONS) { |
| // Show a menu or somethin' |
| } else if (primaryCode == Keyboard.KEYCODE_MODE_CHANGE |
| && mInputView != null) { |
| Keyboard current = mInputView.getKeyboard(); |
| if (current == mSymbolsKeyboard || current == mSymbolsShiftedKeyboard) { |
| current = mQwertyKeyboard; |
| } else { |
| current = mSymbolsKeyboard; |
| } |
| mInputView.setKeyboard(current); |
| if (current == mSymbolsKeyboard) { |
| current.setShifted(false); |
| } |
| } else { |
| handleCharacter(primaryCode, keyCodes); |
| } |
| } |
| |
| /** |
| * 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) { |
| mCandidateView.setSuggestions(suggestions, completions, typedWordValid); |
| if (suggestions != null && suggestions.size() > 0) { |
| setCandidatesViewShown(true); |
| } else if (isFullscreenMode()) { |
| setCandidatesViewShown(true); |
| } else { |
| setCandidatesViewShown(false); |
| } |
| } |
| |
| private void handleBackspace() { |
| final int length = mComposing.length(); |
| if (length > 1) { |
| mComposing.delete(length - 1, length); |
| getCurrentInputConnection().setComposingText(mComposing, mComposing.length()); |
| updateCandidates(); |
| } else if (length > 0) { |
| mComposing.setLength(0); |
| getCurrentInputConnection().commitText("", 0); |
| updateCandidates(); |
| } else { |
| //getCurrentInputConnection().deleteSurroundingText(1, 0); |
| keyDownUp(KeyEvent.KEYCODE_DEL); |
| } |
| updateShiftKeyState(getCurrentInputEditorInfo()); |
| } |
| |
| private void handleShift() { |
| if (mInputView == null) { |
| return; |
| } |
| |
| Keyboard currentKeyboard = mInputView.getKeyboard(); |
| if (mQwertyKeyboard == currentKeyboard) { |
| // Alphabet keyboard |
| checkToggleCapsLock(); |
| mInputView.setShifted(mCapsLock || !mInputView.isShifted()); |
| } else if (currentKeyboard == mSymbolsKeyboard) { |
| mSymbolsKeyboard.setShifted(true); |
| mInputView.setKeyboard(mSymbolsShiftedKeyboard); |
| mSymbolsShiftedKeyboard.setShifted(true); |
| } else if (currentKeyboard == mSymbolsShiftedKeyboard) { |
| mSymbolsShiftedKeyboard.setShifted(false); |
| mInputView.setKeyboard(mSymbolsKeyboard); |
| mSymbolsKeyboard.setShifted(false); |
| } |
| } |
| |
| private void handleCharacter(int primaryCode, int[] keyCodes) { |
| if (isInputViewShown()) { |
| if (mInputView.isShifted()) { |
| primaryCode = Character.toUpperCase(primaryCode); |
| } |
| } |
| if (isAlphabet(primaryCode) && mPredictionOn) { |
| mComposing.append((char) primaryCode); |
| getCurrentInputConnection().setComposingText(mComposing, mComposing.length()); |
| updateShiftKeyState(getCurrentInputEditorInfo()); |
| updateCandidates(); |
| } else { |
| getCurrentInputConnection().commitText( |
| String.valueOf((char) primaryCode), 0); |
| } |
| } |
| |
| private void handleClose() { |
| commitTyped(getCurrentInputConnection()); |
| dismissSoftInput(0); |
| mInputView.closing(); |
| } |
| |
| private void checkToggleCapsLock() { |
| long now = System.currentTimeMillis(); |
| if (mLastShiftTime + 800 > now) { |
| mCapsLock = !mCapsLock; |
| mLastShiftTime = 0; |
| } else { |
| mLastShiftTime = now; |
| } |
| } |
| |
| protected 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) { } |
| |
| public void onRelease(int primaryCode) { } |
| } |