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