package idv.Zero.KerKerInput;
import idv.Zero.KerKerInput.KBManager.NativeKeyboardTypes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnTouchListener;
import android.view.ViewGroup.LayoutParams;
import android.view.inputmethod.InputConnection;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.ColorDrawable;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView.OnKeyboardActionListener;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Handler;
import android.os.Vibrator;
import android.preference.PreferenceManager;
/**
* KerKerInputCore will load all of methods and filters, and
* it can handle the events of physical keyboard and virtual keyboard.
*
* @see OnKeyboardActionListener
*/
public class KerKerInputCore implements OnKeyboardActionListener {
public enum InputMode { MODE_ABC, MODE_SYM, MODE_SYM_ALT, MODE_IME };
private KerKerInputService _frontEnd = null;
private KBManager _kbm = null;
private ArrayList<IKerKerInputMethod> _methods;
private ArrayList<IKerKerInputFilter> _filters;
private IKerKerInputMethod _currentMethod;
private InputMode _currentMode;
private CandidatesViewContainer _candidatesContainer;
private Handler _handler;
private PopupWindow _winMsg;
private TextView _txtvMsg;
private Paint _pntText;
private Boolean shouldVibrate, shouldMakeNoise;
private SoundPool sndPool = null;
private HashMap<Integer, Integer> sndPoolMap = null;
private boolean _candidatesShown;
public KerKerInputCore(KerKerInputService fe)
{
_frontEnd = fe;
_kbm = new KBManager(this);
_methods = new ArrayList<IKerKerInputMethod>();
_filters = new ArrayList<IKerKerInputFilter>();
_currentMethod = null;
_currentMode = InputMode.MODE_ABC;
_handler = new Handler();
_candidatesShown = false;
_pntText = new Paint();
_pntText.setColor(Color.WHITE);
_pntText.setAntiAlias(true);
_pntText.setTextSize(24);
_pntText.setStrokeWidth(0);
setShouldVibrate(false);
setShouldMakeNoise(true);
}
public void initCore()
{
Log.i("KerKerInputCore", "Initializing...");
_winMsg = null;
registerAvailableModules();
}
public void registerAvailableModules() {
IKerKerInputMethod m;
IKerKerInputFilter f;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(_frontEnd);
/**
* FIXME: DYNAMIC LOAD MODULES
*
* I don't have any knowledge of methods to
* dynamically scan the installed packaged
* and get them instantiated here.
*
* So, I'm going to include every package I
* need in the project and explicit load them.
*
*/
if (prefs.getBoolean("enable_bpmf", true))
{
m = new idv.Zero.KerKerInput.Methods.BPMFInput();
_methods.add(m);
}
if (prefs.getBoolean("enable_pinyin", true))
{
m = new idv.Zero.KerKerInput.Methods.PINYINInput();
_methods.add(m);
}
if (prefs.getBoolean("enable_cj5", false))
{
m = new idv.Zero.KerKerInput.Methods.CJInput();
_methods.add(m);
}
if (prefs.getBoolean("enable_barcode", true))
{
m = new idv.Zero.KerKerInput.Methods.BarcodeInput();
_methods.add(m);
}
if (prefs.getBoolean("enable_anti_bpmf", true))
{
f = new idv.Zero.KerKerInput.Filters.AntiPhoneticFilter();
f.onCreateFilter(this);
_filters.add(f);
}
if (prefs.getBoolean("enable_auto_expand", true))
{
f = new idv.Zero.KerKerInput.Filters.AutoExpandFilter();
f.onCreateFilter(this);
_filters.add(f);
}
}
public KerKerInputService getFrontend()
{
return _frontEnd;
}
public LayoutInflater getInflater()
{
return _frontEnd.getLayoutInflater();
}
public View requestCandidatesView()
{
_candidatesContainer = (CandidatesViewContainer)getInflater().inflate(R.layout.candidates_bar, null);
_candidatesContainer.initContainer(this);
return _candidatesContainer;
}
public InputConnection getConnection()
{
return _frontEnd.getCurrentInputConnection();
}
/**
* Change current input method to next one.
* If there are no input method loaded, it will change to first one.
*/
public void requestNextInputMethod()
{
if (_currentMethod == null)
{
setCurrentInputMethod(_methods.get(0));
}
else
{
int curIndex = _methods.indexOf(_currentMethod);
while(true)
{
if (_methods.size() <= curIndex + 1)
curIndex = 0;
else
curIndex++;
if (_methods.get(curIndex).shouldAvailableForSwitchingButton())
{
setCurrentInputMethod(_methods.get(curIndex));
break;
}
}
}
}
/**
* This method will change current input method to what you give.
* And it will initialize all thing about input method.
*
* @param method the method which you want to set up
*/
public void setCurrentInputMethod(IKerKerInputMethod method)
{
if (_currentMethod != null)
_currentMethod.onLeaveInputMethod();
Log.i("KerKerInputCore", "setCurrentInputMethod = " + method);
_currentMethod = method;
_currentMethod.initInputMethod(this);
if (!_currentMethod.hasCustomInputView())
getFrontend().restoreKerKerKeyboardView();
else
getFrontend().setInputView(_currentMethod.onCreateInputView());
_currentMethod.onEnterInputMethod();
_kbm.setCurrentKeyboard(_currentMethod.getDesiredKeyboard());
postShowPopup(_currentMethod.getName());
}
public IKerKerInputMethod getCurrentInputMethod()
{
return _currentMethod;
}
public KBManager getKeyboardManager()
{
return _kbm;
}
/**
* Handle KeyDown event from physical Keyboard input.
* Judging the pressed key and Choosing the solution.
*
* @param keyCode Unicode character of Key
* @param e KeyEvent of the pressed key.
* @return <code>true</code> Finish operation.
* <code>false</code> Let system to handle the rest part.
*/
public boolean onKeyDown(int keyCode, KeyEvent e)
{
// Allow user to user BACK key to hide SIP
if (e.getKeyCode() == KeyEvent.KEYCODE_BACK || e.getKeyCode() == KeyEvent.KEYCODE_SEARCH || e.getKeyCode() == KeyEvent.KEYCODE_MENU ||
e.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN && e.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP && e.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT && e.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT)
return false;
if (e.getKeyCode() == KeyEvent.KEYCODE_ALT_LEFT)
{
if (_currentMode == InputMode.MODE_IME)
{
_currentMethod.commitCurrentComposingBuffer();
_currentMode = InputMode.MODE_ABC;
postShowPopup("英數鍵盤");
}
else
{
_currentMode = InputMode.MODE_IME;
if (_currentMethod == null)
requestNextInputMethod();
_currentMethod.onEnterInputMethod();
postShowPopup(_currentMethod.getName());
}
return true;
}
if (_currentMode == InputMode.MODE_IME && _currentMethod != null && _currentMethod.wantHandleEvent(e.getKeyCode()))
return _currentMethod.onKeyEvent(keyCode, new int[]{keyCode});
return false;
}
public boolean onKeyMultiple(int keyCode, int count, KeyEvent e)
{
return false;
}
// Virtual Keyboard input
public void onKey(int primaryCode, int[] keyCodes) {
if (primaryCode == KBManager.KEYCODE_NEXT_IME)
{
_currentMode = InputMode.MODE_IME;
requestNextInputMethod();
}
else if (primaryCode == KBManager.KEYCODE_DO_OUTPUT_CHARS)
return; // Let IME onText listener handle it.
else if (_currentMode != InputMode.MODE_IME || (_currentMethod != null && !_currentMethod.wantHandleEvent(primaryCode)))
{
// If the IME does not want the event, we assume it's an plain-English keyboard.
switch(primaryCode) {
case Keyboard.KEYCODE_SHIFT: // Shift Key
if (_currentMode == InputMode.MODE_IME)
_currentMethod.commitCurrentComposingBuffer();
Boolean isShifted = !_kbm.getCurrentKeyboard().isShifted();
_kbm.getCurrentKeyboard().setShifted(isShifted);
KeyboardView kv = _kbm.getCurrentKeyboardView();
kv.setShifted(isShifted);
// Dirty Hack :( Force KeyboardView wipe out its buffer...
kv.onSizeChanged(kv.getWidth(), kv.getHeight(), 0, 0);
break;
case KBManager.KEYCODE_SYM: // 123 Keyboard
if (_currentMode == InputMode.MODE_IME)
_currentMethod.commitCurrentComposingBuffer();
_currentMode = InputMode.MODE_SYM;
_kbm.setNativeKeyboard(NativeKeyboardTypes.MODE_SYM);
postShowPopup("123");
hideCandidatesView();
break;
case KBManager.KEYCODE_SYM_ALT: // 123 Keyboard
if (_currentMode == InputMode.MODE_IME)
_currentMethod.commitCurrentComposingBuffer();
_currentMode = InputMode.MODE_SYM_ALT;
_kbm.setNativeKeyboard(NativeKeyboardTypes.MODE_SYM_ALT);
postShowPopup("#+=");
hideCandidatesView();
break;
case KBManager.KEYCODE_ABC: // ABC Keyboard
if (_currentMode == InputMode.MODE_IME)
_currentMethod.commitCurrentComposingBuffer();
_currentMode = InputMode.MODE_ABC;
_kbm.setNativeKeyboard(NativeKeyboardTypes.MODE_ABC);
postShowPopup("ABC");
hideCandidatesView();
break;
case KBManager.KEYCODE_IME: // IME Keyboard
_currentMode = InputMode.MODE_IME;
//showCandidatesView();
if (_currentMethod == null)
requestNextInputMethod();
_currentMethod.onEnterInputMethod();
_kbm.setNativeKeyboard(NativeKeyboardTypes.MODE_IME);
postShowPopup(_currentMethod.getName());
break;
case KBManager.KEYCODE_IME_MENU:
String[] imeNames = new String [_methods.size()];
int i=0;
for(IKerKerInputMethod method : _methods)
imeNames[i++] = method.getName();
final PopupWindow win = new PopupWindow(_frontEnd);
win.setBackgroundDrawable(new ColorDrawable(Color.argb(220, 0, 0, 0)));
win.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
win.setContentView(getInflater().inflate(R.layout.method_chooser, null));
final ListView lstIME = (ListView) win.getContentView().findViewById(R.id.lstIME);
lstIME.setAdapter(new ArrayAdapter<String>(_frontEnd, android.R.layout.simple_list_item_1, imeNames));
win.setWidth(320);
win.setHeight(_kbm.getCurrentKeyboardView().getHeight());
win.setOutsideTouchable(false);
win.setTouchable(true);
win.setTouchInterceptor(new OnTouchListener() {
private boolean detectedMove = false;
private float lastX, lastY;
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP)
{
if (detectedMove)
{
detectedMove = false;
return false;
}
event.offsetLocation(-lstIME.getLeft(), -lstIME.getTop());
Log.i("PopupWindow", "IME selected!");
int pos = lstIME.pointToPosition((int)event.getX(), (int)event.getY());
if (pos > ListView.INVALID_POSITION)
{
_currentMode = InputMode.MODE_IME;
setCurrentInputMethod(_methods.get(pos));
win.dismiss();
}
}
else if (event.getAction() == MotionEvent.ACTION_MOVE && Math.abs(lastX - event.getX()) > 5 && Math.abs(lastY - event.getY()) > 5)
detectedMove = true;
else if (event.getAction() == MotionEvent.ACTION_DOWN)
{
lastX = event.getX();
lastY = event.getY();
}
return false;
}
});
win.showAtLocation(_kbm.getCurrentKeyboardView(), Gravity.BOTTOM, 0, 0);
lstIME.requestFocusFromTouch();
break;
case Keyboard.KEYCODE_DELETE:
getFrontend().sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
for(IKerKerInputFilter f : _filters)
f.onDelete();
break;
default:
if (getKeyboardManager().getCurrentKeyboard().isShifted())
sendKeyChar(Character.toUpperCase((char) primaryCode));
else
sendKeyChar((char) primaryCode);
break;
}
}
else
if (_currentMethod != null) _currentMethod.onKeyEvent(primaryCode, keyCodes);
// User feedbacks
if (shouldMakeNoise)
{
switch(primaryCode)
{
case '\n':
playAudioResource(R.raw.returndown);
playAudioResource(R.raw.returnup);
break;
case ' ':
playAudioResource(R.raw.spacedown);
playAudioResource(R.raw.spaceup);
break;
default:
playAudioResource(R.raw.keydown);
playAudioResource(R.raw.keyup);
}
}
if (shouldVibrate)
((Vibrator)getFrontend().getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
}
public void onText(CharSequence text) {
if (_currentMethod != null)
_currentMethod.onTextEvent(text);
else
commitText(text);
}
public void swipeDown() {
getFrontend().requestHideSelf(0);
}
// Currently we have nothing to do here...
public void onPress(int primaryCode) {}
public void onRelease(int primaryCode) {}
public void swipeLeft() {}
public void swipeRight() {}
public void swipeUp() {}
public void commitCandidate(int selectedCandidate) {
_currentMethod.commitCandidate(selectedCandidate);
}
/**
* Set Candidates list
* @param candidates
*/
public void setCandidates(List<CharSequence> candidates) {
_candidatesContainer.setCandidates(candidates);
}
/**
* Create a new Candidates list to replace original one.
*/
public void clearCandidates() {
_candidatesContainer.setCandidates(new ArrayList<CharSequence>());
}
/**
* Show candidates view (placed above virtual keyboard, or in landscape, the bottom.)
*/
public void showCandidatesView() {
_frontEnd.setCandidatesViewShown(true);
_candidatesShown = true;
}
/**
* Hide & Clear CandidatesView
*/
public void hideCandidatesView() {
if (_currentMode != InputMode.MODE_IME)
{
_frontEnd.setCandidatesViewShown(false);
_candidatesShown = false;
}
if (_candidatesContainer != null)
clearCandidates();
}
/**
* @see InputConnection#setComposingText(CharSequence, int)
* @param buf The composing text
*/
public void setCompositeBuffer(CharSequence buf) {
getConnection().setComposingText(buf, 1);
}
/**
* Shows a short-time popup window to notify users the name of IME
* @see #showPopup
* @param imeName the name of IME
*/
public void postShowPopup(final String imeName) {
_handler.postDelayed(new Runnable(){
public void run() {
try{
showPopup(imeName);
}
catch(Exception e)
{
e.printStackTrace();
}
}
}, 1000);
}
/**
* Re-create popup window if _winMsg is null. Then,
* change its contents to the message to notify users,
* and hide it after one second.
*
* @param resid Resources ID
*/
public void showPopup(int resid)
{
showPopup(getFrontend().getResources().getString(resid));
}
/**
* Re-create popup window if _winMsg is null. Then,
* change its contents to the message to notify users,
* and hide it after one second.
*
* @param msg the string to display
*/
public void showPopup(final CharSequence msg)
{
if (!_frontEnd.isInputViewShown() && !_candidatesShown)
return;
if (_winMsg == null)
{
Log.i("KerKerInputCore", "Re-create popup window");
_winMsg = new PopupWindow(_frontEnd);
_winMsg.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
_winMsg.setBackgroundDrawable(null);
_txtvMsg = (TextView) getInflater().inflate(R.layout.candidates_preview, null);
_winMsg.setContentView(_txtvMsg);
}
try
{
View baseView = (_frontEnd.isInputViewShown() ? _kbm.getCurrentKeyboardView() : _candidatesContainer);
_txtvMsg.setText(msg);
_txtvMsg.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int wordWidth = (int)(_pntText.measureText(msg.toString()));
int popupWidth = wordWidth + _txtvMsg.getPaddingLeft() + _txtvMsg.getPaddingRight();
int popupHeight = _txtvMsg.getMeasuredHeight();
int popupX = (baseView.getWidth() - popupWidth) / 2;
int popupY = -popupHeight;
int[] offset = new int[2];
baseView.getLocationInWindow(offset);
popupY += offset[1];
if (_winMsg.isShowing())
_winMsg.update(popupX, popupY, popupWidth, popupHeight);
else
{
_winMsg.setWidth(popupWidth);
_winMsg.setHeight(popupHeight);
_winMsg.showAtLocation(baseView, Gravity.NO_GRAVITY, popupX, popupY);
}
_txtvMsg.setVisibility(View.VISIBLE);
_handler.postDelayed(new Runnable() {
public void run() {
try {
if (_txtvMsg.getText() == msg.toString())
hidePopup();
}
catch(Exception ex)
{
}
}
}, 1000);
}
catch(Exception e)
{
e.printStackTrace();
Log.e("KerKerInputCore", "Show popup failed, force reset popup window.");
_winMsg = null;
_txtvMsg = null;
}
}
/**
* @see PopupWindow#dismiss()
*/
private void hidePopup()
{
try
{
_winMsg.dismiss();
}
catch(Exception e)
{}
}
/**
* Judging the sent charcode and Calling proper method
*
* @param charCode The Unicode character to send
*/
public void sendKeyChar(char charCode) {
switch (charCode) {
case '\n':
getFrontend().sendKeyChar(charCode);
break;
default:
if (charCode >= '0' && charCode <= '9')
{
getFrontend().sendKeyChar(charCode);
runThroughFilters(Character.valueOf(charCode).toString());
}
else
commitText(Character.valueOf(charCode).toString());
break;
}
}
/**
* Commit text to screen through filters
* @param str
*/
public void commitText(CharSequence str) {
str = runThroughFilters(str.toString());
hideCandidatesView();
if (str.toString().equalsIgnoreCase(""))
{
setCompositeBuffer("");
return;
}
getConnection().commitText(str, 1);
}
/**
* Given string will run through filter to decide
* its result(like expending or being deleted)
*
* @param str the string to filter
* @return the string after filtering
*/
private String runThroughFilters(String str)
{
for(IKerKerInputFilter filter : _filters)
{
str = filter.onTextCommit(str.toString());
if (str == null || str.toString().equalsIgnoreCase(""))
return "";
}
return str;
}
/**
* Create SoundPool if activate this function, or release all sound resources
*
* @param shouldMakeNoise
*/
public void setShouldMakeNoise(Boolean shouldMakeNoise) {
this.shouldMakeNoise = shouldMakeNoise;
if (shouldMakeNoise)
{
sndPool = new SoundPool(6, AudioManager.STREAM_RING, 0);
sndPoolMap = null;
}
else
{
if (sndPool != null)
sndPool.release();
sndPool = null;
sndPoolMap = null;
}
}
public Boolean shouldMakeNoise() {
return shouldMakeNoise;
}
public void setShouldVibrate(Boolean shouldVibrate) {
this.shouldVibrate = shouldVibrate;
}
public Boolean shouldVibrate() {
return shouldVibrate;
}
public void initSounds()
{
if (sndPoolMap != null)
return;
sndPoolMap = new HashMap<Integer, Integer>();
sndPoolMap.put(R.raw.keydown, sndPool.load(getFrontend().getResources().openRawResourceFd(R.raw.keydown), 1));
sndPoolMap.put(R.raw.keyup, sndPool.load(getFrontend().getResources().openRawResourceFd(R.raw.keyup), 1));
sndPoolMap.put(R.raw.spacedown, sndPool.load(getFrontend().getResources().openRawResourceFd(R.raw.spacedown), 1));
sndPoolMap.put(R.raw.spaceup, sndPool.load(getFrontend().getResources().openRawResourceFd(R.raw.spaceup), 1));
sndPoolMap.put(R.raw.returndown, sndPool.load(getFrontend().getResources().openRawResourceFd(R.raw.returndown), 1));
sndPoolMap.put(R.raw.returnup, sndPool.load(getFrontend().getResources().openRawResourceFd(R.raw.returnup), 1));
}
public void releaseSounds()
{
if (sndPoolMap != null)
{
sndPoolMap.clear();
sndPoolMap = null;
}
if (sndPool != null)
{
sndPool.release();
sndPool = null;
}
}
/**
* Play Audio by the ID of resource. Initialize Audio resource if it didn't exist.
*
* @param resourceID the audio resource id to play
*/
private void playAudioResource(final int resourceID)
{
if (sndPool == null || sndPoolMap == null)
initSounds();
int volume = ((AudioManager)getFrontend().getSystemService(Context.AUDIO_SERVICE)).getStreamVolume(AudioManager.STREAM_RING);
Integer rid = sndPoolMap.get(resourceID);
if (rid != null)
sndPool.play(rid, volume, volume, 1, 0, 1f);
}
public void setCurrentMode(InputMode mode)
{
_currentMode = mode;
}
public Handler getHandler() { return _handler; }
}