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 = 247 getCurrentInputConnection().getCursorCapsMode(mEditorInfo.inputType) != 0; 248 mKeyboardView.setShifted(shouldCapitalize); 249 } 250 251 private final KeyboardView.OnKeyboardActionListener mKeyboardActionListener = 252 new KeyboardView.OnKeyboardActionListener() { 253 @Override 254 public void onPress(int primaryCode) { 255 } 256 257 @Override 258 public void onRelease(int primaryCode) { 259 } 260 261 @Override 262 public void onKey(int primaryCode, int[] keyCodes) { 263 if (Log.isLoggable(TAG, Log.DEBUG)) { 264 Log.d(TAG, "onKey " + primaryCode); 265 } 266 InputConnection inputConnection = getCurrentInputConnection(); 267 switch (primaryCode) { 268 case Keyboard.KEYCODE_SHIFT: 269 toggleCapitalization(); 270 break; 271 case Keyboard.KEYCODE_MODE_CHANGE: 272 if (mKeyboardView.getKeyboard() == mQweKeyboard) { 273 mKeyboardView.setKeyboard(mSymbolKeyboard, getLocale()); 274 } else { 275 mKeyboardView.setKeyboard(mQweKeyboard, getLocale()); 276 } 277 break; 278 case Keyboard.KEYCODE_DONE: 279 int action = mEditorInfo.imeOptions & EditorInfo.IME_MASK_ACTION; 280 inputConnection.performEditorAction(action); 281 break; 282 case Keyboard.KEYCODE_DELETE: 283 inputConnection.deleteSurroundingText(1, 0); 284 updateCapitalization(); 285 break; 286 case KEYCODE_MAIN_KEYBOARD: 287 mKeyboardView.setKeyboard(mQweKeyboard, getLocale()); 288 break; 289 case KEYCODE_NUM_KEYBOARD: 290 // No number keyboard layout support. 291 break; 292 case KEYCODE_ALPHA_KEYBOARD: 293 //loadKeyboard(ALPHA_LAYOUT_XML); 294 break; 295 case KEYCODE_CLOSE_KEYBOARD: 296 hideWindow(); 297 break; 298 case KEYCODE_CYCLE_CHAR: 299 CharSequence text = inputConnection.getTextBeforeCursor(1, 0); 300 if (TextUtils.isEmpty(text)) { 301 break; 302 } 303 304 char currChar = text.charAt(0); 305 char altChar = cycleCharacter(currChar); 306 // Don't modify text if there is no alternate. 307 if (currChar != altChar) { 308 inputConnection.deleteSurroundingText(1, 0); 309 inputConnection.commitText(String.valueOf(altChar), 1); 310 } 311 break; 312 case KEYCODE_ENTER: 313 final int imeOptionsActionId = getImeOptionsActionIdFromEditorInfo( 314 mEditorInfo); 315 if (IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { 316 // Either we have an actionLabel and we should 317 // performEditorAction with 318 // actionId regardless of its value. 319 inputConnection.performEditorAction(mEditorInfo.actionId); 320 } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { 321 // We didn't have an actionLabel, but we had another action to 322 // execute. 323 // EditorInfo.IME_ACTION_NONE explicitly means no action. In 324 // contrast, 325 // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an 326 // action, so it 327 // means there should be an action and the app didn't bother to 328 // set a specific 329 // code for it - presumably it only handles one. It does not have 330 // to be treated 331 // in any specific way: anything that is not IME_ACTION_NONE 332 // should be sent to 333 // performEditorAction. 334 inputConnection.performEditorAction(imeOptionsActionId); 335 } else { 336 // No action label, and the action from imeOptions is NONE: this 337 // is a regular 338 // enter key that should input a carriage return. 339 String txt = Character.toString((char) primaryCode); 340 if (mKeyboardView.isShifted()) { 341 txt = txt.toUpperCase(mLocale); 342 } 343 if (Log.isLoggable(TAG, Log.DEBUG)) { 344 Log.d(TAG, "commitText " + txt); 345 } 346 inputConnection.commitText(txt, 1); 347 updateCapitalization(); 348 } 349 break; 350 default: 351 String commitText = Character.toString((char) primaryCode); 352 // Chars always come through as lowercase, so we have to explicitly 353 // uppercase them if the keyboard is shifted. 354 if (mKeyboardView.isShifted()) { 355 commitText = commitText.toUpperCase(mLocale); 356 } 357 if (Log.isLoggable(TAG, Log.DEBUG)) { 358 Log.d(TAG, "commitText " + commitText); 359 } 360 inputConnection.commitText(commitText, 1); 361 updateCapitalization(); 362 } 363 } 364 365 @Override 366 public void onText(CharSequence text) { 367 } 368 369 @Override 370 public void swipeLeft() { 371 } 372 373 @Override 374 public void swipeRight() { 375 } 376 377 @Override 378 public void swipeDown() { 379 } 380 381 @Override 382 public void swipeUp() { 383 } 384 385 @Override 386 public void stopInput() { 387 hideWindow(); 388 } 389 }; 390 391 private final KeyboardView.OnKeyboardActionListener mPopupKeyboardActionListener = 392 new KeyboardView.OnKeyboardActionListener() { 393 @Override 394 public void onPress(int primaryCode) { 395 } 396 397 @Override 398 public void onRelease(int primaryCode) { 399 } 400 401 @Override 402 public void onKey(int primaryCode, int[] keyCodes) { 403 InputConnection inputConnection = getCurrentInputConnection(); 404 String commitText = Character.toString((char) primaryCode); 405 // Chars always come through as lowercase, so we have to explicitly 406 // uppercase them if the keyboard is shifted. 407 if (mKeyboardView.isShifted()) { 408 commitText = commitText.toUpperCase(mLocale); 409 } 410 inputConnection.commitText(commitText, 1); 411 updateCapitalization(); 412 mKeyboardView.dismissPopupKeyboard(); 413 } 414 415 @Override 416 public void onText(CharSequence text) { 417 } 418 419 @Override 420 public void swipeLeft() { 421 } 422 423 @Override 424 public void swipeRight() { 425 } 426 427 @Override 428 public void swipeDown() { 429 } 430 431 @Override 432 public void swipeUp() { 433 } 434 435 @Override 436 public void stopInput() { 437 hideWindow(); 438 } 439 }; 440 441 /** 442 * Cycle through alternate characters of the given character. Return the same character if 443 * there is no alternate. 444 */ cycleCharacter(char current)445 private char cycleCharacter(char current) { 446 if (Character.isUpperCase(current)) { 447 return String.valueOf(current).toLowerCase(mLocale).charAt(0); 448 } else { 449 return String.valueOf(current).toUpperCase(mLocale).charAt(0); 450 } 451 } 452 getImeOptionsActionIdFromEditorInfo(final EditorInfo editorInfo)453 private int getImeOptionsActionIdFromEditorInfo(final EditorInfo editorInfo) { 454 if ((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { 455 return EditorInfo.IME_ACTION_NONE; 456 } else if (editorInfo.actionLabel != null) { 457 return IME_ACTION_CUSTOM_LABEL; 458 } else { 459 // Note: this is different from editorInfo.actionId, hence "ImeOptionsActionId" 460 return editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION; 461 } 462 } 463 } 464