/*
* Copyright (C) 2016 The Android Open Source Project
*
* 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.android.inputmethod.latin;
import android.car.Car;
import android.car.CarNotConnectedException;
import android.car.drivingstate.CarUxRestrictions;
import android.car.drivingstate.CarUxRestrictionsManager;
import android.content.ComponentName;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.Keyboard;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.FrameLayout;
import com.android.inputmethod.latin.car.KeyboardView;
import java.lang.ref.WeakReference;
import java.util.Locale;
import javax.annotation.concurrent.GuardedBy;
/**
* IME for car use case. 2 features are added compared to the original IME.
*
* - Monitor driving status, and put a lockout screen on top of the current keyboard if
* keyboard input is not allowed.
*
- Add a close keyboard button so that user dismiss the keyboard when "back" button is not
* present in the system navigation bar.
*
*/
public class CarLatinIME extends InputMethodService {
private static final String TAG = "CarLatinIME";
private static final String DEFAULT_LANGUAGE = "en";
private static final String LAYOUT_XML = "input_keyboard_layout";
private static final String SYMBOL_LAYOUT_XML = "input_keyboard_layout_symbol";
private static final int KEYCODE_ENTER = '\n';
private static final int IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1;
private static final int MSG_ENABLE_KEYBOARD = 0;
private static final int KEYCODE_CYCLE_CHAR = -7;
private static final int KEYCODE_MAIN_KEYBOARD = -8;
private static final int KEYCODE_NUM_KEYBOARD = -9;
private static final int KEYCODE_ALPHA_KEYBOARD = -10;
private static final int KEYCODE_CLOSE_KEYBOARD = -99;
private Keyboard mQweKeyboard;
private Keyboard mSymbolKeyboard;
private Car mCar;
private CarUxRestrictionsManager mUxRManager;
private View mLockoutView;
private KeyboardView mPopupKeyboardView;
@GuardedBy("this")
private boolean mKeyboardEnabled = true;
private KeyboardView mKeyboardView;
private Locale mLocale;
private final Handler mHandler;
private FrameLayout mKeyboardWrapper;
private EditorInfo mEditorInfo;
private static final class HideKeyboardHandler extends Handler {
private final WeakReference mIME;
public HideKeyboardHandler(CarLatinIME ime) {
mIME = new WeakReference(ime);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ENABLE_KEYBOARD:
if (mIME.get() != null) {
mIME.get().updateKeyboardState(msg.arg1 == 1);
}
break;
}
}
}
private final ServiceConnection mCarConnectionListener =
new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "Car Service connected");
try {
mUxRManager = (CarUxRestrictionsManager) mCar.getCarManager(
Car.CAR_UX_RESTRICTION_SERVICE);
if (mUxRManager != null) {
mUxRManager.registerListener(mCarUxRListener);
} else {
Log.e(TAG, "CarUxRestrictions service not available");
}
} catch (CarNotConnectedException e) {
Log.e(TAG, "car not connected", e);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.e(TAG, "CarService: onServiceDisconnedted " + name);
}
};
private final CarUxRestrictionsManager.OnUxRestrictionsChangedListener mCarUxRListener =
new CarUxRestrictionsManager.OnUxRestrictionsChangedListener() {
@Override
public void onUxRestrictionsChanged(CarUxRestrictions restrictions) {
if (restrictions == null) {
return;
}
boolean keyboardEnabled =
(restrictions.getActiveRestrictions()
& CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD) == 0;
mHandler.sendMessage(mHandler.obtainMessage(
MSG_ENABLE_KEYBOARD, keyboardEnabled ? 1 : 0, 0, null));
}
};
public CarLatinIME() {
super();
mHandler = new HideKeyboardHandler(this);
}
@Override
public void onCreate() {
super.onCreate();
mCar = Car.createCar(this, mCarConnectionListener);
mCar.connect();
mQweKeyboard = createKeyboard(LAYOUT_XML);
mSymbolKeyboard = createKeyboard(SYMBOL_LAYOUT_XML);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mCar != null) {
mCar.disconnect();
}
}
@Override
public View onCreateInputView() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCreateInputView");
}
super.onCreateInputView();
View v = LayoutInflater.from(this).inflate(R.layout.input_keyboard, null);
mKeyboardView = (KeyboardView) v.findViewById(R.id.keyboard);
mLockoutView = v.findViewById(R.id.lockout);
mPopupKeyboardView = (KeyboardView) v.findViewById(R.id.popup_keyboard);
mKeyboardView.setPopupKeyboardView(mPopupKeyboardView);
mKeyboardWrapper = (FrameLayout) v.findViewById(R.id.keyboard_wrapper);
mLockoutView.setBackgroundResource(R.color.ime_background_letters);
synchronized (this) {
updateKeyboardStateLocked();
}
return v;
}
@Override
public void onStartInputView(EditorInfo editorInfo, boolean reastarting) {
super.onStartInputView(editorInfo, reastarting);
mEditorInfo = editorInfo;
mKeyboardView.setKeyboard(mQweKeyboard, getLocale());
mKeyboardWrapper.setPadding(0,
getResources().getDimensionPixelSize(R.dimen.keyboard_padding_vertical), 0, 0);
mKeyboardView.setOnKeyboardActionListener(mKeyboardActionListener);
mPopupKeyboardView.setOnKeyboardActionListener(mPopupKeyboardActionListener);
mKeyboardView.setShifted(mEditorInfo.initialCapsMode != 0);
}
public Locale getLocale() {
if (mLocale == null) {
mLocale = this.getResources().getConfiguration().locale;
}
return mLocale;
}
@Override
public boolean onEvaluateFullscreenMode() {
return false;
}
private Keyboard createKeyboard(String layoutXml) {
Resources res = this.getResources();
Configuration configuration = res.getConfiguration();
Locale oldLocale = configuration.locale;
configuration.locale = new Locale(DEFAULT_LANGUAGE);
res.updateConfiguration(configuration, res.getDisplayMetrics());
Keyboard ret = new Keyboard(
this, res.getIdentifier(layoutXml, "xml", getPackageName()));
mLocale = configuration.locale;
configuration.locale = oldLocale;
return ret;
}
public void updateKeyboardState(boolean enabled) {
synchronized (this) {
mKeyboardEnabled = enabled;
updateKeyboardStateLocked();
}
}
private void updateKeyboardStateLocked() {
if (mLockoutView == null) {
return;
}
mLockoutView.setVisibility(mKeyboardEnabled ? View.GONE : View.VISIBLE);
}
private void toggleCapitalization() {
mKeyboardView.setShifted(!mKeyboardView.isShifted());
}
private void updateCapitalization() {
boolean shouldCapitalize = false;
if (getCurrentInputConnection() != null) {
shouldCapitalize =
getCurrentInputConnection().getCursorCapsMode(mEditorInfo.inputType) != 0;
}
mKeyboardView.setShifted(shouldCapitalize);
}
private final KeyboardView.OnKeyboardActionListener mKeyboardActionListener =
new KeyboardView.OnKeyboardActionListener() {
@Override
public void onPress(int primaryCode) {
}
@Override
public void onRelease(int primaryCode) {
}
@Override
public void onKey(int primaryCode, int[] keyCodes) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onKey " + primaryCode);
}
InputConnection inputConnection = getCurrentInputConnection();
switch (primaryCode) {
case Keyboard.KEYCODE_SHIFT:
toggleCapitalization();
break;
case Keyboard.KEYCODE_MODE_CHANGE:
if (mKeyboardView.getKeyboard() == mQweKeyboard) {
mKeyboardView.setKeyboard(mSymbolKeyboard, getLocale());
} else {
mKeyboardView.setKeyboard(mQweKeyboard, getLocale());
}
break;
case Keyboard.KEYCODE_DONE:
int action = mEditorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
inputConnection.performEditorAction(action);
break;
case Keyboard.KEYCODE_DELETE:
inputConnection.deleteSurroundingText(1, 0);
updateCapitalization();
break;
case KEYCODE_MAIN_KEYBOARD:
mKeyboardView.setKeyboard(mQweKeyboard, getLocale());
break;
case KEYCODE_NUM_KEYBOARD:
// No number keyboard layout support.
break;
case KEYCODE_ALPHA_KEYBOARD:
//loadKeyboard(ALPHA_LAYOUT_XML);
break;
case KEYCODE_CLOSE_KEYBOARD:
hideWindow();
break;
case KEYCODE_CYCLE_CHAR:
CharSequence text = inputConnection.getTextBeforeCursor(1, 0);
if (TextUtils.isEmpty(text)) {
break;
}
char currChar = text.charAt(0);
char altChar = cycleCharacter(currChar);
// Don't modify text if there is no alternate.
if (currChar != altChar) {
inputConnection.deleteSurroundingText(1, 0);
inputConnection.commitText(String.valueOf(altChar), 1);
}
break;
case KEYCODE_ENTER:
final int imeOptionsActionId = getImeOptionsActionIdFromEditorInfo(
mEditorInfo);
if (IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
// Either we have an actionLabel and we should
// performEditorAction with
// actionId regardless of its value.
inputConnection.performEditorAction(mEditorInfo.actionId);
} else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
// We didn't have an actionLabel, but we had another action to
// execute.
// EditorInfo.IME_ACTION_NONE explicitly means no action. In
// contrast,
// EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an
// action, so it
// means there should be an action and the app didn't bother to
// set a specific
// code for it - presumably it only handles one. It does not have
// to be treated
// in any specific way: anything that is not IME_ACTION_NONE
// should be sent to
// performEditorAction.
inputConnection.performEditorAction(imeOptionsActionId);
} else {
// No action label, and the action from imeOptions is NONE: this
// is a regular
// enter key that should input a carriage return.
String txt = Character.toString((char) primaryCode);
if (mKeyboardView.isShifted()) {
txt = txt.toUpperCase(mLocale);
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "commitText " + txt);
}
inputConnection.commitText(txt, 1);
updateCapitalization();
}
break;
default:
String commitText = Character.toString((char) primaryCode);
// Chars always come through as lowercase, so we have to explicitly
// uppercase them if the keyboard is shifted.
if (mKeyboardView.isShifted()) {
commitText = commitText.toUpperCase(mLocale);
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "commitText " + commitText);
}
inputConnection.commitText(commitText, 1);
updateCapitalization();
}
}
@Override
public void onText(CharSequence text) {
}
@Override
public void swipeLeft() {
}
@Override
public void swipeRight() {
}
@Override
public void swipeDown() {
}
@Override
public void swipeUp() {
}
@Override
public void stopInput() {
hideWindow();
}
};
private final KeyboardView.OnKeyboardActionListener mPopupKeyboardActionListener =
new KeyboardView.OnKeyboardActionListener() {
@Override
public void onPress(int primaryCode) {
}
@Override
public void onRelease(int primaryCode) {
}
@Override
public void onKey(int primaryCode, int[] keyCodes) {
InputConnection inputConnection = getCurrentInputConnection();
String commitText = Character.toString((char) primaryCode);
// Chars always come through as lowercase, so we have to explicitly
// uppercase them if the keyboard is shifted.
if (mKeyboardView.isShifted()) {
commitText = commitText.toUpperCase(mLocale);
}
inputConnection.commitText(commitText, 1);
updateCapitalization();
mKeyboardView.dismissPopupKeyboard();
}
@Override
public void onText(CharSequence text) {
}
@Override
public void swipeLeft() {
}
@Override
public void swipeRight() {
}
@Override
public void swipeDown() {
}
@Override
public void swipeUp() {
}
@Override
public void stopInput() {
hideWindow();
}
};
/**
* Cycle through alternate characters of the given character. Return the same character if
* there is no alternate.
*/
private char cycleCharacter(char current) {
if (Character.isUpperCase(current)) {
return String.valueOf(current).toLowerCase(mLocale).charAt(0);
} else {
return String.valueOf(current).toUpperCase(mLocale).charAt(0);
}
}
private int getImeOptionsActionIdFromEditorInfo(final EditorInfo editorInfo) {
if ((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
return EditorInfo.IME_ACTION_NONE;
} else if (editorInfo.actionLabel != null) {
return IME_ACTION_CUSTOM_LABEL;
} else {
// Note: this is different from editorInfo.actionId, hence "ImeOptionsActionId"
return editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
}
}
}