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