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 MSG_ENABLE_KEYBOARD = 0; 63 private static final int KEYCODE_CYCLE_CHAR = -7; 64 private static final int KEYCODE_MAIN_KEYBOARD = -8; 65 private static final int KEYCODE_NUM_KEYBOARD = -9; 66 private static final int KEYCODE_ALPHA_KEYBOARD = -10; 67 private static final int KEYCODE_CLOSE_KEYBOARD = -99; 68 69 private Keyboard mQweKeyboard; 70 private Keyboard mSymbolKeyboard; 71 private Car mCar; 72 private CarSensorManager mSensorManager; 73 74 private View mLockoutView; 75 private KeyboardView mPopupKeyboardView; 76 77 @GuardedBy("this") 78 private boolean mKeyboardEnabled = true; 79 private KeyboardView mKeyboardView; 80 private Locale mLocale; 81 private final Handler mHandler; 82 83 private FrameLayout mKeyboardWrapper; 84 private EditorInfo mEditorInfo; 85 86 private static final class HideKeyboardHandler extends Handler { 87 private final WeakReference<CarLatinIME> mIME; HideKeyboardHandler(CarLatinIME ime)88 public HideKeyboardHandler(CarLatinIME ime) { 89 mIME = new WeakReference<CarLatinIME>(ime); 90 } 91 @Override handleMessage(Message msg)92 public void handleMessage(Message msg) { 93 switch (msg.what) { 94 case MSG_ENABLE_KEYBOARD: 95 if (mIME.get() != null) { 96 mIME.get().updateKeyboardState(msg.arg1 == 1); 97 } 98 break; 99 } 100 } 101 } 102 103 private final ServiceConnection mCarConnectionListener = 104 new ServiceConnection() { 105 public void onServiceConnected(ComponentName name, IBinder service) { 106 Log.d(TAG, "Car Service connected"); 107 try { 108 mSensorManager = (CarSensorManager) mCar.getCarManager(Car.SENSOR_SERVICE); 109 mSensorManager.registerListener(mCarSensorListener, 110 CarSensorManager.SENSOR_TYPE_DRIVING_STATUS, 111 CarSensorManager.SENSOR_RATE_FASTEST); 112 } catch (CarNotConnectedException e) { 113 Log.e(TAG, "car not connected", e); 114 } 115 } 116 117 @Override 118 public void onServiceDisconnected(ComponentName name) { 119 Log.e(TAG, "CarService: onServiceDisconnedted " + name); 120 } 121 }; 122 123 private final CarSensorManager.OnSensorChangedListener mCarSensorListener = 124 new CarSensorManager.OnSensorChangedListener() { 125 @Override 126 public void onSensorChanged(CarSensorEvent event) { 127 if (event.sensorType != CarSensorManager.SENSOR_TYPE_DRIVING_STATUS) { 128 return; 129 } 130 int drivingStatus = event.getDrivingStatusData(null).status; 131 132 boolean keyboardEnabled = 133 (drivingStatus & CarSensorEvent.DRIVE_STATUS_NO_KEYBOARD_INPUT) == 0; 134 mHandler.sendMessage(mHandler.obtainMessage( 135 MSG_ENABLE_KEYBOARD, keyboardEnabled ? 1 : 0, 0, null)); 136 } 137 }; 138 CarLatinIME()139 public CarLatinIME() { 140 super(); 141 mHandler = new HideKeyboardHandler(this); 142 } 143 144 @Override onCreate()145 public void onCreate() { 146 super.onCreate(); 147 mCar = Car.createCar(this, mCarConnectionListener); 148 mCar.connect(); 149 150 mQweKeyboard = createKeyboard(LAYOUT_XML); 151 mSymbolKeyboard = createKeyboard(SYMBOL_LAYOUT_XML); 152 } 153 154 @Override onDestroy()155 public void onDestroy() { 156 super.onDestroy(); 157 if (mCar != null) { 158 mCar.disconnect(); 159 } 160 } 161 162 @Override onCreateInputView()163 public View onCreateInputView() { 164 if (Log.isLoggable(TAG, Log.DEBUG)) { 165 Log.d(TAG, "onCreateInputView"); 166 } 167 super.onCreateInputView(); 168 169 View v = LayoutInflater.from(this).inflate(R.layout.input_keyboard, null); 170 mKeyboardView = (KeyboardView) v.findViewById(R.id.keyboard); 171 172 mLockoutView = v.findViewById(R.id.lockout); 173 mPopupKeyboardView = (KeyboardView) v.findViewById(R.id.popup_keyboard); 174 mKeyboardView.setPopupKeyboardView(mPopupKeyboardView); 175 mKeyboardWrapper = (FrameLayout) v.findViewById(R.id.keyboard_wrapper); 176 mLockoutView.setBackgroundResource(R.color.ime_background_letters); 177 178 synchronized (this) { 179 updateKeyboardStateLocked(); 180 } 181 return v; 182 } 183 184 185 186 @Override onStartInputView(EditorInfo editorInfo, boolean reastarting)187 public void onStartInputView(EditorInfo editorInfo, boolean reastarting) { 188 super.onStartInputView(editorInfo, reastarting); 189 mEditorInfo = editorInfo; 190 mKeyboardView.setKeyboard(mQweKeyboard, getLocale()); 191 mKeyboardWrapper.setPadding(0, 192 getResources().getDimensionPixelSize(R.dimen.keyboard_padding_vertical), 0, 0); 193 mKeyboardView.setOnKeyboardActionListener(mKeyboardActionListener); 194 mPopupKeyboardView.setOnKeyboardActionListener(mPopupKeyboardActionListener); 195 mKeyboardView.setShifted(mKeyboardView.isShifted()); 196 updateCapitalization(); 197 } 198 getLocale()199 public Locale getLocale() { 200 if (mLocale == null) { 201 mLocale = this.getResources().getConfiguration().locale; 202 } 203 return mLocale; 204 } 205 206 @Override onEvaluateFullscreenMode()207 public boolean onEvaluateFullscreenMode() { 208 return false; 209 } 210 createKeyboard(String layoutXml)211 private Keyboard createKeyboard(String layoutXml) { 212 Resources res = this.getResources(); 213 Configuration configuration = res.getConfiguration(); 214 Locale oldLocale = configuration.locale; 215 configuration.locale = new Locale(DEFAULT_LANGUAGE); 216 res.updateConfiguration(configuration, res.getDisplayMetrics()); 217 Keyboard ret = new Keyboard( 218 this, res.getIdentifier(layoutXml, "xml", getPackageName())); 219 mLocale = configuration.locale; 220 configuration.locale = oldLocale; 221 return ret; 222 } 223 updateKeyboardState(boolean enabled)224 public void updateKeyboardState(boolean enabled) { 225 synchronized (this) { 226 mKeyboardEnabled = enabled; 227 updateKeyboardStateLocked(); 228 } 229 } 230 updateKeyboardStateLocked()231 private void updateKeyboardStateLocked() { 232 if (mLockoutView == null) { 233 return; 234 } 235 mLockoutView.setVisibility(mKeyboardEnabled ? View.GONE : View.VISIBLE); 236 } 237 toggleCapitalization()238 private void toggleCapitalization() { 239 mKeyboardView.setShifted(!mKeyboardView.isShifted()); 240 } 241 updateCapitalization()242 private void updateCapitalization() { 243 boolean shouldCapitalize = 244 getCurrentInputConnection().getCursorCapsMode(mEditorInfo.inputType) != 0; 245 mKeyboardView.setShifted(shouldCapitalize); 246 } 247 248 private final KeyboardView.OnKeyboardActionListener mKeyboardActionListener = 249 new KeyboardView.OnKeyboardActionListener() { 250 @Override 251 public void onPress(int primaryCode) { 252 } 253 254 @Override 255 public void onRelease(int primaryCode) { 256 } 257 258 @Override 259 public void onKey(int primaryCode, int[] keyCodes) { 260 if (Log.isLoggable(TAG, Log.DEBUG)) { 261 Log.d(TAG, "onKey " + primaryCode); 262 } 263 InputConnection inputConnection = getCurrentInputConnection(); 264 switch (primaryCode) { 265 case Keyboard.KEYCODE_SHIFT: 266 toggleCapitalization(); 267 break; 268 case Keyboard.KEYCODE_MODE_CHANGE: 269 if (mKeyboardView.getKeyboard() == mQweKeyboard) { 270 mKeyboardView.setKeyboard(mSymbolKeyboard, getLocale()); 271 } else { 272 mKeyboardView.setKeyboard(mQweKeyboard, getLocale()); 273 } 274 break; 275 case Keyboard.KEYCODE_DONE: 276 int action = mEditorInfo.imeOptions & EditorInfo.IME_MASK_ACTION; 277 inputConnection.performEditorAction(action); 278 break; 279 case Keyboard.KEYCODE_DELETE: 280 inputConnection.deleteSurroundingText(1, 0); 281 updateCapitalization(); 282 break; 283 case KEYCODE_MAIN_KEYBOARD: 284 mKeyboardView.setKeyboard(mQweKeyboard, getLocale()); 285 break; 286 case KEYCODE_NUM_KEYBOARD: 287 // No number keyboard layout support. 288 break; 289 case KEYCODE_ALPHA_KEYBOARD: 290 //loadKeyboard(ALPHA_LAYOUT_XML); 291 break; 292 case KEYCODE_CLOSE_KEYBOARD: 293 hideWindow(); 294 break; 295 case KEYCODE_CYCLE_CHAR: 296 CharSequence text = inputConnection.getTextBeforeCursor(1, 0); 297 if (TextUtils.isEmpty(text)) { 298 break; 299 } 300 301 char currChar = text.charAt(0); 302 char altChar = cycleCharacter(currChar); 303 // Don't modify text if there is no alternate. 304 if (currChar != altChar) { 305 inputConnection.deleteSurroundingText(1, 0); 306 inputConnection.commitText(String.valueOf(altChar), 1); 307 } 308 break; 309 default: 310 String commitText = Character.toString((char) primaryCode); 311 // Chars always come through as lowercase, so we have to explicitly 312 // uppercase them if the keyboard is shifted. 313 if (mKeyboardView.isShifted()) { 314 commitText = commitText.toUpperCase(mLocale); 315 } 316 if (Log.isLoggable(TAG, Log.DEBUG)) { 317 Log.d(TAG, "commitText " + commitText); 318 } 319 inputConnection.commitText(commitText, 1); 320 updateCapitalization(); 321 } 322 } 323 324 @Override 325 public void onText(CharSequence text) { 326 } 327 328 @Override 329 public void swipeLeft() { 330 } 331 332 @Override 333 public void swipeRight() { 334 } 335 336 @Override 337 public void swipeDown() { 338 } 339 340 @Override 341 public void swipeUp() { 342 } 343 344 @Override 345 public void stopInput() { 346 hideWindow(); 347 } 348 }; 349 350 private final KeyboardView.OnKeyboardActionListener mPopupKeyboardActionListener = 351 new KeyboardView.OnKeyboardActionListener() { 352 @Override 353 public void onPress(int primaryCode) { 354 } 355 356 @Override 357 public void onRelease(int primaryCode) { 358 } 359 360 @Override 361 public void onKey(int primaryCode, int[] keyCodes) { 362 InputConnection inputConnection = getCurrentInputConnection(); 363 String commitText = Character.toString((char) primaryCode); 364 // Chars always come through as lowercase, so we have to explicitly 365 // uppercase them if the keyboard is shifted. 366 if (mKeyboardView.isShifted()) { 367 commitText = commitText.toUpperCase(mLocale); 368 } 369 inputConnection.commitText(commitText, 1); 370 updateCapitalization(); 371 mKeyboardView.dismissPopupKeyboard(); 372 } 373 374 @Override 375 public void onText(CharSequence text) { 376 } 377 378 @Override 379 public void swipeLeft() { 380 } 381 382 @Override 383 public void swipeRight() { 384 } 385 386 @Override 387 public void swipeDown() { 388 } 389 390 @Override 391 public void swipeUp() { 392 } 393 394 @Override 395 public void stopInput() { 396 hideWindow(); 397 } 398 }; 399 400 /** 401 * Cycle through alternate characters of the given character. Return the same character if 402 * there is no alternate. 403 */ cycleCharacter(char current)404 private char cycleCharacter(char current) { 405 if (Character.isUpperCase(current)) { 406 return String.valueOf(current).toLowerCase(mLocale).charAt(0); 407 } else { 408 return String.valueOf(current).toUpperCase(mLocale).charAt(0); 409 } 410 } 411 } 412