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