blob: 1401c95f2a3a09e4ae759b8d54428f8b2428e20f [file] [log] [blame]
// Copyright 2010-2014, 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.keyboard;
import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
import org.mozc.android.inputmethod.japanese.keyboard.ProbableKeyEventGuesser.LikelihoodCalculator;
import org.mozc.android.inputmethod.japanese.keyboard.ProbableKeyEventGuesser.LikelihoodCalculatorImpl;
import org.mozc.android.inputmethod.japanese.keyboard.ProbableKeyEventGuesser.StatisticsLoader;
import org.mozc.android.inputmethod.japanese.keyboard.ProbableKeyEventGuesser.StatisticsLoader.UpdateStatsListener;
import org.mozc.android.inputmethod.japanese.keyboard.ProbableKeyEventGuesser.StatsFileAccessor;
import org.mozc.android.inputmethod.japanese.keyboard.ProbableKeyEventGuesser.StatsFileAccessorImpl;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchAction;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchPosition;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.KeyEvent.ProbableKeyEvent;
import org.mozc.android.inputmethod.japanese.testing.InstrumentationTestCaseWithMock;
import org.mozc.android.inputmethod.japanese.testing.Parameter;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import android.content.res.Configuration;
import android.test.MoreAsserts;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.SparseArray;
import org.easymock.Capture;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
*/
public class ProbableKeyEventGuesserTest extends InstrumentationTestCaseWithMock {
private static class BlockingThreadPoolExecutor extends ThreadPoolExecutor {
public BlockingThreadPoolExecutor() {
super(0, 1, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1));
}
@Override
public boolean remove(Runnable task) {
return true;
}
@Override
public void execute(Runnable command) {
command.run();
}
}
private static class BlockingExecutor implements Executor {
@Override
public void execute(Runnable command) {
command.run();
}
}
private static final List<TouchEvent> TOUCH_DOWN_UP_EVENT_LIST =
Collections.singletonList(
TouchEvent.newBuilder()
.addStroke(TouchPosition.newBuilder().setAction(TouchAction.TOUCH_DOWN))
.addStroke(TouchPosition.newBuilder().setAction(TouchAction.TOUCH_UP))
.build());
private static String getFormattedKeyboardName(Keyboard keyboard,
Configuration configuration) {
return keyboard.getSpecification().getKeyboardSpecificationName()
.formattedKeyboardName(configuration);
}
private static InputStream createStream(int[] sourceIds, float[][] stats) {
ByteArrayDataOutput b = ByteStreams.newDataOutput();
b.writeInt(stats.length);
if (sourceIds.length != stats.length) {
throw new IllegalArgumentException("sourceIds and stats must be the same size.");
}
for (int i = 0; i < sourceIds.length; ++i) {
b.writeInt(sourceIds[i]);
float[] dataList = stats[i];
for (float data : dataList) {
b.writeFloat(data);
}
}
return new ByteArrayInputStream(b.toByteArray());
}
private ProbableKeyEventGuesser createFakeGuesser(double threshold) {
return new ProbableKeyEventGuesser(
new StatsFileAccessorImpl(getInstrumentation().getTargetContext().getAssets()),
threshold,
new BlockingThreadPoolExecutor(),
new BlockingExecutor(),
new LikelihoodCalculatorImpl());
}
@SmallTest
public void testStaticticsLoader_run() {
int[] sourceIdList = {10, 20};
float[][] statsList = {
{1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f},
{11f, 12f, 13f, 14f, 15f, 16f, 17f, 18f}};
final InputStream testStream = createStream(sourceIdList, statsList);
final Configuration configuration = new Configuration();
configuration.orientation = Configuration.ORIENTATION_LANDSCAPE;
final Keyboard keyboard = KeyboardTest.createKeyboard(
KeyboardSpecification.GODAN_KANA, getInstrumentation());
final Capture<SparseArray<float[]>> capture = new Capture<SparseArray<float[]>>();
StatisticsLoader loader = new StatisticsLoader(
new StatsFileAccessor() {
@Override
public InputStream openStream(
Keyboard keyboardToLoad, Configuration configurationToLoad) {
assertSame(keyboard, keyboardToLoad);
assertSame(configuration, configurationToLoad);
return testStream;
}},
keyboard,
configuration,
new UpdateStatsListener() {
@Override
public void updateStats(String formattedKeyboardName, SparseArray<float[]> stats) {
assertEquals(
getFormattedKeyboardName(keyboard, configuration),
formattedKeyboardName);
capture.setValue(stats);
}
});
loader.run();
assertEquals(sourceIdList.length, capture.getValue().size());
for (int i = 0; i < sourceIdList.length; ++i) {
float[] expectation = statsList[i];
float[] actual = capture.getValue().get(sourceIdList[i]);
assertNotNull(actual);
for (int j = 0; j < expectation.length; ++j) {
assertEquals(expectation[j], actual[j]);
}
}
}
@SmallTest
public void testStaticticsLoader_run_loadNonExistentFile() {
Configuration configuration = new Configuration();
configuration.orientation = Configuration.ORIENTATION_LANDSCAPE;
StatisticsLoader loader = new StatisticsLoader(
new StatsFileAccessor() {
@Override
public InputStream openStream(
Keyboard keyboard, Configuration configuration) throws IOException {
throw new IOException("No file found");
}
},
KeyboardTest.createKeyboard(KeyboardSpecification.GODAN_KANA, getInstrumentation()),
configuration,
new UpdateStatsListener() {
@Override
public void updateStats(String formattedKeyboardName, SparseArray<float[]> stats) {
fail("Should not be called.");
}
});
loader.run();
}
@SuppressWarnings("unchecked")
@SmallTest
public void testStaticticsLoader_run_IOException() {
Configuration configuration = new Configuration();
configuration.orientation = Configuration.ORIENTATION_LANDSCAPE;
// If IOException is thrown on stream access, no update should happen.
StatisticsLoader loader = new StatisticsLoader(
new StatsFileAccessor() {
@Override
public InputStream openStream(
Keyboard keyboard, Configuration configuration) {
return new InputStream() {
@Override
public int read() throws IOException {
throw new IOException();
}
};
}},
KeyboardTest.createKeyboard(
KeyboardSpecification.GODAN_KANA, getInstrumentation()),
configuration,
new UpdateStatsListener() {
@Override
public void updateStats(String formattedKeyboardName, SparseArray<float[]> stats) {
fail("Should not be called.");
}
});
loader.run();
}
@SmallTest
public void testGetLikelihood() {
LikelihoodCalculator calculator = new LikelihoodCalculatorImpl();
// The result itself is not tested.
// Here we test "more likely touch point returns higher likelihood".
float[] probableEvent = {0f, 0f, 1f, 1f, 0f, 0f, 1f, 1f, 1f};
// <- high (near) low (far) ->
float[] testData = {0f, +0.1f, -0.2f, +0.4f, -0.8f, +1f, -1.2f};
for (int i = 0; i < testData.length; ++i) {
for (int j = i; j < testData.length; ++j) {
String msg = i + " vs " + j;
float valueI = testData[i];
float valueJ = testData[j];
assertTrue(msg + ", sx",
calculator.getLikelihood(valueI, 0f, 0f, 0f, probableEvent)
>= calculator.getLikelihood(valueJ, 0f, 0f, 0f, probableEvent));
assertTrue(msg + ", sy",
calculator.getLikelihood(0f, valueI, 0f, 0f, probableEvent)
>= calculator.getLikelihood(0f, valueJ, 0f, 0f, probableEvent));
assertTrue(msg + ", dx",
calculator.getLikelihood(0f, 0f, valueI, 0f, probableEvent)
>= calculator.getLikelihood(0f, 0f, valueJ, 0f, probableEvent));
assertTrue(msg + ", dy",
calculator.getLikelihood(0f, 0f, 0f, valueI, probableEvent)
>= calculator.getLikelihood(0f, 0f, 0f, valueJ, probableEvent));
}
}
}
@SmallTest
public void testConstructor() {
try {
// TODO(matsuzakit): Introduce NullPointerTester. Unfortunately it's not runnable on Android.
new ProbableKeyEventGuesser(null);
fail("Non-null assetManager shouldn't be accepted.");
} catch (NullPointerException e) {
// Expected.
}
new ProbableKeyEventGuesser(getInstrumentation().getTargetContext().getAssets());
}
@SmallTest
public void testSetJapaneseKeyboardWithNullConfig() {
Keyboard godanKana = KeyboardTest.createKeyboard(
KeyboardSpecification.GODAN_KANA, getInstrumentation());
ProbableKeyEventGuesser guesser = createFakeGuesser(0);
// JapaneseKeyboard == godanKana
// Configuration == null
guesser.setKeyboard(godanKana);
guesser.setConfiguration(Optional.<Configuration>absent());
MoreAsserts.assertEmpty(
guesser.getProbableKeyEvents(Arrays.asList(TouchEvent.getDefaultInstance())));
}
@SmallTest
public void testNonExistentJanapaneseKeyboard() {
// This test expects that HARDWARE_QWERTY_ALPHABET doesn't have corresponding
// typing correction stats.
Keyboard keyboard =
new Keyboard(
Optional.<String>absent(),
Collections.<Row>emptyList(), 0f, KeyboardSpecification.HARDWARE_QWERTY_ALPHABET);
Configuration configuration = new Configuration();
configuration.orientation = Configuration.ORIENTATION_LANDSCAPE;
ProbableKeyEventGuesser guesser = createFakeGuesser(0);
guesser.setConfiguration(Optional.of(configuration));
guesser.setKeyboard(keyboard);
MoreAsserts.assertEmpty(
guesser.getProbableKeyEvents(Arrays.asList(TouchEvent.getDefaultInstance())));
}
@SmallTest
public void testCorrectKeyboard() {
Keyboard godanKana = KeyboardTest.createKeyboard(
KeyboardSpecification.GODAN_KANA, getInstrumentation());
Configuration configuration = new Configuration();
configuration.orientation = Configuration.ORIENTATION_LANDSCAPE;
ProbableKeyEventGuesser guesser = createFakeGuesser(Double.NEGATIVE_INFINITY);
guesser.setConfiguration(Optional.of(configuration));
guesser.setKeyboard(godanKana);
MoreAsserts.assertNotEmpty(guesser.getProbableKeyEvents(TOUCH_DOWN_UP_EVENT_LIST));
}
@SmallTest
public void testFilterLessProbableEvents() {
Keyboard godanKana = KeyboardTest.createKeyboard(
KeyboardSpecification.GODAN_KANA, getInstrumentation());
Configuration configuration = new Configuration();
configuration.orientation = Configuration.ORIENTATION_LANDSCAPE;
ProbableKeyEventGuesser guesser = createFakeGuesser(Double.POSITIVE_INFINITY);
guesser.setConfiguration(Optional.of(configuration));
guesser.setKeyboard(godanKana);
MoreAsserts.assertEmpty(guesser.getProbableKeyEvents(TOUCH_DOWN_UP_EVENT_LIST));
}
@SmallTest
public void testEmptyTouchEvent() {
Keyboard godanKana = KeyboardTest.createKeyboard(
KeyboardSpecification.GODAN_KANA, getInstrumentation());
Configuration configuration = new Configuration();
configuration.orientation = Configuration.ORIENTATION_LANDSCAPE;
ProbableKeyEventGuesser guesser = createFakeGuesser(0);
guesser.setConfiguration(Optional.of(configuration));
guesser.setKeyboard(godanKana);
MoreAsserts.assertEmpty(guesser.getProbableKeyEvents(Collections.<TouchEvent>emptyList()));
MoreAsserts.assertEmpty(guesser.getProbableKeyEvents(
Collections.<TouchEvent>singletonList(TouchEvent.getDefaultInstance())));
}
@SmallTest
public void testVariousStatistics() {
final Keyboard godanKana = KeyboardTest.createKeyboard(
KeyboardSpecification.GODAN_KANA, getInstrumentation());
final Configuration configuration = new Configuration();
configuration.orientation = Configuration.ORIENTATION_LANDSCAPE;
class TestData extends Parameter {
int[] sourceIds;
float[][] stats;
List<ProbableKeyEvent> expectation;
TestData(int[] sourceIds, float[][] stats, List<ProbableKeyEvent> expectation) {
this.sourceIds = sourceIds;
this.stats = stats;
this.expectation = expectation;
}
}
final float startX = 1;
final float startY = 2;
final float deltaX = 3;
final float deltaY = 4;
LikelihoodCalculator fakeCalculator = new LikelihoodCalculator() {
@Override
public double getLikelihood(
float sx, float sy, float dx, float dy, float[] probableEvent) {
assertEquals(startX, sx);
assertEquals(startY, sy);
assertEquals(deltaX, dx);
assertEquals(deltaY, dy);
return probableEvent[0];
}
};
// Key code for souceId=1 and 2.
// The value can be obtained by ProbableKeyEventGuesser#getKeycodeMapper() but
// here we don't have available one.
// We should get the value from JapaneseKeyboard after refactoring.
int keyCodeForSourceId1 = 97;
int keyCodeForSourceId2 = 36;
TestData[] testDataList = {
new TestData(
new int[] {},
new float[][] {},
Collections.<ProbableKeyEvent>emptyList()),
new TestData(
new int[] {1},
new float[][] {
{0, 0, 0, 0, 0, 0, 0, 0}},
Collections.<ProbableKeyEvent>emptyList()),
new TestData(
new int[] {1, 2},
new float[][] {
{1f, 0, 0, 0, 0, 0, 0, 0},
{1f, 0, 0, 0, 0, 0, 0, 0}},
Lists.newArrayList(
ProbableKeyEvent.newBuilder()
.setKeyCode(keyCodeForSourceId1).setProbability(1f / 2f).build(),
ProbableKeyEvent.newBuilder()
.setKeyCode(keyCodeForSourceId2).setProbability(1f / 2f).build())),
new TestData(
new int[] {1, 2},
new float[][] {
{1f, 0, 0, 0, 0, 0, 0, 0},
{3f, 0, 0, 0, 0, 0, 0, 0}},
Lists.newArrayList(
ProbableKeyEvent.newBuilder()
.setKeyCode(keyCodeForSourceId1).setProbability(1f / 4f).build(),
ProbableKeyEvent.newBuilder()
.setKeyCode(keyCodeForSourceId2).setProbability(3f / 4f).build())),
// The result of souceId=3 is too small so no corresponding result for it.
new TestData(
new int[] {1, 2, 3},
new float[][] {
{1f, 0, 0, 0, 0, 0, 0, 0},
{1f, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0}},
Lists.newArrayList(
ProbableKeyEvent.newBuilder()
.setKeyCode(keyCodeForSourceId1).setProbability(1f / 2f).build(),
ProbableKeyEvent.newBuilder()
.setKeyCode(keyCodeForSourceId2).setProbability(1f / 2f).build())),
};
for (final TestData testData : testDataList) {
StatsFileAccessor assetManager = new StatsFileAccessor() {
@Override
public InputStream openStream(Keyboard keyboardToLoad, Configuration configurationToLoad) {
assertSame(godanKana, keyboardToLoad);
assertSame(configuration, configurationToLoad);
return createStream(testData.sourceIds, testData.stats);
}
};
ProbableKeyEventGuesser guesser =
new ProbableKeyEventGuesser(
assetManager,
0,
new BlockingThreadPoolExecutor(),
new BlockingExecutor(),
fakeCalculator);
guesser.setConfiguration(Optional.of(configuration));
guesser.setKeyboard(godanKana);
List<TouchEvent> touchEventList =
Collections.singletonList(
TouchEvent.newBuilder()
.addStroke(
TouchPosition.newBuilder()
.setX(startX)
.setY(startY)
.setAction(TouchAction.TOUCH_DOWN))
.addStroke(
TouchPosition.newBuilder()
.setX(startX + deltaX)
.setY(startY + deltaY)
.setAction(TouchAction.TOUCH_UP))
.build());
MoreAsserts.assertContentsInAnyOrder(
testData.toString(),
guesser.getProbableKeyEvents(touchEventList),
testData.expectation.toArray());
}
}
}