• 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(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