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