Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.inputmethod.latin;
     17 
     18 import android.car.CarNotConnectedException;
     19 import android.car.hardware.CarSensorEvent;
     20 import android.car.hardware.CarSensorManager;
     21 import android.content.ComponentName;
     22 import android.content.ServiceConnection;
     23 import android.content.res.Configuration;
     24 import android.content.res.Resources;
     25 import android.inputmethodservice.InputMethodService;
     26 import android.inputmethodservice.Keyboard;
     27 import android.os.Handler;
     28 import android.os.IBinder;
     29 import android.os.Message;
     30 import android.car.Car;
     31 
     32 import android.text.TextUtils;
     33 import android.util.Log;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.inputmethod.EditorInfo;
     37 import android.view.inputmethod.InputConnection;
     38 import android.widget.FrameLayout;
     39 
     40 import com.android.inputmethod.latin.car.KeyboardView;
     41 
     42 import java.lang.ref.WeakReference;
     43 import java.util.Locale;
     44 
     45 import javax.annotation.concurrent.GuardedBy;
     46 
     47 /**
     48  * IME for car use case. 2 features are added compared to the original IME.
     49  * <ul>
     50  *     <li> Monitor driving status, and put a lockout screen on top of the current keyboard if
     51  *          keyboard input is not allowed.
     52  *     <li> Add a close keyboard button so that user dismiss the keyboard when "back" button is not
     53  *          present in the system navigation bar.
     54  * </ul>
     55  */
     56 public class CarLatinIME extends InputMethodService {
     57     private static final String TAG = "CarLatinIME";
     58     private static final String DEFAULT_LANGUAGE = "en";
     59     private static final String LAYOUT_XML = "input_keyboard_layout";
     60     private static final String SYMBOL_LAYOUT_XML = "input_keyboard_layout_symbol";
     61 
     62     private static final int KEYCODE_ENTER = '\n';
     63     private static final int IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1;
     64     private static final int MSG_ENABLE_KEYBOARD = 0;
     65     private static final int KEYCODE_CYCLE_CHAR = -7;
     66     private static final int KEYCODE_MAIN_KEYBOARD = -8;
     67     private static final int KEYCODE_NUM_KEYBOARD = -9;
     68     private static final int KEYCODE_ALPHA_KEYBOARD = -10;
     69     private static final int KEYCODE_CLOSE_KEYBOARD = -99;
     70 
     71     private Keyboard mQweKeyboard;
     72     private Keyboard mSymbolKeyboard;
     73     private Car mCar;
     74     private CarSensorManager mSensorManager;
     75 
     76     private View mLockoutView;
     77     private KeyboardView mPopupKeyboardView;
     78 
     79     @GuardedBy("this")
     80     private boolean mKeyboardEnabled = true;
     81     private KeyboardView mKeyboardView;
     82     private Locale mLocale;
     83     private final Handler mHandler;
     84 
     85     private FrameLayout mKeyboardWrapper;
     86     private EditorInfo mEditorInfo;
     87 
     88     private static final class HideKeyboardHandler extends Handler {
     89         private final WeakReference<CarLatinIME> mIME;
     90         public HideKeyboardHandler(CarLatinIME ime) {
     91             mIME = new WeakReference<CarLatinIME>(ime);
     92         }
     93         @Override
     94         public void handleMessage(Message msg) {
     95             switch (msg.what) {
     96                 case MSG_ENABLE_KEYBOARD:
     97                     if (mIME.get() != null) {
     98                         mIME.get().updateKeyboardState(msg.arg1 == 1);
     99                     }
    100                     break;
    101             }
    102         }
    103     }
    104 
    105     private final ServiceConnection mCarConnectionListener =
    106             new ServiceConnection() {
    107                 public void onServiceConnected(ComponentName name, IBinder service) {
    108                     Log.d(TAG, "Car Service connected");
    109                     try {
    110                         mSensorManager = (CarSensorManager) mCar.getCarManager(Car.SENSOR_SERVICE);
    111                         mSensorManager.registerListener(mCarSensorListener,
    112                                 CarSensorManager.SENSOR_TYPE_DRIVING_STATUS,
    113                                 CarSensorManager.SENSOR_RATE_FASTEST);
    114                     } catch (CarNotConnectedException e) {
    115                         Log.e(TAG, "car not connected", e);
    116                     }
    117                 }
    118 
    119                 @Override
    120                 public void onServiceDisconnected(ComponentName name) {
    121                     Log.e(TAG, "CarService: onServiceDisconnedted " + name);
    122                 }
    123             };
    124 
    125     private final CarSensorManager.OnSensorChangedListener mCarSensorListener =
    126             new CarSensorManager.OnSensorChangedListener() {
    127                 @Override
    128                 public void onSensorChanged(CarSensorEvent event) {
    129                     if (event.sensorType != CarSensorManager.SENSOR_TYPE_DRIVING_STATUS) {
    130                         return;
    131                     }
    132                     int drivingStatus = event.getDrivingStatusData(null).status;
    133 
    134                     boolean keyboardEnabled =
    135                             (drivingStatus & CarSensorEvent.DRIVE_STATUS_NO_KEYBOARD_INPUT) == 0;
    136                     mHandler.sendMessage(mHandler.obtainMessage(
    137                             MSG_ENABLE_KEYBOARD, keyboardEnabled ? 1 : 0, 0, null));
    138                 }
    139             };
    140 
    141     public CarLatinIME() {
    142         super();
    143         mHandler = new HideKeyboardHandler(this);
    144     }
    145 
    146     @Override
    147     public void onCreate() {
    148         super.onCreate();
    149         mCar = Car.createCar(this, mCarConnectionListener);
    150         mCar.connect();
    151 
    152         mQweKeyboard = createKeyboard(LAYOUT_XML);
    153         mSymbolKeyboard = createKeyboard(SYMBOL_LAYOUT_XML);
    154     }
    155 
    156     @Override
    157     public void onDestroy() {
    158         super.onDestroy();
    159         if (mCar != null) {
    160             mCar.disconnect();
    161         }
    162     }
    163 
    164     @Override
    165     public View onCreateInputView() {
    166         if (Log.isLoggable(TAG, Log.DEBUG)) {
    167             Log.d(TAG, "onCreateInputView");
    168         }
    169         super.onCreateInputView();
    170 
    171         View v = LayoutInflater.from(this).inflate(R.layout.input_keyboard, null);
    172         mKeyboardView = (KeyboardView) v.findViewById(R.id.keyboard);
    173 
    174         mLockoutView = v.findViewById(R.id.lockout);
    175         mPopupKeyboardView = (KeyboardView) v.findViewById(R.id.popup_keyboard);
    176         mKeyboardView.setPopupKeyboardView(mPopupKeyboardView);
    177         mKeyboardWrapper = (FrameLayout) v.findViewById(R.id.keyboard_wrapper);
    178         mLockoutView.setBackgroundResource(R.color.ime_background_letters);
    179 
    180         synchronized (this) {
    181             updateKeyboardStateLocked();
    182         }
    183         return v;
    184     }
    185 
    186 
    187 
    188     @Override
    189     public void onStartInputView(EditorInfo editorInfo, boolean reastarting) {
    190         super.onStartInputView(editorInfo, reastarting);
    191         mEditorInfo = editorInfo;
    192         mKeyboardView.setKeyboard(mQweKeyboard, getLocale());
    193         mKeyboardWrapper.setPadding(0,
    194                 getResources().getDimensionPixelSize(R.dimen.keyboard_padding_vertical), 0, 0);
    195         mKeyboardView.setOnKeyboardActionListener(mKeyboardActionListener);
    196         mPopupKeyboardView.setOnKeyboardActionListener(mPopupKeyboardActionListener);
    197         mKeyboardView.setShifted(mKeyboardView.isShifted());
    198         updateCapitalization();
    199     }
    200 
    201     public Locale getLocale() {
    202         if (mLocale == null) {
    203             mLocale = this.getResources().getConfiguration().locale;
    204         }
    205         return mLocale;
    206     }
    207 
    208     @Override
    209     public boolean onEvaluateFullscreenMode() {
    210         return false;
    211     }
    212 
    213     private Keyboard createKeyboard(String layoutXml) {
    214         Resources res = this.getResources();
    215         Configuration configuration = res.getConfiguration();
    216         Locale oldLocale = configuration.locale;
    217         configuration.locale = new Locale(DEFAULT_LANGUAGE);
    218         res.updateConfiguration(configuration, res.getDisplayMetrics());
    219         Keyboard ret = new Keyboard(
    220                 this, res.getIdentifier(layoutXml, "xml", getPackageName()));
    221         mLocale = configuration.locale;
    222         configuration.locale = oldLocale;
    223         return ret;
    224     }
    225 
    226     public void updateKeyboardState(boolean enabled) {
    227         synchronized (this) {
    228             mKeyboardEnabled = enabled;
    229             updateKeyboardStateLocked();
    230         }
    231     }
    232 
    233     private void updateKeyboardStateLocked() {
    234         if (mLockoutView == null) {
    235             return;
    236         }
    237         mLockoutView.setVisibility(mKeyboardEnabled ? View.GONE : View.VISIBLE);
    238     }
    239 
    240     private void toggleCapitalization() {
    241         mKeyboardView.setShifted(!mKeyboardView.isShifted());
    242     }
    243 
    244     private void updateCapitalization() {
    245         boolean shouldCapitalize =
    246                 getCurrentInputConnection().getCursorCapsMode(mEditorInfo.inputType) != 0;
    247         mKeyboardView.setShifted(shouldCapitalize);
    248     }
    249 
    250     private final KeyboardView.OnKeyboardActionListener mKeyboardActionListener =
    251             new KeyboardView.OnKeyboardActionListener() {
    252                 @Override
    253                 public void onPress(int primaryCode) {
    254                 }
    255 
    256                 @Override
    257                 public void onRelease(int primaryCode) {
    258                 }
    259 
    260                 @Override
    261                 public void onKey(int primaryCode, int[] keyCodes) {
    262                     if (Log.isLoggable(TAG, Log.DEBUG)) {
    263                       Log.d(TAG, "onKey " + primaryCode);
    264                     }
    265                     InputConnection inputConnection = getCurrentInputConnection();
    266                     switch (primaryCode) {
    267                         case Keyboard.KEYCODE_SHIFT:
    268                             toggleCapitalization();
    269                             break;
    270                         case Keyboard.KEYCODE_MODE_CHANGE:
    271                             if (mKeyboardView.getKeyboard() == mQweKeyboard) {
    272                                 mKeyboardView.setKeyboard(mSymbolKeyboard, getLocale());
    273                             } else {
    274                                 mKeyboardView.setKeyboard(mQweKeyboard, getLocale());
    275                             }
    276                             break;
    277                         case Keyboard.KEYCODE_DONE:
    278                             int action = mEditorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
    279                             inputConnection.performEditorAction(action);
    280                             break;
    281                         case Keyboard.KEYCODE_DELETE:
    282                             inputConnection.deleteSurroundingText(1, 0);
    283                             updateCapitalization();
    284                             break;
    285                         case KEYCODE_MAIN_KEYBOARD:
    286                             mKeyboardView.setKeyboard(mQweKeyboard, getLocale());
    287                             break;
    288                         case KEYCODE_NUM_KEYBOARD:
    289                             // No number keyboard layout support.
    290                             break;
    291                         case KEYCODE_ALPHA_KEYBOARD:
    292                             //loadKeyboard(ALPHA_LAYOUT_XML);
    293                             break;
    294                         case KEYCODE_CLOSE_KEYBOARD:
    295                             hideWindow();
    296                             break;
    297                         case KEYCODE_CYCLE_CHAR:
    298                             CharSequence text = inputConnection.getTextBeforeCursor(1, 0);
    299                             if (TextUtils.isEmpty(text)) {
    300                                 break;
    301                             }
    302 
    303                             char currChar = text.charAt(0);
    304                             char altChar = cycleCharacter(currChar);
    305                             // Don't modify text if there is no alternate.
    306                             if (currChar != altChar) {
    307                                 inputConnection.deleteSurroundingText(1, 0);
    308                                 inputConnection.commitText(String.valueOf(altChar), 1);
    309                             }
    310                             break;
    311                         case KEYCODE_ENTER:
    312                             final int imeOptionsActionId = getImeOptionsActionIdFromEditorInfo(mEditorInfo);
    313                             if (IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
    314                                 // Either we have an actionLabel and we should performEditorAction with
    315                                 // actionId regardless of its value.
    316                                 inputConnection.performEditorAction(mEditorInfo.actionId);
    317                             } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
    318                                 // We didn't have an actionLabel, but we had another action to execute.
    319                                 // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
    320                                 // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
    321                                 // means there should be an action and the app didn't bother to set a specific
    322                                 // code for it - presumably it only handles one. It does not have to be treated
    323                                 // in any specific way: anything that is not IME_ACTION_NONE should be sent to
    324                                 // performEditorAction.
    325                                 inputConnection.performEditorAction(imeOptionsActionId);
    326                             } else {
    327                                 // No action label, and the action from imeOptions is NONE: this is a regular
    328                                 // enter key that should input a carriage return.
    329                                 String txt = Character.toString((char) primaryCode);
    330                                 if (mKeyboardView.isShifted()) {
    331                                     txt = txt.toUpperCase(mLocale);
    332                                 }
    333                                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    334                                     Log.d(TAG, "commitText " + txt);
    335                                 }
    336                                 inputConnection.commitText(txt, 1);
    337                                 updateCapitalization();
    338                             }
    339                             break;
    340                         default:
    341                             String commitText = Character.toString((char) primaryCode);
    342                             // Chars always come through as lowercase, so we have to explicitly
    343                             // uppercase them if the keyboard is shifted.
    344                             if (mKeyboardView.isShifted()) {
    345                                 commitText = commitText.toUpperCase(mLocale);
    346                             }
    347                             if (Log.isLoggable(TAG, Log.DEBUG)) {
    348                               Log.d(TAG, "commitText " + commitText);
    349                             }
    350                             inputConnection.commitText(commitText, 1);
    351                             updateCapitalization();
    352                     }
    353                 }
    354 
    355                 @Override
    356                 public void onText(CharSequence text) {
    357                 }
    358 
    359                 @Override
    360                 public void swipeLeft() {
    361                 }
    362 
    363                 @Override
    364                 public void swipeRight() {
    365                 }
    366 
    367                 @Override
    368                 public void swipeDown() {
    369                 }
    370 
    371                 @Override
    372                 public void swipeUp() {
    373                 }
    374 
    375                 @Override
    376                 public void stopInput() {
    377                     hideWindow();
    378                 }
    379             };
    380 
    381     private final KeyboardView.OnKeyboardActionListener mPopupKeyboardActionListener =
    382             new KeyboardView.OnKeyboardActionListener() {
    383                 @Override
    384                 public void onPress(int primaryCode) {
    385                 }
    386 
    387                 @Override
    388                 public void onRelease(int primaryCode) {
    389                 }
    390 
    391                 @Override
    392                 public void onKey(int primaryCode, int[] keyCodes) {
    393                     InputConnection inputConnection = getCurrentInputConnection();
    394                     String commitText = Character.toString((char) primaryCode);
    395                     // Chars always come through as lowercase, so we have to explicitly
    396                     // uppercase them if the keyboard is shifted.
    397                     if (mKeyboardView.isShifted()) {
    398                         commitText = commitText.toUpperCase(mLocale);
    399                     }
    400                     inputConnection.commitText(commitText, 1);
    401                     updateCapitalization();
    402                     mKeyboardView.dismissPopupKeyboard();
    403                 }
    404 
    405                 @Override
    406                 public void onText(CharSequence text) {
    407                 }
    408 
    409                 @Override
    410                 public void swipeLeft() {
    411                 }
    412 
    413                 @Override
    414                 public void swipeRight() {
    415                 }
    416 
    417                 @Override
    418                 public void swipeDown() {
    419                 }
    420 
    421                 @Override
    422                 public void swipeUp() {
    423                 }
    424 
    425                 @Override
    426                 public void stopInput() {
    427                     hideWindow();
    428                 }
    429             };
    430 
    431     /**
    432      * Cycle through alternate characters of the given character. Return the same character if
    433      * there is no alternate.
    434      */
    435     private char cycleCharacter(char current) {
    436         if (Character.isUpperCase(current)) {
    437             return String.valueOf(current).toLowerCase(mLocale).charAt(0);
    438         } else {
    439             return String.valueOf(current).toUpperCase(mLocale).charAt(0);
    440         }
    441     }
    442 
    443     private int getImeOptionsActionIdFromEditorInfo(final EditorInfo editorInfo) {
    444         if ((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
    445             return EditorInfo.IME_ACTION_NONE;
    446         } else if (editorInfo.actionLabel != null) {
    447             return IME_ACTION_CUSTOM_LABEL;
    448         } else {
    449             // Note: this is different from editorInfo.actionId, hence "ImeOptionsActionId"
    450             return editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
    451         }
    452     }
    453 }
    454