1 /*
2  * Copyright (C) 2019 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 
17 package com.android.inputmethod.leanback.service;
18 
19 import android.content.Intent;
20 import android.inputmethodservice.InputMethodService;
21 import android.os.Handler;
22 import android.os.Looper;
23 import android.os.Message;
24 import android.view.InputDevice;
25 import android.view.KeyEvent;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.inputmethod.CompletionInfo;
29 import android.view.inputmethod.EditorInfo;
30 import android.view.inputmethod.InputConnection;
31 import android.util.Log;
32 
33 import com.android.inputmethod.leanback.LeanbackKeyboardContainer;
34 import com.android.inputmethod.leanback.LeanbackKeyboardController;
35 import com.android.inputmethod.leanback.LeanbackKeyboardView;
36 import com.android.inputmethod.leanback.LeanbackLocales;
37 import com.android.inputmethod.leanback.LeanbackSuggestionsFactory;
38 import com.android.inputmethod.leanback.LeanbackUtils;
39 
40 /**
41  * This is a simplified version of GridIme
42  */
43 public class LeanbackImeService extends InputMethodService {
44 
45     private static final String TAG = "LbImeService";
46     private static final boolean DEBUG = false;
47 
48     // use dpad events, with lock axis
49     static final int MODE_TRACKPAD_NAVIGATION = 0;
50     // track motion directly.
51     static final int MODE_FREE_MOVEMENT = 1;
52 
53     public static final int MAX_SUGGESTIONS = 10;
54 
55     private static final int MSG_SUGGESTIONS_CLEAR = 123;
56     private static final int SUGGESTIONS_CLEAR_DELAY = 1000;
57 
58     public static final String IME_OPEN = "com.android.inputmethod.leanback.action.IME_OPEN";
59     public static final String IME_CLOSE = "com.android.inputmethod.leanback.action.IME_CLOSE";
60 
61     private LeanbackKeyboardController.InputListener mInputListener
62             = new LeanbackKeyboardController.InputListener() {
63         @Override
64         public void onEntry(int type, int keyCode, CharSequence result) {
65             handleTextEntry(type, keyCode, result);
66         }
67     };
68 
69     private View mInputView;
70     private LeanbackKeyboardController mKeyboardController;
71     private LeanbackSuggestionsFactory mSuggestionsFactory;
72 
73     // IME will auto insert space after clicking on the candidates if next
74     // character is alphabet
75     private boolean mEnterSpaceBeforeCommitting;
76 
77     private boolean mShouldClearSuggestions = true;
78     private final Handler mHandler = new Handler() {
79         @Override
80         public void handleMessage(Message msg) {
81             if (msg.what == MSG_SUGGESTIONS_CLEAR) {
82                 if (mShouldClearSuggestions) {
83                     mSuggestionsFactory.clearSuggestions();
84                     mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions());
85                     mShouldClearSuggestions = false;
86                 }
87             }
88         }
89     };
90 
LeanbackImeService()91     public LeanbackImeService() {
92         if (!enableHardwareAcceleration()) {
93             Log.w(TAG, "Could not enable hardware acceleration");
94         }
95     }
96 
clearSuggestionsDelayed()97     private void clearSuggestionsDelayed() {
98         // if suggestions amend, we should keep clearing them
99         if (!mSuggestionsFactory.shouldSuggestionsAmend()) {
100             mHandler.removeMessages(MSG_SUGGESTIONS_CLEAR);
101             mShouldClearSuggestions = true;
102             mHandler.sendEmptyMessageDelayed(MSG_SUGGESTIONS_CLEAR, SUGGESTIONS_CLEAR_DELAY);
103         }
104     }
105 
106     @Override
onInitializeInterface()107     public void onInitializeInterface() {
108         mKeyboardController = new LeanbackKeyboardController(this, mInputListener);
109         mEnterSpaceBeforeCommitting = false;
110         mSuggestionsFactory = new LeanbackSuggestionsFactory(this, MAX_SUGGESTIONS);
111     }
112 
113     @Override
onCreateInputView()114     public View onCreateInputView() {
115         mInputView = mKeyboardController.getView();
116         mInputView.requestFocus();
117         return mInputView;
118     }
119 
120     /**
121      * {@inheritDoc} This function gets called whenever we start the input
122      * window
123      */
124     @Override
onStartInputView(EditorInfo info, boolean restarting)125     public void onStartInputView(EditorInfo info, boolean restarting) {
126         super.onStartInputView(info, restarting);
127         mKeyboardController.onStartInputView();
128         sendBroadcast(new Intent(IME_OPEN));
129 
130         if (mKeyboardController.areSuggestionsEnabled()) {
131             mSuggestionsFactory.createSuggestions();
132             mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions());
133 
134             // repost text to get completions
135             InputConnection ic = getCurrentInputConnection();
136             if (ic != null) {
137                 String c = getEditorText(ic);
138                 ic.deleteSurroundingText(getCharLengthBeforeCursor(ic),
139                         getCharLengthAfterCursor(ic));
140                 ic.commitText(c, 1);
141             }
142         }
143     }
144 
145 
146     @Override
onFinishInputView(boolean finishingInput)147     public void onFinishInputView(boolean finishingInput) {
148         super.onFinishInputView(finishingInput);
149         sendBroadcast(new Intent(IME_CLOSE));
150         mSuggestionsFactory.clearSuggestions();
151     }
152 
153     /**
154      * {@inheritDoc} This function doesn't get called when we dismiss the
155      * keyboard, and reopen it on the same input field
156      */
157     @Override
onStartInput(EditorInfo attribute, boolean restarting)158     public void onStartInput(EditorInfo attribute, boolean restarting) {
159         super.onStartInput(attribute, restarting);
160         mEnterSpaceBeforeCommitting = false;
161         mSuggestionsFactory.onStartInput(attribute);
162         mKeyboardController.onStartInput(attribute);
163     }
164 
165     /**
166      * {@inheritDoc} Always return true to show GridIme when editText calls
167      * requestFocus
168      */
169     @Override
onShowInputRequested(int flags, boolean configChange)170     public boolean onShowInputRequested(int flags, boolean configChange) {
171         return true;
172     }
173 
174     /**
175      * {@inheritDoc} Always enable soft keyboard. If we return the super method,
176      * the IME will not be shown if there is a hardware keyboard connected
177      */
178     @Override
onEvaluateInputViewShown()179     public boolean onEvaluateInputViewShown() {
180         return true;
181     }
182 
183     @Override
onEvaluateFullscreenMode()184     public boolean onEvaluateFullscreenMode() {
185         // Superclass always returns true in landscape mode.
186         // Assume we're on TV with lots of display area.
187         return false;
188     }
189 
190     @Override
onKeyUp(int keyCode, KeyEvent event)191     public boolean onKeyUp(int keyCode, KeyEvent event) {
192         if (isInputViewShown()
193                 && mKeyboardController.onKeyUp(keyCode, event)) {
194             return true;
195         }
196         return super.onKeyUp(keyCode, event);
197     }
198 
199     @Override
onKeyDown(int keyCode, KeyEvent event)200     public boolean onKeyDown(int keyCode, KeyEvent event) {
201         if (isInputViewShown()
202                 && mKeyboardController.onKeyDown(keyCode, event)) {
203             return true;
204         }
205         return super.onKeyDown(keyCode, event);
206     }
207 
208     @Override
onGenericMotionEvent(MotionEvent event)209     public boolean onGenericMotionEvent(MotionEvent event) {
210         if (isInputViewShown() && (event.getSource() & InputDevice.SOURCE_TOUCH_NAVIGATION)
211                 == InputDevice.SOURCE_TOUCH_NAVIGATION) {
212             if (mKeyboardController.onGenericMotionEvent(event)) {
213                 return true;
214             }
215         }
216         return super.onGenericMotionEvent(event);
217     }
218 
219     @Override
onDisplayCompletions(CompletionInfo[] completions)220     public void onDisplayCompletions(CompletionInfo[] completions) {
221         if (mKeyboardController.areSuggestionsEnabled()) {
222             mShouldClearSuggestions = false;
223             mHandler.removeMessages(MSG_SUGGESTIONS_CLEAR);
224             mSuggestionsFactory.onDisplayCompletions(completions);
225             mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions());
226         }
227     }
228 
getEditorText(InputConnection ic)229     private String getEditorText(InputConnection ic) {
230         StringBuilder editorText = new StringBuilder();
231         CharSequence textBeforeCursor = ic.getTextBeforeCursor(1000, 0);
232         CharSequence textAfterCursor = ic.getTextAfterCursor(1000, 0);
233         if (textBeforeCursor != null) {
234             editorText.append(textBeforeCursor);
235         }
236         if (textAfterCursor != null) {
237             editorText.append(textAfterCursor);
238         }
239         return editorText.toString();
240     }
241 
getAmpersandLocation(InputConnection ic)242     private int getAmpersandLocation(InputConnection ic) {
243         String editorText = getEditorText(ic);
244         int indexOf = editorText.indexOf('@');
245         if (indexOf < 0) {
246             indexOf = editorText.length();
247         }
248 
249         return indexOf;
250     }
251 
getCharLengthBeforeCursor(InputConnection ic)252     private int getCharLengthBeforeCursor(InputConnection ic) {
253         final CharSequence textLeft = ic.getTextBeforeCursor(1000, 0);
254         return textLeft != null ? textLeft.length() : 0;
255     }
256 
getCharLengthAfterCursor(InputConnection ic )257     private int getCharLengthAfterCursor(InputConnection ic ) {
258         final CharSequence textRight = ic.getTextAfterCursor(1000, 0);
259         return textRight != null ? textRight.length() : 0;
260     }
261 
handleTextEntry(int type, int keyCode, CharSequence c)262     private void handleTextEntry(int type, int keyCode, CharSequence c) {
263         InputConnection ic = getCurrentInputConnection();
264         boolean updateSuggestions = true;
265 
266         if (ic == null) {
267             return;
268         }
269 
270         switch (type) {
271             case LeanbackKeyboardController.InputListener.ENTRY_TYPE_BACKSPACE:
272                 clearSuggestionsDelayed();
273                 ic.deleteSurroundingText(1, 0);
274                 mEnterSpaceBeforeCommitting = false;
275                 break;
276             case LeanbackKeyboardController.InputListener.ENTRY_TYPE_LEFT:
277             case LeanbackKeyboardController.InputListener.ENTRY_TYPE_RIGHT:
278                 CharSequence textBeforeCursor = ic.getTextBeforeCursor(1000, 0);
279                 int newCursorPosition = textBeforeCursor == null ? 0 : textBeforeCursor.length();
280 
281                 if (type == LeanbackKeyboardController.InputListener.ENTRY_TYPE_LEFT) {
282                     if (newCursorPosition > 0) {
283                         newCursorPosition--;
284                     }
285                 } else {
286                     CharSequence textAfterCursor = ic.getTextAfterCursor(1000, 0);
287                     if (textAfterCursor != null && textAfterCursor.length() > 0) {
288                         newCursorPosition++;
289                     }
290                 }
291 
292                 ic.setSelection(newCursorPosition, newCursorPosition);
293                 break;
294             case LeanbackKeyboardController.InputListener.ENTRY_TYPE_STRING:
295                 clearSuggestionsDelayed();
296                 if (mEnterSpaceBeforeCommitting
297                         && mKeyboardController.enableAutoEnterSpace()) {
298                     if (LeanbackUtils.isAlphabet(keyCode)) {
299                         ic.commitText(" ", 1);
300                     }
301                     mEnterSpaceBeforeCommitting = false;
302                 }
303                 ic.commitText(c, 1);
304                 if (keyCode == LeanbackKeyboardView.ASCII_PERIOD) {
305                     mEnterSpaceBeforeCommitting = true;
306                 }
307                 break;
308             case LeanbackKeyboardController.InputListener.ENTRY_TYPE_SUGGESTION:
309             case LeanbackKeyboardController.InputListener.ENTRY_TYPE_VOICE:
310                 clearSuggestionsDelayed();
311                 if (!mSuggestionsFactory.shouldSuggestionsAmend()) {
312                     ic.deleteSurroundingText(getCharLengthBeforeCursor(ic),
313                             getCharLengthAfterCursor(ic));
314                 } else {
315                     int location = getAmpersandLocation(ic);
316                     ic.setSelection(location, location);
317                     ic.deleteSurroundingText(0, getCharLengthAfterCursor(ic));
318                 }
319                 ic.commitText(c, 1);
320                 mEnterSpaceBeforeCommitting = true;
321                 // go straight into action (skip updating suggestions)
322             case LeanbackKeyboardController.InputListener.ENTRY_TYPE_ACTION:
323                 sendDefaultEditorAction(false);
324                 updateSuggestions = false;
325                 break;
326             case LeanbackKeyboardController.InputListener.ENTRY_TYPE_DISMISS:
327                 ic.performEditorAction(EditorInfo.IME_ACTION_NONE);
328                 updateSuggestions = false;
329                 break;
330             case LeanbackKeyboardController.InputListener.ENTRY_TYPE_VOICE_DISMISS:
331                 ic.performEditorAction(EditorInfo.IME_ACTION_GO);
332                 updateSuggestions = false;
333                 break;
334         }
335 
336         if (mKeyboardController.areSuggestionsEnabled() && updateSuggestions) {
337             mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions());
338         }
339     }
340 
onHideIme()341     public void onHideIme() {
342         requestHideSelf(0);
343     }
344 }
345