1/*
2 * Copyright (C) 2008 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
17package com.android.internal.view;
18
19import android.os.Bundle;
20import android.os.RemoteException;
21import android.os.SystemClock;
22import android.util.Log;
23import android.view.KeyEvent;
24import android.view.inputmethod.CompletionInfo;
25import android.view.inputmethod.CorrectionInfo;
26import android.view.inputmethod.ExtractedText;
27import android.view.inputmethod.ExtractedTextRequest;
28import android.view.inputmethod.InputConnection;
29
30public class InputConnectionWrapper implements InputConnection {
31    private static final int MAX_WAIT_TIME_MILLIS = 2000;
32    private final IInputContext mIInputContext;
33
34    static class InputContextCallback extends IInputContextCallback.Stub {
35        private static final String TAG = "InputConnectionWrapper.ICC";
36        public int mSeq;
37        public boolean mHaveValue;
38        public CharSequence mTextBeforeCursor;
39        public CharSequence mTextAfterCursor;
40        public CharSequence mSelectedText;
41        public ExtractedText mExtractedText;
42        public int mCursorCapsMode;
43
44        // A 'pool' of one InputContextCallback.  Each ICW request will attempt to gain
45        // exclusive access to this object.
46        private static InputContextCallback sInstance = new InputContextCallback();
47        private static int sSequenceNumber = 1;
48
49        /**
50         * Returns an InputContextCallback object that is guaranteed not to be in use by
51         * any other thread.  The returned object's 'have value' flag is cleared and its expected
52         * sequence number is set to a new integer.  We use a sequence number so that replies that
53         * occur after a timeout has expired are not interpreted as replies to a later request.
54         */
55        private static InputContextCallback getInstance() {
56            synchronized (InputContextCallback.class) {
57                // Return sInstance if it's non-null, otherwise construct a new callback
58                InputContextCallback callback;
59                if (sInstance != null) {
60                    callback = sInstance;
61                    sInstance = null;
62
63                    // Reset the callback
64                    callback.mHaveValue = false;
65                } else {
66                    callback = new InputContextCallback();
67                }
68
69                // Set the sequence number
70                callback.mSeq = sSequenceNumber++;
71                return callback;
72            }
73        }
74
75        /**
76         * Makes the given InputContextCallback available for use in the future.
77         */
78        private void dispose() {
79            synchronized (InputContextCallback.class) {
80                // If sInstance is non-null, just let this object be garbage-collected
81                if (sInstance == null) {
82                    // Allow any objects being held to be gc'ed
83                    mTextAfterCursor = null;
84                    mTextBeforeCursor = null;
85                    mExtractedText = null;
86                    sInstance = this;
87                }
88            }
89        }
90
91        public void setTextBeforeCursor(CharSequence textBeforeCursor, int seq) {
92            synchronized (this) {
93                if (seq == mSeq) {
94                    mTextBeforeCursor = textBeforeCursor;
95                    mHaveValue = true;
96                    notifyAll();
97                } else {
98                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
99                            + ") in setTextBeforeCursor, ignoring.");
100                }
101            }
102        }
103
104        public void setTextAfterCursor(CharSequence textAfterCursor, int seq) {
105            synchronized (this) {
106                if (seq == mSeq) {
107                    mTextAfterCursor = textAfterCursor;
108                    mHaveValue = true;
109                    notifyAll();
110                } else {
111                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
112                            + ") in setTextAfterCursor, ignoring.");
113                }
114            }
115        }
116
117        public void setSelectedText(CharSequence selectedText, int seq) {
118            synchronized (this) {
119                if (seq == mSeq) {
120                    mSelectedText = selectedText;
121                    mHaveValue = true;
122                    notifyAll();
123                } else {
124                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
125                            + ") in setSelectedText, ignoring.");
126                }
127            }
128        }
129
130        public void setCursorCapsMode(int capsMode, int seq) {
131            synchronized (this) {
132                if (seq == mSeq) {
133                    mCursorCapsMode = capsMode;
134                    mHaveValue = true;
135                    notifyAll();
136                } else {
137                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
138                            + ") in setCursorCapsMode, ignoring.");
139                }
140            }
141        }
142
143        public void setExtractedText(ExtractedText extractedText, int seq) {
144            synchronized (this) {
145                if (seq == mSeq) {
146                    mExtractedText = extractedText;
147                    mHaveValue = true;
148                    notifyAll();
149                } else {
150                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
151                            + ") in setExtractedText, ignoring.");
152                }
153            }
154        }
155
156        /**
157         * Waits for a result for up to {@link #MAX_WAIT_TIME_MILLIS} milliseconds.
158         *
159         * <p>The caller must be synchronized on this callback object.
160         */
161        void waitForResultLocked() {
162            long startTime = SystemClock.uptimeMillis();
163            long endTime = startTime + MAX_WAIT_TIME_MILLIS;
164
165            while (!mHaveValue) {
166                long remainingTime = endTime - SystemClock.uptimeMillis();
167                if (remainingTime <= 0) {
168                    Log.w(TAG, "Timed out waiting on IInputContextCallback");
169                    return;
170                }
171                try {
172                    wait(remainingTime);
173                } catch (InterruptedException e) {
174                }
175            }
176        }
177    }
178
179    public InputConnectionWrapper(IInputContext inputContext) {
180        mIInputContext = inputContext;
181    }
182
183    public CharSequence getTextAfterCursor(int length, int flags) {
184        CharSequence value = null;
185        try {
186            InputContextCallback callback = InputContextCallback.getInstance();
187            mIInputContext.getTextAfterCursor(length, flags, callback.mSeq, callback);
188            synchronized (callback) {
189                callback.waitForResultLocked();
190                if (callback.mHaveValue) {
191                    value = callback.mTextAfterCursor;
192                }
193            }
194            callback.dispose();
195        } catch (RemoteException e) {
196            return null;
197        }
198        return value;
199    }
200
201    public CharSequence getTextBeforeCursor(int length, int flags) {
202        CharSequence value = null;
203        try {
204            InputContextCallback callback = InputContextCallback.getInstance();
205            mIInputContext.getTextBeforeCursor(length, flags, callback.mSeq, callback);
206            synchronized (callback) {
207                callback.waitForResultLocked();
208                if (callback.mHaveValue) {
209                    value = callback.mTextBeforeCursor;
210                }
211            }
212            callback.dispose();
213        } catch (RemoteException e) {
214            return null;
215        }
216        return value;
217    }
218
219    public CharSequence getSelectedText(int flags) {
220        CharSequence value = null;
221        try {
222            InputContextCallback callback = InputContextCallback.getInstance();
223            mIInputContext.getSelectedText(flags, callback.mSeq, callback);
224            synchronized (callback) {
225                callback.waitForResultLocked();
226                if (callback.mHaveValue) {
227                    value = callback.mSelectedText;
228                }
229            }
230            callback.dispose();
231        } catch (RemoteException e) {
232            return null;
233        }
234        return value;
235    }
236
237    public int getCursorCapsMode(int reqModes) {
238        int value = 0;
239        try {
240            InputContextCallback callback = InputContextCallback.getInstance();
241            mIInputContext.getCursorCapsMode(reqModes, callback.mSeq, callback);
242            synchronized (callback) {
243                callback.waitForResultLocked();
244                if (callback.mHaveValue) {
245                    value = callback.mCursorCapsMode;
246                }
247            }
248            callback.dispose();
249        } catch (RemoteException e) {
250            return 0;
251        }
252        return value;
253    }
254
255    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
256        ExtractedText value = null;
257        try {
258            InputContextCallback callback = InputContextCallback.getInstance();
259            mIInputContext.getExtractedText(request, flags, callback.mSeq, callback);
260            synchronized (callback) {
261                callback.waitForResultLocked();
262                if (callback.mHaveValue) {
263                    value = callback.mExtractedText;
264                }
265            }
266            callback.dispose();
267        } catch (RemoteException e) {
268            return null;
269        }
270        return value;
271    }
272
273    public boolean commitText(CharSequence text, int newCursorPosition) {
274        try {
275            mIInputContext.commitText(text, newCursorPosition);
276            return true;
277        } catch (RemoteException e) {
278            return false;
279        }
280    }
281
282    public boolean commitCompletion(CompletionInfo text) {
283        try {
284            mIInputContext.commitCompletion(text);
285            return true;
286        } catch (RemoteException e) {
287            return false;
288        }
289    }
290
291    public boolean commitCorrection(CorrectionInfo correctionInfo) {
292        try {
293            mIInputContext.commitCorrection(correctionInfo);
294            return true;
295        } catch (RemoteException e) {
296            return false;
297        }
298    }
299
300    public boolean setSelection(int start, int end) {
301        try {
302            mIInputContext.setSelection(start, end);
303            return true;
304        } catch (RemoteException e) {
305            return false;
306        }
307    }
308
309    public boolean performEditorAction(int actionCode) {
310        try {
311            mIInputContext.performEditorAction(actionCode);
312            return true;
313        } catch (RemoteException e) {
314            return false;
315        }
316    }
317
318    public boolean performContextMenuAction(int id) {
319        try {
320            mIInputContext.performContextMenuAction(id);
321            return true;
322        } catch (RemoteException e) {
323            return false;
324        }
325    }
326
327    public boolean setComposingRegion(int start, int end) {
328        try {
329            mIInputContext.setComposingRegion(start, end);
330            return true;
331        } catch (RemoteException e) {
332            return false;
333        }
334    }
335
336    public boolean setComposingText(CharSequence text, int newCursorPosition) {
337        try {
338            mIInputContext.setComposingText(text, newCursorPosition);
339            return true;
340        } catch (RemoteException e) {
341            return false;
342        }
343    }
344
345    public boolean finishComposingText() {
346        try {
347            mIInputContext.finishComposingText();
348            return true;
349        } catch (RemoteException e) {
350            return false;
351        }
352    }
353
354    public boolean beginBatchEdit() {
355        try {
356            mIInputContext.beginBatchEdit();
357            return true;
358        } catch (RemoteException e) {
359            return false;
360        }
361    }
362
363    public boolean endBatchEdit() {
364        try {
365            mIInputContext.endBatchEdit();
366            return true;
367        } catch (RemoteException e) {
368            return false;
369        }
370    }
371
372    public boolean sendKeyEvent(KeyEvent event) {
373        try {
374            mIInputContext.sendKeyEvent(event);
375            return true;
376        } catch (RemoteException e) {
377            return false;
378        }
379    }
380
381    public boolean clearMetaKeyStates(int states) {
382        try {
383            mIInputContext.clearMetaKeyStates(states);
384            return true;
385        } catch (RemoteException e) {
386            return false;
387        }
388    }
389
390    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
391        try {
392            mIInputContext.deleteSurroundingText(beforeLength, afterLength);
393            return true;
394        } catch (RemoteException e) {
395            return false;
396        }
397    }
398
399    public boolean reportFullscreenMode(boolean enabled) {
400        try {
401            mIInputContext.reportFullscreenMode(enabled);
402            return true;
403        } catch (RemoteException e) {
404            return false;
405        }
406    }
407
408    public boolean performPrivateCommand(String action, Bundle data) {
409        try {
410            mIInputContext.performPrivateCommand(action, data);
411            return true;
412        } catch (RemoteException e) {
413            return false;
414        }
415    }
416}
417