blob: bb9d26d3b8a39397c8d388f99f161b14640c09cd [file] [log] [blame]
// Copyright 2010-2015, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package org.mozc.android.inputmethod.japanese.session;
import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface;
import org.mozc.android.inputmethod.japanese.MozcLog;
import org.mozc.android.inputmethod.japanese.MozcUtil;
import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Capability;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Capability.TextDeletionCapabilityType;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Context.InputFieldType;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.GenericStorageEntry;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.GenericStorageEntry.StorageType;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.CommandType;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.InputOrBuilder;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.KeyEvent.SpecialKey;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Output;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.SessionCommand;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.SessionCommand.UsageStatsEvent;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoConfig.Config;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoUserDictionaryStorage.UserDictionaryCommand;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoUserDictionaryStorage.UserDictionaryCommandStatus;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
/**
* This class handles asynchronous and synchronous execution of command evaluation based on
* {@link SessionHandler}.
*
* All execution is done on the single worker thread (not the UI thread). For asynchronous
* execution, an evaluation task is sent to the worker thread, and the method returns immediately.
* For synchronous execution, a task is sent to the worker thread, too, but it waits the
* execution of the task (and pending tasks which have been sent before the task),
* and then returns.
*
*/
public class SessionExecutor {
// At the moment we call mozc server via JNI interface directly,
// while other platforms, e.g. Win/Mac/Linux etc, use IPC.
// In order to keep the call order correctly, we call it from the single worker thread.
// Note that we use well-known double check lazy initialization,
// so that we can inject instances via reflections for testing purposes.
@VisibleForTesting static volatile Optional<SessionExecutor> instance = Optional.absent();
private static SessionExecutor getInstanceInternal(
Optional<SessionHandlerFactory> factory, Context applicationContext) {
Optional<SessionExecutor> result = instance;
if (!result.isPresent()) {
synchronized (SessionExecutor.class) {
result = instance;
if (!result.isPresent()) {
result = instance = Optional.of(new SessionExecutor());
if (factory.isPresent()) {
result.get().reset(factory.get(), applicationContext);
}
}
}
}
return result.get();
}
/**
* Replaces the singleton instance to {@code executor} so {@code getInstance} family
* returns the new instance.
* @param executor the new instance to replace the old one.
* @return the old instance.
*/
@VisibleForTesting public static Optional<SessionExecutor> setInstanceForTest(
Optional<SessionExecutor> executor) {
synchronized (SessionExecutor.class) {
Optional<SessionExecutor> old = instance;
instance = Preconditions.checkNotNull(executor);
return old;
}
}
/**
* Returns an instance of {@link SessionExecutor}.
* This method may return an instance without initialization, assuming it will be initialized
* by client in appropriate timing.
*/
public static SessionExecutor getInstance(Context applicationContext) {
return getInstanceInternal(
Optional.<SessionHandlerFactory>absent(), Preconditions.checkNotNull(applicationContext));
}
/**
* Returns an instance of {@link SessionExecutor}. At first invocation, the instance will be
* initialized by using given {@code factory}. Otherwise, the {@code factory} is simply ignored.
*/
public static SessionExecutor getInstanceInitializedIfNecessary(
SessionHandlerFactory factory, Context applicationContext) {
return getInstanceInternal(
Optional.of(factory), Preconditions.checkNotNull(applicationContext));
}
private static volatile Optional<HandlerThread> sessionHandlerThread = Optional.absent();
private static HandlerThread getHandlerThread() {
Optional<HandlerThread> result = sessionHandlerThread;
if (!result.isPresent()) {
synchronized (SessionExecutor.class) {
result = sessionHandlerThread;
if (!result.isPresent()) {
result = Optional.of(new HandlerThread("Session worker thread"));
result.get().setDaemon(true);
result.get().start();
sessionHandlerThread = result;
}
}
}
return result.get();
}
/**
* An interface to accept the result of asynchronous evaluation.
*/
public interface EvaluationCallback {
void onCompleted(Optional<Command> command, Optional<KeyEventInterface> triggeringKeyEvent);
}
@VisibleForTesting static class AsynchronousEvaluationContext {
// For asynchronous evaluation, we set the sessionId in the callback running on the worker
// thread. So, this class has Input.Builer as an argument for an evaluation, while
// SynchronousEvaluationContext has Input because it is not necessary to be set sessionId.
final long timeStamp;
final Input.Builder inputBuilder;
volatile Optional<Command> outCommand = Optional.absent();
final Optional<KeyEventInterface> triggeringKeyEvent;
final Optional<EvaluationCallback> callback;
final Optional<Handler> callbackHandler;
AsynchronousEvaluationContext(long timeStamp,
Input.Builder inputBuilder,
Optional<KeyEventInterface> triggeringKeyEvent,
Optional<EvaluationCallback> callback,
Optional<Handler> callbackHandler) {
this.timeStamp = timeStamp;
this.inputBuilder = Preconditions.checkNotNull(inputBuilder);
this.triggeringKeyEvent = Preconditions.checkNotNull(triggeringKeyEvent);
this.callback = Preconditions.checkNotNull(callback);
this.callbackHandler = Preconditions.checkNotNull(callbackHandler);
}
}
@VisibleForTesting static class SynchronousEvaluationContext {
final Input input;
volatile Optional<Command> outCommand = Optional.absent();
final CountDownLatch evaluationSynchronizer;
SynchronousEvaluationContext(Input input, CountDownLatch evaluationSynchronizer) {
this.input = Preconditions.checkNotNull(input);
this.evaluationSynchronizer = Preconditions.checkNotNull(evaluationSynchronizer);
}
}
/** Context class just lines handler queue. */
@VisibleForTesting static class KeyEventCallbackContext {
final KeyEventInterface triggeringKeyEvent;
final EvaluationCallback callback;
final Handler callbackHandler;
KeyEventCallbackContext(KeyEventInterface triggeringKeyEvent,
EvaluationCallback callback,
Handler callbackHandler) {
this.triggeringKeyEvent = Preconditions.checkNotNull(triggeringKeyEvent);
this.callback = Preconditions.checkNotNull(callback);
this.callbackHandler = Preconditions.checkNotNull(callbackHandler);
}
}
/**
* A core implementation of evaluation executing process.
*
* <p>This class takes messages from the UI thread. By using {@link SessionHandler},
* it evaluates the {@link Input} in a message, and then returns the result with notifying
* the UI thread if necessary.
* All evaluations should be done with this class in order to keep evaluation in the incoming
* order.
*/
@VisibleForTesting static class ExecutorMainCallback implements Handler.Callback {
/**
* Initializes the currently connected sesion handler.
* We need to initialize the current session handler in the executor thread due to
* performance reason. This message should be run before any other messages.
*/
static final int INITIALIZE_SESSION_HANDLER = 0;
/**
* Deletes the session.
*/
static final int DELETE_SESSION = 1;
/**
* Evaluates the command asynchronously.
*/
static final int EVALUATE_ASYNCHRONOUSLY = 2;
/**
* Evaluates the command asynchronously as similar to {@code EVALUATE_ASYNCHRONOUSLY}.
* The difference from it is this should be used for the actual "key" event, such as
* "hit 'A' key," "hit 'BACKSPACE'", "hit 'SPACEKEY'" or something.
*
* Note: this is used to figure out whether it is ok to skip some heavier instruction
* in the server.
*/
static final int EVALUATE_KEYEVENT_ASYNCHRONOUSLY = 3;
/**
* Evaluates the command synchronously.
*/
static final int EVALUATE_SYNCHRONOUSLY = 4;
/**
* Updates the current request, and notify it to the server.
*/
static final int UPDATE_REQUEST = 5;
/**
* Just pass a message to callback.
*/
static final int PASS_TO_CALLBACK = 6;
@VisibleForTesting static final long INVALID_SESSION_ID = 0;
/**
* A set of CommandType which don't need session id.
*/
private static final Set<CommandType> SESSION_INDEPENDENT_COMMAND_TYPE_SET =
Collections.unmodifiableSet(EnumSet.of(
CommandType.NO_OPERATION,
CommandType.SET_CONFIG,
CommandType.GET_CONFIG,
CommandType.SET_IMPOSED_CONFIG,
CommandType.CLEAR_USER_HISTORY,
CommandType.CLEAR_USER_PREDICTION,
CommandType.CLEAR_UNUSED_USER_PREDICTION,
CommandType.CLEAR_STORAGE,
CommandType.READ_ALL_FROM_STORAGE,
CommandType.RELOAD,
CommandType.SEND_USER_DICTIONARY_COMMAND));
private final SessionHandler sessionHandler;
// Mozc session's ID.
// Set on CREATE_SESSION and will not be updated.
@VisibleForTesting long sessionId = INVALID_SESSION_ID;
@VisibleForTesting Optional<Request.Builder> request = Optional.absent();
// The logging for debugging is disabled by default.
boolean isLogging = false;
@VisibleForTesting ExecutorMainCallback(SessionHandler sessionHandler) {
this.sessionHandler = Preconditions.checkNotNull(sessionHandler);
}
@Override
public boolean handleMessage(Message message) {
Preconditions.checkNotNull(message);
// Dispatch the message.
switch (message.what) {
case INITIALIZE_SESSION_HANDLER:
sessionHandler.initialize(Context.class.cast(message.obj));
break;
case DELETE_SESSION:
deleteSession();
break;
case EVALUATE_ASYNCHRONOUSLY:
case EVALUATE_KEYEVENT_ASYNCHRONOUSLY:
evaluateAsynchronously(
AsynchronousEvaluationContext.class.cast(message.obj), message.getTarget());
break;
case EVALUATE_SYNCHRONOUSLY:
evaluateSynchronously(SynchronousEvaluationContext.class.cast(message.obj));
break;
case UPDATE_REQUEST:
updateRequest(Input.Builder.class.cast(message.obj));
break;
case PASS_TO_CALLBACK:
passToCallBack(KeyEventCallbackContext.class.cast(message.obj));
break;
default:
// We don't process unknown messages.
return false;
}
return true;
}
private Command evaluate(Input input) {
Command inCommand = Command.newBuilder()
.setInput(input)
.setOutput(Output.getDefaultInstance())
.build();
// Note that toString() of ProtocolBuffers' message is very heavy.
if (isLogging) {
MozcCommandDebugger.inLog(inCommand);
}
Command outCommand = sessionHandler.evalCommand(inCommand);
if (isLogging) {
MozcCommandDebugger.outLog(outCommand);
}
return outCommand;
}
@VisibleForTesting void ensureSession() {
if (sessionId != INVALID_SESSION_ID) {
return;
}
// Send CREATE_SESSION command and keep the returned sessionId.
Input input = Input.newBuilder()
.setType(CommandType.CREATE_SESSION)
.setCapability(Capability.newBuilder()
.setTextDeletion(TextDeletionCapabilityType.DELETE_PRECEDING_TEXT))
.build();
sessionId = evaluate(input).getOutput().getId();
// Just after session creation, we send the default "request" to the server,
// with ignoring its result.
// Set mobile dedicated fields, which will not be changed.
// Other fields may be set when the input view is changed.
Request.Builder builder = Request.newBuilder();
MozcUtil.setSoftwareKeyboardRequest(builder);
request = Optional.of(builder);
evaluate(Input.newBuilder()
.setId(sessionId)
.setType(CommandType.SET_REQUEST)
.setRequest(request.get())
.build());
}
void deleteSession() {
if (sessionId == INVALID_SESSION_ID) {
return;
}
Input input = Input.newBuilder()
.setType(CommandType.DELETE_SESSION)
.setId(sessionId)
.build();
evaluate(input);
sessionId = INVALID_SESSION_ID;
request = Optional.<Request.Builder>absent();
}
/**
* Returns {@code true} iff the given {@code output} is squashable by following output.
*
* <p>Here, a squashable output A may be dropped if the next output B is sent before the
* UI thread processes A (for performance reason).
*
* <p>{@link Output} consists from both fields which can be overwritten by following outputs
* (e.g. composing text, a candidate list), and/or ones which cannot be (e.g. conversion
* result). Squashing will happen when an output consists from only overwritable fields and
* then another output, which will overwrite the UI change caused by the older output, comes.
*/
@VisibleForTesting static boolean isSquashableOutput(Output output) {
// - If the output contains a result, it is not squashable because it is necessary
// to commit the string to the client application.
// - If it has deletion_range, it is not squashable either because it is necessary
// to notify the editing to the client application.
// - If the key is not consumed, it is not squashable because it is necessary to pass through
// the event to the client application.
// - If the event doesn't have candidates, it is not squashable because we need to ensure
// the keyboard is visible.
// - Otherwise squashable. An example is toggling a character. It contains neither
// result string nor deletion_range, has candidates (by suggestion) and the keyevent
// is consumed.
return (output.getResult().getValue().length() == 0)
&& !output.hasDeletionRange()
&& output.getConsumed()
&& (output.getAllCandidateWords().getCandidatesCount() > 0);
}
/**
* @return {@code true} if the given {@code inputBuilder} needs to be set session id.
* Otherwise {@code false}.
*/
@VisibleForTesting static boolean isSessionIdRequired(InputOrBuilder input) {
return !SESSION_INDEPENDENT_COMMAND_TYPE_SET.contains(input.getType());
}
@VisibleForTesting void evaluateAsynchronously(AsynchronousEvaluationContext context,
Handler sessionExecutorHandler) {
// Before the evaluation, we remove all pending squashable result callbacks for performance
// reason of less powerful devices.
Input.Builder inputBuilder = context.inputBuilder;
Optional<Handler> callbackHandler = context.callbackHandler;
if (callbackHandler.isPresent()
&& inputBuilder.getCommand().getType() != SessionCommand.CommandType.EXPAND_SUGGESTION) {
// Do not squash by EXPAND_SUGGESTION request, because the result of EXPAND_SUGGESTION
// won't affect the inputConnection in MozcService, as the result should update
// only candidates conceptually.
callbackHandler.get().removeMessages(CallbackHandler.SQUASHABLE_OUTPUT);
}
if (inputBuilder.hasKey() &&
(!inputBuilder.getKey().hasSpecialKey() ||
inputBuilder.getKey().getSpecialKey() == SpecialKey.BACKSPACE) &&
sessionExecutorHandler.hasMessages(EVALUATE_KEYEVENT_ASYNCHRONOUSLY)) {
// Do not request suggestion result, due to performance reason, when:
// - the key is normal key or backspace, and
// - there is (at least one) following event.
inputBuilder.setRequestSuggestion(false);
}
// We set the session id to the input in asynchronous evaluation before the evaluation,
// if necessary.
if (isSessionIdRequired(inputBuilder)) {
ensureSession();
inputBuilder.setId(sessionId);
}
context.outCommand = Optional.of(evaluate(inputBuilder.build()));
// Invoke callback handler if necessary.
if (callbackHandler.isPresent()) {
// For performance reason of, especially, less powerful devices, we want to skip
// rendering whose effect will be overwritten by following (pending) rendering.
// We annotate if the output can be overwritten or not here, so that we can remove
// only those messages in later evaluation.
Output output = context.outCommand.get().getOutput();
Message message = callbackHandler.get().obtainMessage(
isSquashableOutput(output) ? CallbackHandler.SQUASHABLE_OUTPUT
: CallbackHandler.UNSQUASHABLE_OUTPUT,
context);
callbackHandler.get().sendMessage(message);
}
}
@VisibleForTesting void evaluateSynchronously(SynchronousEvaluationContext context) {
Input input = context.input;
Preconditions.checkArgument(
!isSessionIdRequired(input),
"We expect only non-session-id-related input for synchronous evaluation: " + input);
context.outCommand = Optional.of(evaluate(input));
// The client thread is waiting for the evaluation by evaluationSynchronizer,
// so notify the thread via the lock.
context.evaluationSynchronizer.countDown();
}
@VisibleForTesting void updateRequest(Input.Builder inputBuilder) {
ensureSession();
Preconditions.checkState(request.isPresent());
request.get().mergeFrom(inputBuilder.getRequest());
Input input = inputBuilder
.setId(sessionId)
.setType(CommandType.SET_REQUEST)
.setRequest(request.get())
.build();
// Do not render the result because the result does not have preedit.
evaluate(input);
}
@VisibleForTesting void passToCallBack(KeyEventCallbackContext context) {
Handler callbackHandler = context.callbackHandler;
Message message = callbackHandler.obtainMessage(CallbackHandler.UNSQUASHABLE_OUTPUT, context);
callbackHandler.sendMessage(message);
}
}
/**
* A handler to process callback for asynchronous evaluation on the UI thread.
*/
@VisibleForTesting static class CallbackHandler extends Handler {
/**
* The message with this {@code what} cannot be overwritten by following evaluation.
*/
static final int UNSQUASHABLE_OUTPUT = 0;
/**
* The message with this {@code what} may be cancelled by following evaluation
* because the result of the message would be overwritten soon.
*/
static final int SQUASHABLE_OUTPUT = 1;
long cancelTimeStamp = System.nanoTime();
CallbackHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message message) {
if (Preconditions.checkNotNull(message).obj.getClass() == KeyEventCallbackContext.class) {
KeyEventCallbackContext keyEventContext = KeyEventCallbackContext.class.cast(message.obj);
keyEventContext.callback.onCompleted(
Optional.<Command>absent(), Optional.of(keyEventContext.triggeringKeyEvent));
return;
}
AsynchronousEvaluationContext context = AsynchronousEvaluationContext.class.cast(message.obj);
// Note that this method should be run on the UI thread, where removePendingEvaluations runs,
// so we don't need to take a lock here.
if (context.timeStamp - cancelTimeStamp > 0) {
Preconditions.checkState(context.callback.isPresent());
context.callback.get().onCompleted(context.outCommand, context.triggeringKeyEvent);
}
}
}
@VisibleForTesting Optional<Handler> handler = Optional.absent();
private Optional<ExecutorMainCallback> mainCallback = Optional.absent();
private final CallbackHandler callbackHandler;
// Note that theoretically the constructor should be private in order to keep this singleton,
@VisibleForTesting protected SessionExecutor() {
callbackHandler = new CallbackHandler(Looper.getMainLooper());
}
private static void waitForQueueForEmpty(Handler handler) {
final CountDownLatch synchronizer = new CountDownLatch(1);
handler.post(new Runnable() {
@Override
public void run() {
synchronizer.countDown();
}
});
try {
synchronizer.await();
} catch (InterruptedException exception) {
MozcLog.w("waitForQueueForEmpty is interrupted.");
}
}
/**
* Blocks until both incoming and outgoing queues become empty, for testing.
*/
@VisibleForTesting
public void waitForAllQueuesForEmpty() {
Preconditions.checkState(handler.isPresent());
waitForQueueForEmpty(handler.get());
waitForQueueForEmpty(callbackHandler);
}
/**
* Resets the instance by setting {@code SessionHandler} created by the given {@code factory}.
*/
public void reset(SessionHandlerFactory factory, Context applicationContext) {
Preconditions.checkNotNull(factory);
Preconditions.checkNotNull(applicationContext);
HandlerThread thread = getHandlerThread();
mainCallback = Optional.of(new ExecutorMainCallback(factory.create()));
handler = Optional.of(new Handler(thread.getLooper(), mainCallback.get()));
handler.get().sendMessage(handler.get().obtainMessage(
ExecutorMainCallback.INITIALIZE_SESSION_HANDLER, applicationContext));
}
/**
* @param isLogging Set {@code true} if logging of evaluations is needed.
*/
public void setLogging(boolean isLogging) {
if (mainCallback.isPresent()) {
mainCallback.get().isLogging = isLogging;
}
}
/**
* Remove pending evaluations from the pending queue.
*/
public void removePendingEvaluations() {
callbackHandler.cancelTimeStamp = System.nanoTime();
if (handler.isPresent()) {
handler.get().removeMessages(ExecutorMainCallback.EVALUATE_ASYNCHRONOUSLY);
handler.get().removeMessages(ExecutorMainCallback.EVALUATE_KEYEVENT_ASYNCHRONOUSLY);
handler.get().removeMessages(ExecutorMainCallback.EVALUATE_SYNCHRONOUSLY);
handler.get().removeMessages(ExecutorMainCallback.UPDATE_REQUEST);
}
callbackHandler.removeMessages(CallbackHandler.UNSQUASHABLE_OUTPUT);
callbackHandler.removeMessages(CallbackHandler.SQUASHABLE_OUTPUT);
}
public void deleteSession() {
Preconditions.checkState(handler.isPresent());
handler.get().sendMessage(handler.get().obtainMessage(ExecutorMainCallback.DELETE_SESSION));
}
/**
* Evaluates the input caused by triggeringKeyEvent on the JNI worker thread.
* When the evaluation is done, callbackHandler will receive the message with the evaluation
* context.
*
* This method returns immediately, i.e., even after this method's invocation,
* it shouldn't be assumed that the evaluation is done.
*
* @param inputBuilder the input data
* @param triggeringKeyEvent a key event which triggers this evaluation
* @param callback a callback handler if needed
*/
@VisibleForTesting void evaluateAsynchronously(
Input.Builder inputBuilder, Optional<KeyEventInterface> triggeringKeyEvent,
Optional<EvaluationCallback> callback) {
Preconditions.checkState(handler.isPresent());
AsynchronousEvaluationContext context = new AsynchronousEvaluationContext(
System.nanoTime(), Preconditions.checkNotNull(inputBuilder),
Preconditions.checkNotNull(triggeringKeyEvent), Preconditions.checkNotNull(callback),
callback.isPresent() ? Optional.<Handler>of(callbackHandler) : Optional.<Handler>absent());
int type = (triggeringKeyEvent.isPresent())
? ExecutorMainCallback.EVALUATE_KEYEVENT_ASYNCHRONOUSLY
: ExecutorMainCallback.EVALUATE_ASYNCHRONOUSLY;
handler.get().sendMessage(handler.get().obtainMessage(type, context));
}
/**
* Sends {@code SEND_KEY} command to the server asynchronously.
*/
public void sendKey(ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface triggeringKeyEvent,
List<TouchEvent> touchEventList, EvaluationCallback callback) {
Preconditions.checkNotNull(mozcKeyEvent);
Preconditions.checkNotNull(triggeringKeyEvent);
Preconditions.checkNotNull(touchEventList);
Preconditions.checkNotNull(callback);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_KEY)
.setKey(mozcKeyEvent)
.addAllTouchEvents(touchEventList);
evaluateAsynchronously(inputBuilder, Optional.of(triggeringKeyEvent), Optional.of(callback));
}
/**
* Sends {@code SUBMIT} command to the server asynchronously.
*/
public void submit(EvaluationCallback callback) {
Preconditions.checkNotNull(callback);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.SUBMIT));
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.of(callback));
}
/**
* Sends {@code SWITCH_INPUT_MODE} command to the server asynchronously.
*/
public void switchInputMode(Optional<KeyEventInterface> triggeringKeyEvent, CompositionMode mode,
EvaluationCallback callback) {
Preconditions.checkNotNull(triggeringKeyEvent);
Preconditions.checkNotNull(mode);
Preconditions.checkNotNull(callback);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.SWITCH_INPUT_MODE)
.setCompositionMode(mode));
evaluateAsynchronously(inputBuilder, triggeringKeyEvent, Optional.of(callback));
}
/**
* Sends {@code SUBMIT_CANDIDATE} command to the server asynchronously.
*/
public void submitCandidate(int candidateId, Optional<Integer> rowIndex,
EvaluationCallback callback) {
Preconditions.checkNotNull(rowIndex);
Preconditions.checkNotNull(callback);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.SUBMIT_CANDIDATE)
.setId(candidateId));
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.of(callback));
if (rowIndex.isPresent()) {
candidateSubmissionStatsEvent(rowIndex.get());
}
}
private void candidateSubmissionStatsEvent(int rowIndex) {
Preconditions.checkArgument(rowIndex >= 0);
UsageStatsEvent event;
switch (rowIndex) {
case 0:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_0;
break;
case 1:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_1;
break;
case 2:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_2;
break;
case 3:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_3;
break;
case 4:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_4;
break;
case 5:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_5;
break;
case 6:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_6;
break;
case 7:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_7;
break;
case 8:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_8;
break;
case 9:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_9;
break;
default:
event = UsageStatsEvent.SUBMITTED_CANDIDATE_ROW_GE10;
}
sendUsageStatsEvent(event);
}
/**
* Sends {@code RESET_CONTEXT} command to the server asynchronously.
*/
public void resetContext() {
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.RESET_CONTEXT));
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.<EvaluationCallback>absent());
}
/**
* Sends {@code MOVE_CURSOR} command to the server asynchronously.
*/
public void moveCursor(int cursorPosition, EvaluationCallback callback) {
Preconditions.checkNotNull(callback);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(ProtoCommands.SessionCommand.CommandType.MOVE_CURSOR)
.setCursorPosition(cursorPosition));
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.of(callback));
}
/**
* Sends {@code CONVERT_NEXT_PAGE} command to the server asynchronously.
*/
public void pageDown(EvaluationCallback callback) {
Preconditions.checkNotNull(callback);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.CONVERT_NEXT_PAGE));
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.of(callback));
}
/**
* Sends {@code CONVERT_PREV_PAGE} command to the server asynchronously.
*/
public void pageUp(EvaluationCallback callback) {
Preconditions.checkNotNull(callback);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.CONVERT_PREV_PAGE));
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.of(callback));
}
/**
* Sends {@code SWITCH_INPUT_FIELD_TYPE} command to the server asynchronously.
*/
public void switchInputFieldType(InputFieldType inputFieldType) {
Preconditions.checkNotNull(inputFieldType);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(ProtoCommands.SessionCommand.CommandType.SWITCH_INPUT_FIELD_TYPE))
.setContext(ProtoCommands.Context.newBuilder()
.setInputFieldType(inputFieldType));
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.<EvaluationCallback>absent());
}
/**
* Sends {@code UNDO_OR_REWIND} command to the server asynchronously.
*/
public void undoOrRewind(List<TouchEvent> touchEventList, EvaluationCallback callback) {
Preconditions.checkNotNull(touchEventList);
Preconditions.checkNotNull(callback);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.UNDO_OR_REWIND))
.addAllTouchEvents(touchEventList);
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.of(callback));
}
/**
* Sends {@code INSERT_TO_STORAGE} with given {@code type}, {@code key} and {@code values}
* to the server asynchronously.
*/
public void insertToStorage(StorageType type, String key, List<ByteString> values) {
Preconditions.checkNotNull(type);
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(values);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.INSERT_TO_STORAGE)
.setStorageEntry(GenericStorageEntry.newBuilder()
.setType(type)
.setKey(key)
.addAllValue(values));
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.<EvaluationCallback>absent());
}
public void expandSuggestion(EvaluationCallback callback) {
Preconditions.checkNotNull(callback);
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(
SessionCommand.newBuilder().setType(SessionCommand.CommandType.EXPAND_SUGGESTION));
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.of(callback));
}
public void preferenceUsageStatsEvent(SharedPreferences sharedPreferences, Resources resources) {
Preconditions.checkNotNull(sharedPreferences);
Preconditions.checkNotNull(resources);
ClientSidePreference landscapePreference =
new ClientSidePreference(
sharedPreferences, resources, Configuration.ORIENTATION_LANDSCAPE);
evaluateAsynchronously(
Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.USAGE_STATS_EVENT)
.setUsageStatsEvent(UsageStatsEvent.SOFTWARE_KEYBOARD_LAYOUT_LANDSCAPE)
.setUsageStatsEventIntValue(landscapePreference.getKeyboardLayout().getId())),
Optional.<KeyEventInterface>absent(), Optional.<EvaluationCallback>absent());
ClientSidePreference portraitPreference =
new ClientSidePreference(
sharedPreferences, resources, Configuration.ORIENTATION_PORTRAIT);
evaluateAsynchronously(
Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.USAGE_STATS_EVENT)
.setUsageStatsEvent(UsageStatsEvent.SOFTWARE_KEYBOARD_LAYOUT_PORTRAIT)
.setUsageStatsEventIntValue(portraitPreference.getKeyboardLayout().getId())),
Optional.<KeyEventInterface>absent(), Optional.<EvaluationCallback>absent());
}
public void touchEventUsageStatsEvent(List<TouchEvent> touchEventList) {
if (Preconditions.checkNotNull(touchEventList).isEmpty()) {
return;
}
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.USAGE_STATS_EVENT))
.addAllTouchEvents(touchEventList);
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.<EvaluationCallback>absent());
}
public void syncData() {
Input.Builder inputBuilder = Input.newBuilder()
.setType(CommandType.SYNC_DATA);
evaluateAsynchronously(
inputBuilder, Optional.<KeyEventInterface>absent(), Optional.<EvaluationCallback>absent());
}
/**
* Evaluates the input on the JNI worker thread, and wait that the evaluation is done.
* This method blocks (typically <30ms).
*/
@VisibleForTesting Output evaluateSynchronously(Input input) {
Preconditions.checkState(handler.isPresent());
CountDownLatch evaluationSynchronizer = new CountDownLatch(1);
SynchronousEvaluationContext context =
new SynchronousEvaluationContext(input, evaluationSynchronizer);
handler.get().sendMessage(handler.get().obtainMessage(
ExecutorMainCallback.EVALUATE_SYNCHRONOUSLY, context));
try {
evaluationSynchronizer.await();
} catch (InterruptedException exception) {
MozcLog.w("Session thread is interrupted during evaluation.");
}
return context.outCommand.get().getOutput();
}
/**
* Gets a config from the server.
*/
public Config getConfig() {
Input input = Input.newBuilder().setType(Input.CommandType.GET_CONFIG).build();
return evaluateSynchronously(input).getConfig();
}
/**
* Sets the given {@code config} to the server.
*/
public void setConfig(Config config) {
Preconditions.checkNotNull(config);
// Ignore output.
evaluateAsynchronously(
Input.newBuilder().setType(Input.CommandType.SET_CONFIG).setConfig(config),
Optional.<KeyEventInterface>absent(), Optional.<EvaluationCallback>absent());
}
/**
* Sets the given {@code config} to the server as imposed config.
*/
public void setImposedConfig(Config config) {
Preconditions.checkNotNull(config);
// Ignore output.
evaluateAsynchronously(
Input.newBuilder().setType(Input.CommandType.SET_IMPOSED_CONFIG).setConfig(config),
Optional.<KeyEventInterface>absent(), Optional.<EvaluationCallback>absent());
}
/**
* Clears the unused user prediction from the server synchronously.
*/
public void clearUnusedUserPrediction() {
evaluateSynchronously(
Input.newBuilder().setType(CommandType.CLEAR_UNUSED_USER_PREDICTION).build());
}
/**
* Clears the user's history from the server synchronously.
*/
public void clearUserHistory() {
evaluateSynchronously(
Input.newBuilder().setType(CommandType.CLEAR_USER_HISTORY).build());
}
/**
* Clears user's prediction from the server synchronously.
*/
public void clearUserPrediction() {
evaluateSynchronously(
Input.newBuilder().setType(CommandType.CLEAR_USER_PREDICTION).build());
}
/**
* Clears a generic storage, which is used typically by symbol history.
* @param storageType the storage to be cleared
*/
public void clearStorage(StorageType storageType) {
Preconditions.checkNotNull(storageType);
evaluateSynchronously(
Input.newBuilder()
.setType(CommandType.CLEAR_STORAGE)
.setStorageEntry(GenericStorageEntry.newBuilder().setType(storageType))
.build());
}
/**
* Reads stored values of the given {@code type} from the server, and returns it.
*/
public List<ByteString> readAllFromStorage(StorageType type) {
Preconditions.checkNotNull(type);
Input input = Input.newBuilder()
.setType(CommandType.READ_ALL_FROM_STORAGE)
.setStorageEntry(GenericStorageEntry.newBuilder()
.setType(type))
.build();
Output output = evaluateSynchronously(input);
return output.getStorageEntry().getValueList();
}
/**
* Sends 'Reload' request to the server, typically for reloading user dictionaries.
*/
public void reload() {
// Ignore output.
evaluateAsynchronously(
Input.newBuilder().setType(Input.CommandType.RELOAD), Optional.<KeyEventInterface>absent(),
Optional.<EvaluationCallback>absent());
}
/**
* Sends SEND_USER_DICTIONARY_COMMAND to edit user dictionaries.
*/
public UserDictionaryCommandStatus sendUserDictionaryCommand(UserDictionaryCommand command) {
Preconditions.checkNotNull(command);
Output output = evaluateSynchronously(Input.newBuilder()
.setType(CommandType.SEND_USER_DICTIONARY_COMMAND)
.setUserDictionaryCommand(command)
.build());
return output.getUserDictionaryCommandStatus();
}
/**
* Sends an UPDATE_REQUEST command to the evaluation thread.
*/
public void updateRequest(Request update, List<TouchEvent> touchEventList) {
Preconditions.checkNotNull(update);
Preconditions.checkNotNull(touchEventList);
Preconditions.checkState(handler.isPresent());
Input.Builder inputBuilder = Input.newBuilder()
.setRequest(update)
.addAllTouchEvents(touchEventList);
handler.get().sendMessage(
handler.get().obtainMessage(ExecutorMainCallback.UPDATE_REQUEST, inputBuilder));
}
public void sendKeyEvent(KeyEventInterface triggeringKeyEvent, EvaluationCallback callback) {
Preconditions.checkNotNull(triggeringKeyEvent);
Preconditions.checkNotNull(callback);
Preconditions.checkState(handler.isPresent());
KeyEventCallbackContext context =
new KeyEventCallbackContext(triggeringKeyEvent, callback, callbackHandler);
handler.get().sendMessage(
handler.get().obtainMessage(ExecutorMainCallback.PASS_TO_CALLBACK, context));
}
public void sendUsageStatsEvent(UsageStatsEvent event) {
evaluateAsynchronously(
Input.newBuilder()
.setType(CommandType.SEND_COMMAND)
.setCommand(SessionCommand.newBuilder()
.setType(SessionCommand.CommandType.USAGE_STATS_EVENT)
.setUsageStatsEvent(event)),
Optional.<KeyEventInterface>absent(), Optional.<EvaluationCallback>absent());
}
}