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