1/*
2 * Copyright (C) 2013 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.inputmethod.latin.inputlogic;
18
19import android.os.Handler;
20import android.os.HandlerThread;
21import android.os.Message;
22
23import com.android.inputmethod.compat.LooperCompatUtils;
24import com.android.inputmethod.latin.LatinIME;
25import com.android.inputmethod.latin.SuggestedWords;
26import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
27import com.android.inputmethod.latin.common.InputPointers;
28
29/**
30 * A helper to manage deferred tasks for the input logic.
31 */
32class InputLogicHandler implements Handler.Callback {
33    final Handler mNonUIThreadHandler;
34    // TODO: remove this reference.
35    final LatinIME mLatinIME;
36    final InputLogic mInputLogic;
37    private final Object mLock = new Object();
38    private boolean mInBatchInput; // synchronized using {@link #mLock}.
39
40    private static final int MSG_GET_SUGGESTED_WORDS = 1;
41
42    // A handler that never does anything. This is used for cases where events come before anything
43    // is initialized, though probably only the monkey can actually do this.
44    public static final InputLogicHandler NULL_HANDLER = new InputLogicHandler() {
45        @Override
46        public void reset() {}
47        @Override
48        public boolean handleMessage(final Message msg) { return true; }
49        @Override
50        public void onStartBatchInput() {}
51        @Override
52        public void onUpdateBatchInput(final InputPointers batchPointers,
53                final int sequenceNumber) {}
54        @Override
55        public void onCancelBatchInput() {}
56        @Override
57        public void updateTailBatchInput(final InputPointers batchPointers,
58                final int sequenceNumber) {}
59        @Override
60        public void getSuggestedWords(final int sessionId, final int sequenceNumber,
61                final OnGetSuggestedWordsCallback callback) {}
62    };
63
64    InputLogicHandler() {
65        mNonUIThreadHandler = null;
66        mLatinIME = null;
67        mInputLogic = null;
68    }
69
70    public InputLogicHandler(final LatinIME latinIME, final InputLogic inputLogic) {
71        final HandlerThread handlerThread = new HandlerThread(
72                InputLogicHandler.class.getSimpleName());
73        handlerThread.start();
74        mNonUIThreadHandler = new Handler(handlerThread.getLooper(), this);
75        mLatinIME = latinIME;
76        mInputLogic = inputLogic;
77    }
78
79    public void reset() {
80        mNonUIThreadHandler.removeCallbacksAndMessages(null);
81    }
82
83    // In unit tests, we create several instances of LatinIME, which results in several instances
84    // of InputLogicHandler. To avoid these handlers lingering, we call this.
85    public void destroy() {
86        LooperCompatUtils.quitSafely(mNonUIThreadHandler.getLooper());
87    }
88
89    /**
90     * Handle a message.
91     * @see android.os.Handler.Callback#handleMessage(android.os.Message)
92     */
93    // Called on the Non-UI handler thread by the Handler code.
94    @Override
95    public boolean handleMessage(final Message msg) {
96        switch (msg.what) {
97            case MSG_GET_SUGGESTED_WORDS:
98                mLatinIME.getSuggestedWords(msg.arg1 /* inputStyle */,
99                        msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj);
100                break;
101        }
102        return true;
103    }
104
105    // Called on the UI thread by InputLogic.
106    public void onStartBatchInput() {
107        synchronized (mLock) {
108            mInBatchInput = true;
109        }
110    }
111
112    public boolean isInBatchInput() {
113        return mInBatchInput;
114    }
115
116    /**
117     * Fetch suggestions corresponding to an update of a batch input.
118     * @param batchPointers the updated pointers, including the part that was passed last time.
119     * @param sequenceNumber the sequence number associated with this batch input.
120     * @param isTailBatchInput true if this is the end of a batch input, false if it's an update.
121     */
122    // This method can be called from any thread and will see to it that the correct threads
123    // are used for parts that require it. This method will send a message to the Non-UI handler
124    // thread to pull suggestions, and get the inlined callback to get called on the Non-UI
125    // handler thread. If this is the end of a batch input, the callback will then proceed to
126    // send a message to the UI handler in LatinIME so that showing suggestions can be done on
127    // the UI thread.
128    private void updateBatchInput(final InputPointers batchPointers,
129            final int sequenceNumber, final boolean isTailBatchInput) {
130        synchronized (mLock) {
131            if (!mInBatchInput) {
132                // Batch input has ended or canceled while the message was being delivered.
133                return;
134            }
135            mInputLogic.mWordComposer.setBatchInputPointers(batchPointers);
136            final OnGetSuggestedWordsCallback callback = new OnGetSuggestedWordsCallback() {
137                @Override
138                public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
139                    showGestureSuggestionsWithPreviewVisuals(suggestedWords, isTailBatchInput);
140                }
141            };
142            getSuggestedWords(isTailBatchInput ? SuggestedWords.INPUT_STYLE_TAIL_BATCH
143                    : SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber, callback);
144        }
145    }
146
147    void showGestureSuggestionsWithPreviewVisuals(final SuggestedWords suggestedWordsForBatchInput,
148            final boolean isTailBatchInput) {
149        final SuggestedWords suggestedWordsToShowSuggestions;
150        // We're now inside the callback. This always runs on the Non-UI thread,
151        // no matter what thread updateBatchInput was originally called on.
152        if (suggestedWordsForBatchInput.isEmpty()) {
153            // Use old suggestions if we don't have any new ones.
154            // Previous suggestions are found in InputLogic#mSuggestedWords.
155            // Since these are the most recent ones and we just recomputed
156            // new ones to update them, then the previous ones are there.
157            suggestedWordsToShowSuggestions = mInputLogic.mSuggestedWords;
158        } else {
159            suggestedWordsToShowSuggestions = suggestedWordsForBatchInput;
160        }
161        mLatinIME.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWordsToShowSuggestions,
162                isTailBatchInput /* dismissGestureFloatingPreviewText */);
163        if (isTailBatchInput) {
164            mInBatchInput = false;
165            // The following call schedules onEndBatchInputInternal
166            // to be called on the UI thread.
167            mLatinIME.mHandler.showTailBatchInputResult(suggestedWordsToShowSuggestions);
168        }
169    }
170
171    /**
172     * Update a batch input.
173     *
174     * This fetches suggestions and updates the suggestion strip and the floating text preview.
175     *
176     * @param batchPointers the updated batch pointers.
177     * @param sequenceNumber the sequence number associated with this batch input.
178     */
179    // Called on the UI thread by InputLogic.
180    public void onUpdateBatchInput(final InputPointers batchPointers,
181            final int sequenceNumber) {
182        updateBatchInput(batchPointers, sequenceNumber, false /* isTailBatchInput */);
183    }
184
185    /**
186     * Cancel a batch input.
187     *
188     * Note that as opposed to updateTailBatchInput, we do the UI side of this immediately on the
189     * same thread, rather than get this to call a method in LatinIME. This is because
190     * canceling a batch input does not necessitate the long operation of pulling suggestions.
191     */
192    // Called on the UI thread by InputLogic.
193    public void onCancelBatchInput() {
194        synchronized (mLock) {
195            mInBatchInput = false;
196        }
197    }
198
199    /**
200     * Trigger an update for a tail batch input.
201     *
202     * A tail batch input is the last update for a gesture, the one that is triggered after the
203     * user lifts their finger. This method schedules fetching suggestions on the non-UI thread,
204     * then when the suggestions are computed it comes back on the UI thread to update the
205     * suggestion strip, commit the first suggestion, and dismiss the floating text preview.
206     *
207     * @param batchPointers the updated batch pointers.
208     * @param sequenceNumber the sequence number associated with this batch input.
209     */
210    // Called on the UI thread by InputLogic.
211    public void updateTailBatchInput(final InputPointers batchPointers,
212            final int sequenceNumber) {
213        updateBatchInput(batchPointers, sequenceNumber, true /* isTailBatchInput */);
214    }
215
216    public void getSuggestedWords(final int inputStyle, final int sequenceNumber,
217            final OnGetSuggestedWordsCallback callback) {
218        mNonUIThreadHandler.obtainMessage(
219                MSG_GET_SUGGESTED_WORDS, inputStyle, sequenceNumber, callback).sendToTarget();
220    }
221}
222