package ee.ioc.phon.android.speak.service;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.speech.SpeechRecognizer;
import android.text.InputType;
import android.view.View;
import android.view.Window;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import java.util.List;
import ee.ioc.phon.android.speak.Log;
import ee.ioc.phon.android.speak.R;
import ee.ioc.phon.android.speak.activity.PermissionsRequesterActivity;
import ee.ioc.phon.android.speak.model.CallerInfo;
import ee.ioc.phon.android.speak.utils.Utils;
import ee.ioc.phon.android.speak.view.AbstractSpeechInputViewListener;
import ee.ioc.phon.android.speak.view.SpeechInputView;
import ee.ioc.phon.android.speechutils.Extras;
import ee.ioc.phon.android.speechutils.editor.CommandEditor;
import ee.ioc.phon.android.speechutils.editor.CommandEditorResult;
import ee.ioc.phon.android.speechutils.editor.InputConnectionCommandEditor;
import ee.ioc.phon.android.speechutils.utils.PreferenceUtils;
public class SpeechInputMethodService extends InputMethodService {
private InputMethodManager mInputMethodManager;
private SpeechInputView mInputView;
private CommandEditor mCommandEditor;
private boolean mShowPartialResults;
private SharedPreferences mPrefs;
private Resources mRes;
@Override
public void onCreate() {
super.onCreate();
Log.i("onCreate");
mInputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
mCommandEditor = new InputConnectionCommandEditor(getApplicationContext());
}
/**
* This is called at configuration change. We just kill the running session.
* TODO: better handle configuration changes
*/
@Override
public void onInitializeInterface() {
Log.i("onInitializeInterface");
closeSession();
}
@Override
public View onCreateInputView() {
Log.i("onCreateInputView");
mInputView = (SpeechInputView) getLayoutInflater().inflate(R.layout.voice_ime_view, null, false);
return mInputView;
}
/**
* We check the type of editor control and if we probably cannot handle it (e.g. dates)
* or do not want to (e.g. passwords) then we hand the editing over to another keyboard.
* TODO: handle inputType = 0
*/
@Override
public void onStartInput(EditorInfo attribute, boolean restarting) {
super.onStartInput(attribute, restarting);
String type = "UNKNOWN";
switch (attribute.inputType & InputType.TYPE_MASK_CLASS) {
case InputType.TYPE_CLASS_NUMBER:
type = "NUMBER";
break;
case InputType.TYPE_CLASS_DATETIME:
type = "DATETIME";
switchToLastIme();
break;
case InputType.TYPE_CLASS_PHONE:
type = "PHONE";
break;
case InputType.TYPE_CLASS_TEXT:
int variation = attribute.inputType & InputType.TYPE_MASK_VARIATION;
type = "TEXT/";
if (variation == InputType.TYPE_TEXT_VARIATION_PASSWORD ||
variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) {
// We refuse to recognize passwords for privacy reasons.
type += "PASSWORD || VISIBLE_PASSWORD";
switchToLastIme();
} else if (variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) {
type += "EMAIL_ADDRESS";
} else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
// URI bar of Chrome and Firefox, can also handle search queries, thus supported
type += "URI";
} else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
// List filtering? Used in the Dialer search bar, thus supported
type += "FILTER";
} else {
type += variation;
}
// This is used in the standard search bar (e.g. in Google Play).
if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
type += "FLAG_AUTO_COMPLETE";
}
break;
default:
}
Log.i("onStartInput: " + type + ", " + attribute.inputType + ", " + attribute.imeOptions + ", " + restarting);
}
/**
* Note that when editing a HTML page, then switching between form fields might fail to call
* this method with restarting=false, we thus always update the editor info (incl. inputType).
*/
@Override
public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
super.onStartInputView(editorInfo, restarting);
Log.i("onStartInputView: " + editorInfo.inputType + "/" + editorInfo.imeOptions + "/" + restarting);
((InputConnectionCommandEditor) mCommandEditor).setInputConnection(getCurrentInputConnection());
mPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
mRes = getResources();
mInputView.init(R.array.keysIme, new CallerInfo(makeExtras(mPrefs, mRes), editorInfo, getPackageName()));
// TODO: update this less often (in onStart)
closeSession();
if (restarting) {
return;
}
mInputView.setListener(getSpeechInputViewListener(editorInfo.packageName));
mShowPartialResults = PreferenceUtils.getPrefBoolean(mPrefs, mRes, R.string.keyImeShowPartialResults, R.bool.defaultImeShowPartialResults);
// Launch recognition immediately (if set so)
if (PreferenceUtils.getPrefBoolean(mPrefs, mRes, R.string.keyImeAutoStart, R.bool.defaultImeAutoStart)) {
Log.i("Auto-starting");
mInputView.start();
}
}
/**
* Called when the input view is being hidden from the user.
* This will be called either prior to hiding the window,
* or prior to switching to another target for editing.
*
* @param finishingInput If true, onFinishInput() will be called immediately after.
*/
@Override
public void onFinishInputView(boolean finishingInput) {
// TODO: maybe do not call super
super.onFinishInputView(finishingInput);
Log.i("onFinishInputView: " + finishingInput);
if (!finishingInput) {
closeSession();
}
}
/**
* Called to inform the input method that text input has finished in the last editor.
* At this point there may be a call to onStartInput(EditorInfo, boolean) to perform input in a new editor,
* or the input method may be left idle.
* This method is not called when input restarts in the same editor.
*/
@Override
public void onFinishInput() {
// TODO: maybe do not call super
super.onFinishInput();
Log.i("onFinishInput");
closeSession();
}
@Override
public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) {
Log.i("onCurrentInputMethodSubtypeChanged: " + subtype + ": " + subtype.getExtraValue());
closeSession();
}
private void closeSession() {
if (mInputView != null) {
mInputView.cancel();
}
}
private IBinder getToken() {
final Dialog dialog = getWindow();
if (dialog == null) {
return null;
}
final Window window = dialog.getWindow();
if (window == null) {
return null;
}
return window.getAttributes().token;
}
/**
* Switch to another IME by selecting it from the list of all active IMEs (isAskUser==true), or
* by taking the next IME in the IME rotation (isAskUser==false on JELLY_BEAN).
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void switchIme(boolean isAskUser) {
closeSession();
if (isAskUser) {
mInputMethodManager.showInputMethodPicker();
} else {
final IBinder token = getToken();
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mInputMethodManager.switchToNextInputMethod(token, false /* not onlyCurrentIme */);
} else {
mInputMethodManager.switchToLastInputMethod(token);
}
} catch (NoSuchMethodError e) {
Log.e("IME switch failed", e);
}
}
}
/**
* Switch to the previous IME, either when the user tries to edit an unsupported field (e.g. password),
* or when they explicitly want to be taken back to the previous IME e.g. in case of a one-shot
* speech input.
*/
private void switchToLastIme() {
closeSession();
mInputMethodManager.switchToLastInputMethod(getToken());
}
private static String getText(List<String> results) {
if (results.size() > 0) {
return results.get(0);
}
return "";
}
private static Bundle makeExtras(SharedPreferences prefs, Resources res) {
Bundle extras = new Bundle();
boolean isUnlimitedDuration = !PreferenceUtils.getPrefBoolean(prefs, res,
R.string.keyImeAutoStopAfterPause, R.bool.defaultImeAutoStopAfterPause);
extras.putBoolean(Extras.EXTRA_UNLIMITED_DURATION, isUnlimitedDuration);
extras.putBoolean(Extras.EXTRA_DICTATION_MODE, isUnlimitedDuration);
return extras;
}
private SpeechInputView.SpeechInputViewListener getSpeechInputViewListener(final String packageName) {
return new AbstractSpeechInputViewListener() {
// TODO: quick hack to add app to the matcher, not sure if we can access the
// class name of the app
private ComponentName app = new ComponentName(packageName, packageName);
@Override
public void onComboChange(String language, ComponentName service) {
// TODO: name of the rewrites table configurable
mCommandEditor.setRewriters(Utils.makeList(Utils.genRewriters(mPrefs, mRes, null, language, service, app)));
}
@Override
public void onPartialResult(List<String> results) {
if (mShowPartialResults) {
mCommandEditor.commitPartialResult(getText(results));
}
}
@Override
public void onFinalResult(List<String> results, Bundle bundle) {
CommandEditorResult editorResult = mCommandEditor.commitFinalResult(getText(results));
if (editorResult != null && mInputView != null && editorResult.isCommand()) {
mInputView.showMessage(editorResult.ppCommand(), editorResult.isSuccess());
}
}
@Override
public void onSwitchIme(boolean isAskUser) {
switchIme(isAskUser);
}
@Override
public void onSwitchToLastIme() {
switchToLastIme();
}
@Override
public void onSearch() {
closeSession();
mCommandEditor.runOp(mCommandEditor.imeActionSearch());
requestHideSelf(0);
}
@Override
public void onDeleteLastWord() {
mCommandEditor.runOp(mCommandEditor.deleteLeftWord());
}
@Override
public void onAddNewline() {
mCommandEditor.runOp(mCommandEditor.replaceSel("\n"));
}
@Override
public void goUp() {
mCommandEditor.runOp(mCommandEditor.keyUp());
}
@Override
public void goDown() {
mCommandEditor.runOp(mCommandEditor.keyDown());
}
@Override
public void onAddSpace() {
mCommandEditor.runOp(mCommandEditor.replaceSel(" "));
}
@Override
public void onSelectAll() {
// TODO: show ContextMenu
mCommandEditor.runOp(mCommandEditor.selectAll());
}
@Override
public void onReset() {
// TODO: hide ContextMenu (if visible)
mCommandEditor.runOp(mCommandEditor.moveRel(0));
}
@Override
public void onStartListening() {
Log.i("IME: onStartListening");
mCommandEditor.reset();
}
@Override
public void onStopListening() {
Log.i("IME: onStopListening");
}
// TODO: add onCancel()
@Override
public void onError(int errorCode) {
Log.i("IME: onError: " + errorCode);
if (errorCode == SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS) {
Intent intent = new Intent(SpeechInputMethodService.this, PermissionsRequesterActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
SpeechInputMethodService.this.startActivity(intent);
}
}
};
}
}